Merge "Fix race condition in lowlight trigger logic" into udc-dev
This commit is contained in:
@@ -25,6 +25,7 @@ filegroup {
|
||||
name: "low_light_dream_lib-sources",
|
||||
srcs: [
|
||||
"src/**/*.java",
|
||||
"src/**/*.kt",
|
||||
],
|
||||
path: "src",
|
||||
}
|
||||
@@ -37,10 +38,15 @@ android_library {
|
||||
resource_dirs: [
|
||||
"res",
|
||||
],
|
||||
libs: [
|
||||
"kotlin-annotations",
|
||||
],
|
||||
static_libs: [
|
||||
"androidx.arch.core_core-runtime",
|
||||
"dagger2",
|
||||
"jsr330",
|
||||
"kotlinx-coroutines-android",
|
||||
"kotlinx-coroutines-core",
|
||||
],
|
||||
manifest: "AndroidManifest.xml",
|
||||
plugins: ["dagger2-compiler"],
|
||||
|
||||
@@ -17,4 +17,7 @@
|
||||
<resources>
|
||||
<!-- The dream component used when the device is low light environment. -->
|
||||
<string translatable="false" name="config_lowLightDreamComponent"/>
|
||||
<!-- The max number of milliseconds to wait for the low light transition before setting
|
||||
the system dream component -->
|
||||
<integer name="config_lowLightTransitionTimeoutMs">2000</integer>
|
||||
</resources>
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.dream.lowlight;
|
||||
|
||||
import static com.android.dream.lowlight.dagger.LowLightDreamModule.LOW_LIGHT_DREAM_COMPONENT;
|
||||
|
||||
import android.annotation.IntDef;
|
||||
import android.annotation.RequiresPermission;
|
||||
import android.app.DreamManager;
|
||||
import android.content.ComponentName;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
/**
|
||||
* Maintains the ambient light mode of the environment the device is in, and sets a low light dream
|
||||
* component, if present, as the system dream when the ambient light mode is low light.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public final class LowLightDreamManager {
|
||||
private static final String TAG = "LowLightDreamManager";
|
||||
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef(prefix = { "AMBIENT_LIGHT_MODE_" }, value = {
|
||||
AMBIENT_LIGHT_MODE_UNKNOWN,
|
||||
AMBIENT_LIGHT_MODE_REGULAR,
|
||||
AMBIENT_LIGHT_MODE_LOW_LIGHT
|
||||
})
|
||||
public @interface AmbientLightMode {}
|
||||
|
||||
/**
|
||||
* Constant for ambient light mode being unknown.
|
||||
* @hide
|
||||
*/
|
||||
public static final int AMBIENT_LIGHT_MODE_UNKNOWN = 0;
|
||||
|
||||
/**
|
||||
* Constant for ambient light mode being regular / bright.
|
||||
* @hide
|
||||
*/
|
||||
public static final int AMBIENT_LIGHT_MODE_REGULAR = 1;
|
||||
|
||||
/**
|
||||
* Constant for ambient light mode being low light / dim.
|
||||
* @hide
|
||||
*/
|
||||
public static final int AMBIENT_LIGHT_MODE_LOW_LIGHT = 2;
|
||||
|
||||
private final DreamManager mDreamManager;
|
||||
private final LowLightTransitionCoordinator mLowLightTransitionCoordinator;
|
||||
|
||||
@Nullable
|
||||
private final ComponentName mLowLightDreamComponent;
|
||||
|
||||
private int mAmbientLightMode = AMBIENT_LIGHT_MODE_UNKNOWN;
|
||||
|
||||
@Inject
|
||||
public LowLightDreamManager(
|
||||
DreamManager dreamManager,
|
||||
LowLightTransitionCoordinator lowLightTransitionCoordinator,
|
||||
@Named(LOW_LIGHT_DREAM_COMPONENT) @Nullable ComponentName lowLightDreamComponent) {
|
||||
mDreamManager = dreamManager;
|
||||
mLowLightTransitionCoordinator = lowLightTransitionCoordinator;
|
||||
mLowLightDreamComponent = lowLightDreamComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current ambient light mode.
|
||||
* @hide
|
||||
*/
|
||||
@RequiresPermission(android.Manifest.permission.WRITE_DREAM_STATE)
|
||||
public void setAmbientLightMode(@AmbientLightMode int ambientLightMode) {
|
||||
if (mLowLightDreamComponent == null) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "ignore ambient light mode change because low light dream component "
|
||||
+ "is empty");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (mAmbientLightMode == ambientLightMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "ambient light mode changed from " + mAmbientLightMode + " to "
|
||||
+ ambientLightMode);
|
||||
}
|
||||
|
||||
mAmbientLightMode = ambientLightMode;
|
||||
|
||||
boolean shouldEnterLowLight = mAmbientLightMode == AMBIENT_LIGHT_MODE_LOW_LIGHT;
|
||||
mLowLightTransitionCoordinator.notifyBeforeLowLightTransition(shouldEnterLowLight,
|
||||
() -> mDreamManager.setSystemDreamComponent(
|
||||
shouldEnterLowLight ? mLowLightDreamComponent : null));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.dream.lowlight
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.IntDef
|
||||
import android.annotation.RequiresPermission
|
||||
import android.app.DreamManager
|
||||
import android.content.ComponentName
|
||||
import android.util.Log
|
||||
import com.android.dream.lowlight.dagger.LowLightDreamModule
|
||||
import com.android.dream.lowlight.dagger.qualifiers.Application
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
import kotlin.time.DurationUnit
|
||||
import kotlin.time.toDuration
|
||||
|
||||
/**
|
||||
* Maintains the ambient light mode of the environment the device is in, and sets a low light dream
|
||||
* component, if present, as the system dream when the ambient light mode is low light.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
class LowLightDreamManager @Inject constructor(
|
||||
@Application private val coroutineScope: CoroutineScope,
|
||||
private val dreamManager: DreamManager,
|
||||
private val lowLightTransitionCoordinator: LowLightTransitionCoordinator,
|
||||
@param:Named(LowLightDreamModule.LOW_LIGHT_DREAM_COMPONENT)
|
||||
private val lowLightDreamComponent: ComponentName?,
|
||||
@param:Named(LowLightDreamModule.LOW_LIGHT_TRANSITION_TIMEOUT_MS)
|
||||
private val lowLightTransitionTimeoutMs: Long
|
||||
) {
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
@IntDef(
|
||||
prefix = ["AMBIENT_LIGHT_MODE_"],
|
||||
value = [
|
||||
AMBIENT_LIGHT_MODE_UNKNOWN,
|
||||
AMBIENT_LIGHT_MODE_REGULAR,
|
||||
AMBIENT_LIGHT_MODE_LOW_LIGHT
|
||||
]
|
||||
)
|
||||
annotation class AmbientLightMode
|
||||
|
||||
private var mTransitionJob: Job? = null
|
||||
private var mAmbientLightMode = AMBIENT_LIGHT_MODE_UNKNOWN
|
||||
private val mLowLightTransitionTimeout =
|
||||
lowLightTransitionTimeoutMs.toDuration(DurationUnit.MILLISECONDS)
|
||||
|
||||
/**
|
||||
* Sets the current ambient light mode.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@RequiresPermission(Manifest.permission.WRITE_DREAM_STATE)
|
||||
fun setAmbientLightMode(@AmbientLightMode ambientLightMode: Int) {
|
||||
if (lowLightDreamComponent == null) {
|
||||
if (DEBUG) {
|
||||
Log.d(
|
||||
TAG,
|
||||
"ignore ambient light mode change because low light dream component is empty"
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (mAmbientLightMode == ambientLightMode) {
|
||||
return
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.d(
|
||||
TAG, "ambient light mode changed from $mAmbientLightMode to $ambientLightMode"
|
||||
)
|
||||
}
|
||||
mAmbientLightMode = ambientLightMode
|
||||
val shouldEnterLowLight = mAmbientLightMode == AMBIENT_LIGHT_MODE_LOW_LIGHT
|
||||
|
||||
// Cancel any previous transitions
|
||||
mTransitionJob?.cancel()
|
||||
mTransitionJob = coroutineScope.launch {
|
||||
try {
|
||||
lowLightTransitionCoordinator.waitForLowLightTransitionAnimation(
|
||||
timeout = mLowLightTransitionTimeout,
|
||||
entering = shouldEnterLowLight
|
||||
)
|
||||
} catch (ex: TimeoutCancellationException) {
|
||||
Log.e(TAG, "timed out while waiting for low light animation", ex)
|
||||
}
|
||||
dreamManager.setSystemDreamComponent(
|
||||
if (shouldEnterLowLight) lowLightDreamComponent else null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "LowLightDreamManager"
|
||||
private val DEBUG = Log.isLoggable(TAG, Log.DEBUG)
|
||||
|
||||
/**
|
||||
* Constant for ambient light mode being unknown.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
const val AMBIENT_LIGHT_MODE_UNKNOWN = 0
|
||||
|
||||
/**
|
||||
* Constant for ambient light mode being regular / bright.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
const val AMBIENT_LIGHT_MODE_REGULAR = 1
|
||||
|
||||
/**
|
||||
* Constant for ambient light mode being low light / dim.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
const val AMBIENT_LIGHT_MODE_LOW_LIGHT = 2
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.dream.lowlight;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.annotation.Nullable;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
/**
|
||||
* Helper class that allows listening and running animations before entering or exiting low light.
|
||||
*/
|
||||
@Singleton
|
||||
public class LowLightTransitionCoordinator {
|
||||
/**
|
||||
* Listener that is notified before low light entry.
|
||||
*/
|
||||
public interface LowLightEnterListener {
|
||||
/**
|
||||
* Callback that is notified before the device enters low light.
|
||||
*
|
||||
* @return an optional animator that will be waited upon before entering low light.
|
||||
*/
|
||||
Animator onBeforeEnterLowLight();
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener that is notified before low light exit.
|
||||
*/
|
||||
public interface LowLightExitListener {
|
||||
/**
|
||||
* Callback that is notified before the device exits low light.
|
||||
*
|
||||
* @return an optional animator that will be waited upon before exiting low light.
|
||||
*/
|
||||
Animator onBeforeExitLowLight();
|
||||
}
|
||||
|
||||
private LowLightEnterListener mLowLightEnterListener;
|
||||
private LowLightExitListener mLowLightExitListener;
|
||||
|
||||
@Inject
|
||||
public LowLightTransitionCoordinator() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the listener for the low light enter event.
|
||||
*
|
||||
* Only one listener can be set at a time. This method will overwrite any previously set
|
||||
* listener. Null can be used to unset the listener.
|
||||
*/
|
||||
public void setLowLightEnterListener(@Nullable LowLightEnterListener lowLightEnterListener) {
|
||||
mLowLightEnterListener = lowLightEnterListener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the listener for the low light exit event.
|
||||
*
|
||||
* Only one listener can be set at a time. This method will overwrite any previously set
|
||||
* listener. Null can be used to unset the listener.
|
||||
*/
|
||||
public void setLowLightExitListener(@Nullable LowLightExitListener lowLightExitListener) {
|
||||
mLowLightExitListener = lowLightExitListener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies listeners that the device is about to enter or exit low light.
|
||||
*
|
||||
* @param entering true if listeners should be notified before entering low light, false if this
|
||||
* is notifying before exiting.
|
||||
* @param callback callback that will be run after listeners complete.
|
||||
*/
|
||||
void notifyBeforeLowLightTransition(boolean entering, Runnable callback) {
|
||||
Animator animator = null;
|
||||
|
||||
if (entering && mLowLightEnterListener != null) {
|
||||
animator = mLowLightEnterListener.onBeforeEnterLowLight();
|
||||
} else if (!entering && mLowLightExitListener != null) {
|
||||
animator = mLowLightExitListener.onBeforeExitLowLight();
|
||||
}
|
||||
|
||||
// If the listener returned an animator to indicate it was running an animation, run the
|
||||
// callback after the animation completes, otherwise call the callback directly.
|
||||
if (animator != null) {
|
||||
animator.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animator) {
|
||||
callback.run();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
callback.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.dream.lowlight
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import com.android.dream.lowlight.util.suspendCoroutineWithTimeout
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.time.Duration
|
||||
|
||||
/**
|
||||
* Helper class that allows listening and running animations before entering or exiting low light.
|
||||
*/
|
||||
@Singleton
|
||||
class LowLightTransitionCoordinator @Inject constructor() {
|
||||
/**
|
||||
* Listener that is notified before low light entry.
|
||||
*/
|
||||
interface LowLightEnterListener {
|
||||
/**
|
||||
* Callback that is notified before the device enters low light.
|
||||
*
|
||||
* @return an optional animator that will be waited upon before entering low light.
|
||||
*/
|
||||
fun onBeforeEnterLowLight(): Animator?
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener that is notified before low light exit.
|
||||
*/
|
||||
interface LowLightExitListener {
|
||||
/**
|
||||
* Callback that is notified before the device exits low light.
|
||||
*
|
||||
* @return an optional animator that will be waited upon before exiting low light.
|
||||
*/
|
||||
fun onBeforeExitLowLight(): Animator?
|
||||
}
|
||||
|
||||
private var mLowLightEnterListener: LowLightEnterListener? = null
|
||||
private var mLowLightExitListener: LowLightExitListener? = null
|
||||
|
||||
/**
|
||||
* Sets the listener for the low light enter event.
|
||||
*
|
||||
* Only one listener can be set at a time. This method will overwrite any previously set
|
||||
* listener. Null can be used to unset the listener.
|
||||
*/
|
||||
fun setLowLightEnterListener(lowLightEnterListener: LowLightEnterListener?) {
|
||||
mLowLightEnterListener = lowLightEnterListener
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the listener for the low light exit event.
|
||||
*
|
||||
* Only one listener can be set at a time. This method will overwrite any previously set
|
||||
* listener. Null can be used to unset the listener.
|
||||
*/
|
||||
fun setLowLightExitListener(lowLightExitListener: LowLightExitListener?) {
|
||||
mLowLightExitListener = lowLightExitListener
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies listeners that the device is about to enter or exit low light, and waits for the
|
||||
* animation to complete. If this function is cancelled, the animation is also cancelled.
|
||||
*
|
||||
* @param timeout the maximum duration to wait for the transition animation. If the animation
|
||||
* does not complete within this time period, a
|
||||
* @param entering true if listeners should be notified before entering low light, false if this
|
||||
* is notifying before exiting.
|
||||
*/
|
||||
suspend fun waitForLowLightTransitionAnimation(timeout: Duration, entering: Boolean) =
|
||||
suspendCoroutineWithTimeout(timeout) { continuation ->
|
||||
var animator: Animator? = null
|
||||
if (entering && mLowLightEnterListener != null) {
|
||||
animator = mLowLightEnterListener!!.onBeforeEnterLowLight()
|
||||
} else if (!entering && mLowLightExitListener != null) {
|
||||
animator = mLowLightExitListener!!.onBeforeExitLowLight()
|
||||
}
|
||||
|
||||
if (animator == null) {
|
||||
continuation.resume(Unit)
|
||||
return@suspendCoroutineWithTimeout
|
||||
}
|
||||
|
||||
// If the listener returned an animator to indicate it was running an animation, run the
|
||||
// callback after the animation completes, otherwise call the callback directly.
|
||||
val listener = object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animator: Animator) {
|
||||
continuation.resume(Unit)
|
||||
}
|
||||
|
||||
override fun onAnimationCancel(animation: Animator) {
|
||||
continuation.cancel()
|
||||
}
|
||||
}
|
||||
animator.addListener(listener)
|
||||
continuation.invokeOnCancellation {
|
||||
animator.removeListener(listener)
|
||||
animator.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.dream.lowlight.dagger;
|
||||
|
||||
import android.app.DreamManager;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.android.dream.lowlight.R;
|
||||
|
||||
import javax.inject.Named;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
|
||||
/**
|
||||
* Dagger module for low light dream.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@Module
|
||||
public interface LowLightDreamModule {
|
||||
String LOW_LIGHT_DREAM_COMPONENT = "low_light_dream_component";
|
||||
|
||||
/**
|
||||
* Provides dream manager.
|
||||
*/
|
||||
@Provides
|
||||
static DreamManager providesDreamManager(Context context) {
|
||||
return context.getSystemService(DreamManager.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the component name of the low light dream, or null if not configured.
|
||||
*/
|
||||
@Provides
|
||||
@Named(LOW_LIGHT_DREAM_COMPONENT)
|
||||
@Nullable
|
||||
static ComponentName providesLowLightDreamComponent(Context context) {
|
||||
final String lowLightDreamComponent = context.getResources().getString(
|
||||
R.string.config_lowLightDreamComponent);
|
||||
return lowLightDreamComponent.isEmpty() ? null
|
||||
: ComponentName.unflattenFromString(lowLightDreamComponent);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.dream.lowlight.dagger
|
||||
|
||||
import android.app.DreamManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import com.android.dream.lowlight.R
|
||||
import com.android.dream.lowlight.dagger.qualifiers.Application
|
||||
import com.android.dream.lowlight.dagger.qualifiers.Main
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import javax.inject.Named
|
||||
|
||||
/**
|
||||
* Dagger module for low light dream.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@Module
|
||||
object LowLightDreamModule {
|
||||
/**
|
||||
* Provides dream manager.
|
||||
*/
|
||||
@Provides
|
||||
fun providesDreamManager(context: Context): DreamManager {
|
||||
return requireNotNull(context.getSystemService(DreamManager::class.java))
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the component name of the low light dream, or null if not configured.
|
||||
*/
|
||||
@Provides
|
||||
@Named(LOW_LIGHT_DREAM_COMPONENT)
|
||||
fun providesLowLightDreamComponent(context: Context): ComponentName? {
|
||||
val lowLightDreamComponent = context.resources.getString(
|
||||
R.string.config_lowLightDreamComponent
|
||||
)
|
||||
return if (lowLightDreamComponent.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
ComponentName.unflattenFromString(lowLightDreamComponent)
|
||||
}
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Named(LOW_LIGHT_TRANSITION_TIMEOUT_MS)
|
||||
fun providesLowLightTransitionTimeout(context: Context): Long {
|
||||
return context.resources.getInteger(R.integer.config_lowLightTransitionTimeoutMs).toLong()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Main
|
||||
fun providesMainDispatcher(): CoroutineDispatcher {
|
||||
return Dispatchers.Main.immediate
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Application
|
||||
fun providesApplicationScope(@Main dispatcher: CoroutineDispatcher): CoroutineScope {
|
||||
return CoroutineScope(dispatcher)
|
||||
}
|
||||
|
||||
const val LOW_LIGHT_DREAM_COMPONENT = "low_light_dream_component"
|
||||
const val LOW_LIGHT_TRANSITION_TIMEOUT_MS = "low_light_transition_timeout"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.android.dream.lowlight.dagger.qualifiers
|
||||
|
||||
import android.content.Context
|
||||
import javax.inject.Qualifier
|
||||
|
||||
/**
|
||||
* Used to qualify a context as [Context.getApplicationContext]
|
||||
*/
|
||||
@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class Application
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.android.dream.lowlight.dagger.qualifiers
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
/**
|
||||
* Used to qualify code running on the main thread.
|
||||
*/
|
||||
@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class Main
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.dream.lowlight.util
|
||||
|
||||
import kotlinx.coroutines.CancellableContinuation
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import kotlin.time.Duration
|
||||
|
||||
suspend inline fun <T> suspendCoroutineWithTimeout(
|
||||
timeout: Duration,
|
||||
crossinline block: (CancellableContinuation<T>) -> Unit
|
||||
) = withTimeout(timeout) {
|
||||
suspendCancellableCoroutine(block = block)
|
||||
}
|
||||
@@ -20,6 +20,7 @@ android_test {
|
||||
name: "LowLightDreamTests",
|
||||
srcs: [
|
||||
"**/*.java",
|
||||
"**/*.kt",
|
||||
],
|
||||
static_libs: [
|
||||
"LowLightDreamLib",
|
||||
@@ -28,6 +29,7 @@ android_test {
|
||||
"androidx.test.ext.junit",
|
||||
"frameworks-base-testutils",
|
||||
"junit",
|
||||
"kotlinx_coroutines_test",
|
||||
"mockito-target-extended-minus-junit4",
|
||||
"platform-test-annotations",
|
||||
"testables",
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.dream.lowlight;
|
||||
|
||||
import static com.android.dream.lowlight.LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT;
|
||||
import static com.android.dream.lowlight.LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR;
|
||||
import static com.android.dream.lowlight.LowLightDreamManager.AMBIENT_LIGHT_MODE_UNKNOWN;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.clearInvocations;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import android.app.DreamManager;
|
||||
import android.content.ComponentName;
|
||||
import android.testing.AndroidTestingRunner;
|
||||
|
||||
import androidx.test.filters.SmallTest;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
@SmallTest
|
||||
@RunWith(AndroidTestingRunner.class)
|
||||
public class LowLightDreamManagerTest {
|
||||
@Mock
|
||||
private DreamManager mDreamManager;
|
||||
|
||||
@Mock
|
||||
private LowLightTransitionCoordinator mTransitionCoordinator;
|
||||
|
||||
@Mock
|
||||
private ComponentName mDreamComponent;
|
||||
|
||||
LowLightDreamManager mLowLightDreamManager;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
|
||||
// Automatically run any provided Runnable to mTransitionCoordinator to simplify testing.
|
||||
doAnswer(invocation -> {
|
||||
((Runnable) invocation.getArgument(1)).run();
|
||||
return null;
|
||||
}).when(mTransitionCoordinator).notifyBeforeLowLightTransition(anyBoolean(),
|
||||
any(Runnable.class));
|
||||
|
||||
mLowLightDreamManager = new LowLightDreamManager(mDreamManager, mTransitionCoordinator,
|
||||
mDreamComponent);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setAmbientLightMode_lowLight_setSystemDream() {
|
||||
mLowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT);
|
||||
|
||||
verify(mTransitionCoordinator).notifyBeforeLowLightTransition(eq(true), any());
|
||||
verify(mDreamManager).setSystemDreamComponent(mDreamComponent);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setAmbientLightMode_regularLight_clearSystemDream() {
|
||||
mLowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_REGULAR);
|
||||
|
||||
verify(mTransitionCoordinator).notifyBeforeLowLightTransition(eq(false), any());
|
||||
verify(mDreamManager).setSystemDreamComponent(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setAmbientLightMode_defaultUnknownMode_clearSystemDream() {
|
||||
// Set to low light first.
|
||||
mLowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT);
|
||||
clearInvocations(mDreamManager);
|
||||
|
||||
// Return to default unknown mode.
|
||||
mLowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_UNKNOWN);
|
||||
|
||||
verify(mDreamManager).setSystemDreamComponent(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setAmbientLightMode_dreamComponentNotSet_doNothing() {
|
||||
final LowLightDreamManager lowLightDreamManager = new LowLightDreamManager(mDreamManager,
|
||||
mTransitionCoordinator, null /*dream component*/);
|
||||
|
||||
lowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT);
|
||||
|
||||
verify(mDreamManager, never()).setSystemDreamComponent(any());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.dream.lowlight
|
||||
|
||||
import android.animation.Animator
|
||||
import android.app.DreamManager
|
||||
import android.content.ComponentName
|
||||
import android.testing.AndroidTestingRunner
|
||||
import androidx.test.filters.SmallTest
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.advanceTimeBy
|
||||
import kotlinx.coroutines.test.runCurrent
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito.clearInvocations
|
||||
import org.mockito.Mockito.never
|
||||
import org.mockito.Mockito.times
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.MockitoAnnotations
|
||||
import src.com.android.dream.lowlight.utils.any
|
||||
import src.com.android.dream.lowlight.utils.withArgCaptor
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@SmallTest
|
||||
@RunWith(AndroidTestingRunner::class)
|
||||
class LowLightDreamManagerTest {
|
||||
@Mock
|
||||
private lateinit var mDreamManager: DreamManager
|
||||
@Mock
|
||||
private lateinit var mEnterAnimator: Animator
|
||||
@Mock
|
||||
private lateinit var mExitAnimator: Animator
|
||||
|
||||
private lateinit var mTransitionCoordinator: LowLightTransitionCoordinator
|
||||
private lateinit var mLowLightDreamManager: LowLightDreamManager
|
||||
private lateinit var testScope: TestScope
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
testScope = TestScope(StandardTestDispatcher())
|
||||
|
||||
mTransitionCoordinator = LowLightTransitionCoordinator()
|
||||
mTransitionCoordinator.setLowLightEnterListener(
|
||||
object : LowLightTransitionCoordinator.LowLightEnterListener {
|
||||
override fun onBeforeEnterLowLight() = mEnterAnimator
|
||||
})
|
||||
mTransitionCoordinator.setLowLightExitListener(
|
||||
object : LowLightTransitionCoordinator.LowLightExitListener {
|
||||
override fun onBeforeExitLowLight() = mExitAnimator
|
||||
})
|
||||
|
||||
mLowLightDreamManager = LowLightDreamManager(
|
||||
coroutineScope = testScope,
|
||||
dreamManager = mDreamManager,
|
||||
lowLightTransitionCoordinator = mTransitionCoordinator,
|
||||
lowLightDreamComponent = DREAM_COMPONENT,
|
||||
lowLightTransitionTimeoutMs = LOW_LIGHT_TIMEOUT_MS
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun setAmbientLightMode_lowLight_setSystemDream() = testScope.runTest {
|
||||
mLowLightDreamManager.setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT)
|
||||
runCurrent()
|
||||
verify(mDreamManager, never()).setSystemDreamComponent(DREAM_COMPONENT)
|
||||
completeEnterAnimations()
|
||||
runCurrent()
|
||||
verify(mDreamManager).setSystemDreamComponent(DREAM_COMPONENT)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun setAmbientLightMode_regularLight_clearSystemDream() = testScope.runTest {
|
||||
mLowLightDreamManager.setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR)
|
||||
runCurrent()
|
||||
verify(mDreamManager, never()).setSystemDreamComponent(null)
|
||||
completeExitAnimations()
|
||||
runCurrent()
|
||||
verify(mDreamManager).setSystemDreamComponent(null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun setAmbientLightMode_defaultUnknownMode_clearSystemDream() = testScope.runTest {
|
||||
// Set to low light first.
|
||||
mLowLightDreamManager.setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT)
|
||||
runCurrent()
|
||||
completeEnterAnimations()
|
||||
runCurrent()
|
||||
verify(mDreamManager).setSystemDreamComponent(DREAM_COMPONENT)
|
||||
clearInvocations(mDreamManager)
|
||||
|
||||
// Return to default unknown mode.
|
||||
mLowLightDreamManager.setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_UNKNOWN)
|
||||
runCurrent()
|
||||
completeExitAnimations()
|
||||
runCurrent()
|
||||
verify(mDreamManager).setSystemDreamComponent(null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun setAmbientLightMode_dreamComponentNotSet_doNothing() = testScope.runTest {
|
||||
val lowLightDreamManager = LowLightDreamManager(
|
||||
coroutineScope = testScope,
|
||||
dreamManager = mDreamManager,
|
||||
lowLightTransitionCoordinator = mTransitionCoordinator,
|
||||
lowLightDreamComponent = null,
|
||||
lowLightTransitionTimeoutMs = LOW_LIGHT_TIMEOUT_MS
|
||||
)
|
||||
lowLightDreamManager.setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT)
|
||||
runCurrent()
|
||||
verify(mEnterAnimator, never()).addListener(any())
|
||||
verify(mDreamManager, never()).setSystemDreamComponent(any())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun setAmbientLightMode_multipleTimesBeforeAnimationEnds_cancelsPrevious() = testScope.runTest {
|
||||
mLowLightDreamManager.setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT)
|
||||
runCurrent()
|
||||
// If we reset the light mode back to regular before the previous animation finishes, it
|
||||
// should be ignored.
|
||||
mLowLightDreamManager.setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR)
|
||||
runCurrent()
|
||||
completeEnterAnimations()
|
||||
completeExitAnimations()
|
||||
runCurrent()
|
||||
verify(mDreamManager, times(1)).setSystemDreamComponent(null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun setAmbientLightMode_animatorNeverFinishes_timesOut() = testScope.runTest {
|
||||
mLowLightDreamManager.setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT)
|
||||
advanceTimeBy(delayTimeMillis = LOW_LIGHT_TIMEOUT_MS + 1)
|
||||
// Animation never finishes, but we should still set the system dream
|
||||
verify(mDreamManager).setSystemDreamComponent(DREAM_COMPONENT)
|
||||
}
|
||||
|
||||
private fun completeEnterAnimations() {
|
||||
val listener = withArgCaptor { verify(mEnterAnimator).addListener(capture()) }
|
||||
listener.onAnimationEnd(mEnterAnimator)
|
||||
}
|
||||
|
||||
private fun completeExitAnimations() {
|
||||
val listener = withArgCaptor { verify(mExitAnimator).addListener(capture()) }
|
||||
listener.onAnimationEnd(mExitAnimator)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DREAM_COMPONENT = ComponentName("test_package", "test_dream")
|
||||
private const val LOW_LIGHT_TIMEOUT_MS: Long = 1000
|
||||
}
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.dream.lowlight;
|
||||
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyZeroInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.testing.AndroidTestingRunner;
|
||||
|
||||
import androidx.test.filters.SmallTest;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
@SmallTest
|
||||
@RunWith(AndroidTestingRunner.class)
|
||||
public class LowLightTransitionCoordinatorTest {
|
||||
@Mock
|
||||
private LowLightTransitionCoordinator.LowLightEnterListener mEnterListener;
|
||||
|
||||
@Mock
|
||||
private LowLightTransitionCoordinator.LowLightExitListener mExitListener;
|
||||
|
||||
@Mock
|
||||
private Animator mAnimator;
|
||||
|
||||
@Captor
|
||||
private ArgumentCaptor<Animator.AnimatorListener> mAnimatorListenerCaptor;
|
||||
|
||||
@Mock
|
||||
private Runnable mRunnable;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onEnterCalledOnListeners() {
|
||||
LowLightTransitionCoordinator coordinator = new LowLightTransitionCoordinator();
|
||||
|
||||
coordinator.setLowLightEnterListener(mEnterListener);
|
||||
|
||||
coordinator.notifyBeforeLowLightTransition(true, mRunnable);
|
||||
|
||||
verify(mEnterListener).onBeforeEnterLowLight();
|
||||
verify(mRunnable).run();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onExitCalledOnListeners() {
|
||||
LowLightTransitionCoordinator coordinator = new LowLightTransitionCoordinator();
|
||||
|
||||
coordinator.setLowLightExitListener(mExitListener);
|
||||
|
||||
coordinator.notifyBeforeLowLightTransition(false, mRunnable);
|
||||
|
||||
verify(mExitListener).onBeforeExitLowLight();
|
||||
verify(mRunnable).run();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void listenerNotCalledAfterRemoval() {
|
||||
LowLightTransitionCoordinator coordinator = new LowLightTransitionCoordinator();
|
||||
|
||||
coordinator.setLowLightEnterListener(mEnterListener);
|
||||
coordinator.setLowLightEnterListener(null);
|
||||
|
||||
coordinator.notifyBeforeLowLightTransition(true, mRunnable);
|
||||
|
||||
verifyZeroInteractions(mEnterListener);
|
||||
verify(mRunnable).run();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void runnableCalledAfterAnimationEnds() {
|
||||
when(mEnterListener.onBeforeEnterLowLight()).thenReturn(mAnimator);
|
||||
|
||||
LowLightTransitionCoordinator coordinator = new LowLightTransitionCoordinator();
|
||||
coordinator.setLowLightEnterListener(mEnterListener);
|
||||
|
||||
coordinator.notifyBeforeLowLightTransition(true, mRunnable);
|
||||
|
||||
// Animator listener is added and the runnable is not run yet.
|
||||
verify(mAnimator).addListener(mAnimatorListenerCaptor.capture());
|
||||
verifyZeroInteractions(mRunnable);
|
||||
|
||||
// Runnable is run once the animation ends.
|
||||
mAnimatorListenerCaptor.getValue().onAnimationEnd(null);
|
||||
verify(mRunnable).run();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.dream.lowlight
|
||||
|
||||
import android.animation.Animator
|
||||
import android.testing.AndroidTestingRunner
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.dream.lowlight.LowLightTransitionCoordinator.LowLightEnterListener
|
||||
import com.android.dream.lowlight.LowLightTransitionCoordinator.LowLightExitListener
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.advanceTimeBy
|
||||
import kotlinx.coroutines.test.runCurrent
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.ArgumentCaptor
|
||||
import org.mockito.Captor
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito.never
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.MockitoAnnotations
|
||||
import src.com.android.dream.lowlight.utils.whenever
|
||||
import kotlin.time.DurationUnit
|
||||
import kotlin.time.toDuration
|
||||
|
||||
@SmallTest
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@RunWith(AndroidTestingRunner::class)
|
||||
class LowLightTransitionCoordinatorTest {
|
||||
@Mock
|
||||
private lateinit var mEnterListener: LowLightEnterListener
|
||||
|
||||
@Mock
|
||||
private lateinit var mExitListener: LowLightExitListener
|
||||
|
||||
@Mock
|
||||
private lateinit var mAnimator: Animator
|
||||
|
||||
@Captor
|
||||
private lateinit var mAnimatorListenerCaptor: ArgumentCaptor<Animator.AnimatorListener>
|
||||
|
||||
private lateinit var testScope: TestScope
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
testScope = TestScope(StandardTestDispatcher())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onEnterCalledOnListeners() = testScope.runTest {
|
||||
val coordinator = LowLightTransitionCoordinator()
|
||||
coordinator.setLowLightEnterListener(mEnterListener)
|
||||
val job = launch {
|
||||
coordinator.waitForLowLightTransitionAnimation(timeout = TIMEOUT, entering = true)
|
||||
}
|
||||
runCurrent()
|
||||
verify(mEnterListener).onBeforeEnterLowLight()
|
||||
assertThat(job.isCompleted).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onExitCalledOnListeners() = testScope.runTest {
|
||||
val coordinator = LowLightTransitionCoordinator()
|
||||
coordinator.setLowLightExitListener(mExitListener)
|
||||
val job = launch {
|
||||
coordinator.waitForLowLightTransitionAnimation(timeout = TIMEOUT, entering = false)
|
||||
}
|
||||
runCurrent()
|
||||
verify(mExitListener).onBeforeExitLowLight()
|
||||
assertThat(job.isCompleted).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun listenerNotCalledAfterRemoval() = testScope.runTest {
|
||||
val coordinator = LowLightTransitionCoordinator()
|
||||
coordinator.setLowLightEnterListener(mEnterListener)
|
||||
coordinator.setLowLightEnterListener(null)
|
||||
val job = launch {
|
||||
coordinator.waitForLowLightTransitionAnimation(timeout = TIMEOUT, entering = true)
|
||||
}
|
||||
runCurrent()
|
||||
verify(mEnterListener, never()).onBeforeEnterLowLight()
|
||||
assertThat(job.isCompleted).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun waitsForAnimationToEnd() = testScope.runTest {
|
||||
whenever(mEnterListener.onBeforeEnterLowLight()).thenReturn(mAnimator)
|
||||
val coordinator = LowLightTransitionCoordinator()
|
||||
coordinator.setLowLightEnterListener(mEnterListener)
|
||||
val job = launch {
|
||||
coordinator.waitForLowLightTransitionAnimation(timeout = TIMEOUT, entering = true)
|
||||
}
|
||||
runCurrent()
|
||||
// Animator listener is added and the runnable is not run yet.
|
||||
verify(mAnimator).addListener(mAnimatorListenerCaptor.capture())
|
||||
assertThat(job.isCompleted).isFalse()
|
||||
|
||||
// Runnable is run once the animation ends.
|
||||
mAnimatorListenerCaptor.value.onAnimationEnd(mAnimator)
|
||||
runCurrent()
|
||||
assertThat(job.isCompleted).isTrue()
|
||||
assertThat(job.isCancelled).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun waitsForTimeoutIfAnimationNeverEnds() = testScope.runTest {
|
||||
whenever(mEnterListener.onBeforeEnterLowLight()).thenReturn(mAnimator)
|
||||
val coordinator = LowLightTransitionCoordinator()
|
||||
coordinator.setLowLightEnterListener(mEnterListener)
|
||||
val job = launch {
|
||||
coordinator.waitForLowLightTransitionAnimation(timeout = TIMEOUT, entering = true)
|
||||
}
|
||||
runCurrent()
|
||||
assertThat(job.isCancelled).isFalse()
|
||||
advanceTimeBy(delayTimeMillis = TIMEOUT.inWholeMilliseconds + 1)
|
||||
// If animator doesn't complete within the timeout, we should cancel ourselves.
|
||||
assertThat(job.isCancelled).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shouldCancelIfAnimationIsCancelled() = testScope.runTest {
|
||||
whenever(mEnterListener.onBeforeEnterLowLight()).thenReturn(mAnimator)
|
||||
val coordinator = LowLightTransitionCoordinator()
|
||||
coordinator.setLowLightEnterListener(mEnterListener)
|
||||
val job = launch {
|
||||
coordinator.waitForLowLightTransitionAnimation(timeout = TIMEOUT, entering = true)
|
||||
}
|
||||
runCurrent()
|
||||
// Animator listener is added and the runnable is not run yet.
|
||||
verify(mAnimator).addListener(mAnimatorListenerCaptor.capture())
|
||||
assertThat(job.isCompleted).isFalse()
|
||||
assertThat(job.isCancelled).isFalse()
|
||||
|
||||
// Runnable is run once the animation ends.
|
||||
mAnimatorListenerCaptor.value.onAnimationCancel(mAnimator)
|
||||
runCurrent()
|
||||
assertThat(job.isCompleted).isTrue()
|
||||
assertThat(job.isCancelled).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shouldCancelAnimatorWhenJobCancelled() = testScope.runTest {
|
||||
whenever(mEnterListener.onBeforeEnterLowLight()).thenReturn(mAnimator)
|
||||
val coordinator = LowLightTransitionCoordinator()
|
||||
coordinator.setLowLightEnterListener(mEnterListener)
|
||||
val job = launch {
|
||||
coordinator.waitForLowLightTransitionAnimation(timeout = TIMEOUT, entering = true)
|
||||
}
|
||||
runCurrent()
|
||||
// Animator listener is added and the runnable is not run yet.
|
||||
verify(mAnimator).addListener(mAnimatorListenerCaptor.capture())
|
||||
verify(mAnimator, never()).cancel()
|
||||
assertThat(job.isCompleted).isFalse()
|
||||
|
||||
job.cancel()
|
||||
// We should have removed the listener and cancelled the animator
|
||||
verify(mAnimator).removeListener(mAnimatorListenerCaptor.value)
|
||||
verify(mAnimator).cancel()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TIMEOUT = 1.toDuration(DurationUnit.SECONDS)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package src.com.android.dream.lowlight.utils
|
||||
|
||||
import org.mockito.ArgumentCaptor
|
||||
import org.mockito.ArgumentMatcher
|
||||
import org.mockito.Mockito
|
||||
import org.mockito.Mockito.`when`
|
||||
import org.mockito.stubbing.OngoingStubbing
|
||||
import org.mockito.stubbing.Stubber
|
||||
|
||||
/**
|
||||
* Returns Mockito.eq() as nullable type to avoid java.lang.IllegalStateException when
|
||||
* null is returned.
|
||||
*
|
||||
* Generic T is nullable because implicitly bounded by Any?.
|
||||
*/
|
||||
fun <T> eq(obj: T): T = Mockito.eq<T>(obj)
|
||||
|
||||
/**
|
||||
* Returns Mockito.any() as nullable type to avoid java.lang.IllegalStateException when
|
||||
* null is returned.
|
||||
*
|
||||
* Generic T is nullable because implicitly bounded by Any?.
|
||||
*/
|
||||
fun <T> any(type: Class<T>): T = Mockito.any<T>(type)
|
||||
inline fun <reified T> any(): T = any(T::class.java)
|
||||
|
||||
/**
|
||||
* Returns Mockito.argThat() as nullable type to avoid java.lang.IllegalStateException when
|
||||
* null is returned.
|
||||
*
|
||||
* Generic T is nullable because implicitly bounded by Any?.
|
||||
*/
|
||||
fun <T> argThat(matcher: ArgumentMatcher<T>): T = Mockito.argThat(matcher)
|
||||
|
||||
/**
|
||||
* Kotlin type-inferred version of Mockito.nullable()
|
||||
*/
|
||||
inline fun <reified T> nullable(): T? = Mockito.nullable(T::class.java)
|
||||
|
||||
/**
|
||||
* Returns ArgumentCaptor.capture() as nullable type to avoid java.lang.IllegalStateException
|
||||
* when null is returned.
|
||||
*
|
||||
* Generic T is nullable because implicitly bounded by Any?.
|
||||
*/
|
||||
fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
|
||||
|
||||
/**
|
||||
* Helper function for creating an argumentCaptor in kotlin.
|
||||
*
|
||||
* Generic T is nullable because implicitly bounded by Any?.
|
||||
*/
|
||||
inline fun <reified T : Any> argumentCaptor(): ArgumentCaptor<T> =
|
||||
ArgumentCaptor.forClass(T::class.java)
|
||||
|
||||
/**
|
||||
* Helper function for creating new mocks, without the need to pass in a [Class] instance.
|
||||
*
|
||||
* Generic T is nullable because implicitly bounded by Any?.
|
||||
*
|
||||
* @param apply builder function to simplify stub configuration by improving type inference.
|
||||
*/
|
||||
inline fun <reified T : Any> mock(apply: T.() -> Unit = {}): T = Mockito.mock(T::class.java)
|
||||
.apply(apply)
|
||||
|
||||
/**
|
||||
* Helper function for stubbing methods without the need to use backticks.
|
||||
*
|
||||
* @see Mockito.when
|
||||
*/
|
||||
fun <T> whenever(methodCall: T): OngoingStubbing<T> = `when`(methodCall)
|
||||
fun <T> Stubber.whenever(mock: T): T = `when`(mock)
|
||||
|
||||
/**
|
||||
* A kotlin implemented wrapper of [ArgumentCaptor] which prevents the following exception when
|
||||
* kotlin tests are mocking kotlin objects and the methods take non-null parameters:
|
||||
*
|
||||
* java.lang.NullPointerException: capture() must not be null
|
||||
*/
|
||||
class KotlinArgumentCaptor<T> constructor(clazz: Class<T>) {
|
||||
private val wrapped: ArgumentCaptor<T> = ArgumentCaptor.forClass(clazz)
|
||||
fun capture(): T = wrapped.capture()
|
||||
val value: T
|
||||
get() = wrapped.value
|
||||
val allValues: List<T>
|
||||
get() = wrapped.allValues
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for creating an argumentCaptor in kotlin.
|
||||
*
|
||||
* Generic T is nullable because implicitly bounded by Any?.
|
||||
*/
|
||||
inline fun <reified T : Any> kotlinArgumentCaptor(): KotlinArgumentCaptor<T> =
|
||||
KotlinArgumentCaptor(T::class.java)
|
||||
|
||||
/**
|
||||
* Helper function for creating and using a single-use ArgumentCaptor in kotlin.
|
||||
*
|
||||
* val captor = argumentCaptor<Foo>()
|
||||
* verify(...).someMethod(captor.capture())
|
||||
* val captured = captor.value
|
||||
*
|
||||
* becomes:
|
||||
*
|
||||
* val captured = withArgCaptor<Foo> { verify(...).someMethod(capture()) }
|
||||
*
|
||||
* NOTE: this uses the KotlinArgumentCaptor to avoid the NullPointerException.
|
||||
*/
|
||||
inline fun <reified T : Any> withArgCaptor(block: KotlinArgumentCaptor<T>.() -> Unit): T =
|
||||
kotlinArgumentCaptor<T>().apply { block() }.value
|
||||
|
||||
/**
|
||||
* Variant of [withArgCaptor] for capturing multiple arguments.
|
||||
*
|
||||
* val captor = argumentCaptor<Foo>()
|
||||
* verify(...).someMethod(captor.capture())
|
||||
* val captured: List<Foo> = captor.allValues
|
||||
*
|
||||
* becomes:
|
||||
*
|
||||
* val capturedList = captureMany<Foo> { verify(...).someMethod(capture()) }
|
||||
*/
|
||||
inline fun <reified T : Any> captureMany(block: KotlinArgumentCaptor<T>.() -> Unit): List<T> =
|
||||
kotlinArgumentCaptor<T>().apply{ block() }.allValues
|
||||
Reference in New Issue
Block a user