Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f0cba6ab authored by Lucas Silva's avatar Lucas Silva Committed by Android (Google) Code Review
Browse files

Merge "Fix race condition in lowlight trigger logic" into udc-dev

parents ea2fee7c 6dd113cc
Loading
Loading
Loading
Loading
+6 −0
Original line number Diff line number Diff line
@@ -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"],
+3 −0
Original line number Diff line number Diff line
@@ -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>
+0 −122
Original line number Diff line number Diff line
/*
 * 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));
    }
}
+138 −0
Original line number Diff line number Diff line
/*
 * 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
    }
}
+118 −0
Original line number Diff line number Diff line
@@ -13,51 +13,47 @@
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.dream.lowlight

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;
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
public class LowLightTransitionCoordinator {
class LowLightTransitionCoordinator @Inject constructor() {
    /**
     * Listener that is notified before low light entry.
     */
    public interface LowLightEnterListener {
    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();
        fun onBeforeEnterLowLight(): Animator?
    }

    /**
     * Listener that is notified before low light exit.
     */
    public interface LowLightExitListener {
    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();
        fun onBeforeExitLowLight(): Animator?
    }

    private LowLightEnterListener mLowLightEnterListener;
    private LowLightExitListener mLowLightExitListener;

    @Inject
    public LowLightTransitionCoordinator() {
    }
    private var mLowLightEnterListener: LowLightEnterListener? = null
    private var mLowLightExitListener: LowLightExitListener? = null

    /**
     * Sets the listener for the low light enter event.
@@ -65,8 +61,8 @@ public class LowLightTransitionCoordinator {
     * 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;
    fun setLowLightEnterListener(lowLightEnterListener: LowLightEnterListener?) {
        mLowLightEnterListener = lowLightEnterListener
    }

    /**
@@ -75,37 +71,48 @@ public class LowLightTransitionCoordinator {
     * 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;
    fun setLowLightExitListener(lowLightExitListener: LowLightExitListener?) {
        mLowLightExitListener = lowLightExitListener
    }

    /**
     * Notifies listeners that the device is about to enter or exit low light.
     * 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.
     * @param callback callback that will be run after listeners complete.
     */
    void notifyBeforeLowLightTransition(boolean entering, Runnable callback) {
        Animator animator = null;

    suspend fun waitForLowLightTransitionAnimation(timeout: Duration, entering: Boolean) =
        suspendCoroutineWithTimeout(timeout) { continuation ->
            var animator: Animator? = null
            if (entering && mLowLightEnterListener != null) {
            animator = mLowLightEnterListener.onBeforeEnterLowLight();
                animator = mLowLightEnterListener!!.onBeforeEnterLowLight()
            } else if (!entering && mLowLightExitListener != null) {
            animator = mLowLightExitListener.onBeforeExitLowLight();
                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.
        if (animator != null) {
            animator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animator) {
                    callback.run();
            val listener = object : AnimatorListenerAdapter() {
                override fun onAnimationEnd(animator: Animator) {
                    continuation.resume(Unit)
                }

                override fun onAnimationCancel(animation: Animator) {
                    continuation.cancel()
                }
            }
            });
        } else {
            callback.run();
            animator.addListener(listener)
            continuation.invokeOnCancellation {
                animator.removeListener(listener)
                animator.cancel()
            }
        }
}
Loading