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

Commit 14a06552 authored by Menghan Li's avatar Menghan Li
Browse files

feat(EDT): Update the force dark theme trigger condition

The original method of observing UiModeManager#ForceInvertChangeListener
led to memory leaks due to frequent activity changes. Switching to a
settings key observer provides a more stable and leak-free solution.

Bug: 368721320
Flag: android.view.accessibility.force_invert_color
Test: atest ViewRootImplTest
Change-Id: I99ea786e393c0945c3a1aee41a1e8240f94223e3
parent 8ea1586b
Loading
Loading
Loading
Loading
+33 −32
Original line number Diff line number Diff line
@@ -152,6 +152,7 @@ import android.annotation.UiContext;
import android.app.ActivityManager;
import android.app.ActivityThread;
import android.app.ResourcesManager;
import android.app.UiModeManager;
import android.app.WindowConfiguration;
import android.app.compat.CompatChanges;
import android.app.servertransaction.WindowStateTransactionItem;
@@ -195,6 +196,7 @@ import android.hardware.display.DisplayManagerGlobal;
import android.hardware.input.InputManagerGlobal;
import android.hardware.input.InputSettings;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
@@ -471,8 +473,6 @@ public final class ViewRootImpl implements ViewParent,
    @Nullable
    private ContentObserver mForceInvertObserver;
    private static final int INVALID_VALUE = Integer.MIN_VALUE;
    private int mForceInvertEnabled = INVALID_VALUE;
    /**
     * Callback for notifying about global configuration changes.
     */
@@ -554,6 +554,8 @@ public final class ViewRootImpl implements ViewParent,
    @UiContext
    public final Context mContext;
    private UiModeManager mUiModeManager;
    @UnsupportedAppUsage
    final IWindowSession mWindowSession;
    @NonNull Display mDisplay;
@@ -1803,23 +1805,6 @@ public final class ViewRootImpl implements ViewParent,
        }
    }
    private boolean isForceInvertEnabled() {
        if (mForceInvertEnabled == INVALID_VALUE) {
            reloadForceInvertEnabled();
        }
        return mForceInvertEnabled == 1;
    }
    private void reloadForceInvertEnabled() {
        if (forceInvertColor()) {
            mForceInvertEnabled = Settings.Secure.getIntForUser(
                    mContext.getContentResolver(),
                    Settings.Secure.ACCESSIBILITY_FORCE_INVERT_COLOR_ENABLED,
                    /* def= */ 0,
                    UserHandle.myUserId());
        }
    }
    /**
     * Register any kind of listeners if setView was success.
     */
@@ -1855,20 +1840,25 @@ public final class ViewRootImpl implements ViewParent,
                mForceInvertObserver = new ContentObserver(mHandler) {
                    @Override
                    public void onChange(boolean selfChange) {
                        reloadForceInvertEnabled();
                        updateForceDarkMode();
                    }
                };
                mContext.getContentResolver().registerContentObserver(
                final Uri[] urisToObserve = {
                    Settings.Secure.getUriFor(
                                Settings.Secure.ACCESSIBILITY_FORCE_INVERT_COLOR_ENABLED
                        ),
                        Settings.Secure.ACCESSIBILITY_FORCE_INVERT_COLOR_ENABLED),
                    Settings.Secure.getUriFor(Settings.Secure.UI_NIGHT_MODE)
                };
                for (Uri uri : urisToObserve) {
                    mContext.getContentResolver().registerContentObserver(
                            uri,
                            false,
                            mForceInvertObserver,
                            UserHandle.myUserId());
                }
            }
        }
    }
    /**
     * Unregister all listeners while detachedFromWindow.
@@ -2072,21 +2062,25 @@ public final class ViewRootImpl implements ViewParent,
        return getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
    }
    /** Returns true if force dark should be enabled according to various settings */
    /**
     * Determines the type of force dark to apply, considering force inversion, system night mode,
     * and app-specific settings (including developer opt-outs).
     *
     * @return A {@link ForceDarkType.ForceDarkTypeDef} constant indicating the force dark type.
     */
    @VisibleForTesting
    public @ForceDarkType.ForceDarkTypeDef int determineForceDarkType() {
        if (forceInvertColor()) {
            // Force invert ignores all developer opt-outs.
            // We also ignore dark theme, since the app developer can override the user's preference
            // for dark mode in configuration.uiMode. Instead, we assume that the force invert
            // setting will be enabled at the same time dark theme is in the Settings app.
            if (isForceInvertEnabled()) {
            // for dark mode in configuration.uiMode. Instead, we assume that both force invert and
            // the system's dark theme are enabled.
            if (getUiModeManager().getForceInvertState() == UiModeManager.FORCE_INVERT_TYPE_DARK) {
                return ForceDarkType.FORCE_INVERT_COLOR_DARK;
            }
        }
        boolean useAutoDark = getNightMode() == Configuration.UI_MODE_NIGHT_YES;
        if (useAutoDark) {
            boolean forceDarkAllowedDefault =
                    SystemProperties.getBoolean(ThreadedRenderer.DEBUG_FORCE_DARK, false);
@@ -9391,6 +9385,13 @@ public final class ViewRootImpl implements ViewParent,
        return mAudioManager;
    }
    private UiModeManager getUiModeManager() {
        if (mUiModeManager == null) {
            mUiModeManager = mContext.getSystemService(UiModeManager.class);
        }
        return mUiModeManager;
    }
    private Vibrator getSystemVibrator() {
        if (mVibrator == null) {
            mVibrator = mContext.getSystemService(Vibrator.class);
+71 −44
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package android.view;

import static android.app.UiModeManager.MODE_NIGHT_NO;
import static android.app.UiModeManager.MODE_NIGHT_YES;
import static android.util.SequenceUtils.getInitSeq;
import static android.view.HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING;
import static android.view.InputDevice.SOURCE_ROTARY_ENCODER;
@@ -67,8 +69,10 @@ import static org.junit.Assume.assumeTrue;
import android.annotation.NonNull;
import android.app.Instrumentation;
import android.app.UiModeManager;
import android.app.UiModeManager.ForceInvertType;
import android.content.Context;
import android.graphics.ForceDarkType;
import android.graphics.ForceDarkType.ForceDarkTypeDef;
import android.graphics.Rect;
import android.hardware.display.DisplayManagerGlobal;
import android.os.Binder;
@@ -93,9 +97,12 @@ import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;

import com.android.compatibility.common.util.ShellIdentityUtils;
import com.android.compatibility.common.util.TestUtils;
import com.android.cts.input.BlockingQueueEventVerifier;
import com.android.window.flags.Flags;

import com.google.common.truth.Expect;

import org.hamcrest.Matcher;
import org.junit.After;
import org.junit.AfterClass;
@@ -124,6 +131,8 @@ public class ViewRootImplTest {

    @Rule
    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
    @Rule
    public final Expect mExpect = Expect.create();

    private ViewRootImpl mViewRootImpl;
    private View mView;
@@ -1507,49 +1516,34 @@ public class ViewRootImplTest {
    }

    @Test
    public void forceInvertOffDarkThemeOff_forceDarkModeDisabled() {
        mSetFlagsRule.enableFlags(FLAG_FORCE_INVERT_COLOR);
        ShellIdentityUtils.invokeWithShellPermissions(() -> {
            Settings.Secure.putInt(
                    sContext.getContentResolver(),
                    Settings.Secure.ACCESSIBILITY_FORCE_INVERT_COLOR_ENABLED,
                    /* value= */ 0
            );
            var uiModeManager = sContext.getSystemService(UiModeManager.class);
            uiModeManager.setNightMode(UiModeManager.MODE_NIGHT_NO);
        });

        sInstrumentation.runOnMainSync(() ->
                mViewRootImpl.updateConfiguration(sContext.getDisplayNoVerify().getDisplayId())
        );

        assertThat(mViewRootImpl.determineForceDarkType()).isEqualTo(ForceDarkType.NONE);
    }

    @Test
    public void forceInvertOnDarkThemeOff_forceDarkModeEnabled() {
        mSetFlagsRule.enableFlags(FLAG_FORCE_INVERT_COLOR);
        ShellIdentityUtils.invokeWithShellPermissions(() -> {
            Settings.Secure.putInt(
                    sContext.getContentResolver(),
                    Settings.Secure.ACCESSIBILITY_FORCE_INVERT_COLOR_ENABLED,
                    /* value= */ 1
            );
            var uiModeManager = sContext.getSystemService(UiModeManager.class);
            uiModeManager.setNightMode(UiModeManager.MODE_NIGHT_NO);
        });

        sInstrumentation.runOnMainSync(() ->
                mViewRootImpl.updateConfiguration(sContext.getDisplayNoVerify().getDisplayId())
        );

        assertThat(mViewRootImpl.determineForceDarkType())
                .isEqualTo(ForceDarkType.FORCE_INVERT_COLOR_DARK);
    @RequiresFlagsEnabled(FLAG_FORCE_INVERT_COLOR)
    public void updateConfiguration_returnsExpectedForceDarkMode() {
        waitForSystemNightModeActivated(true);

        verifyForceDarkType(/* isAppInNightMode= */ true, /* isForceInvertEnabled= */ true,
                UiModeManager.FORCE_INVERT_TYPE_DARK, ForceDarkType.FORCE_INVERT_COLOR_DARK);
        verifyForceDarkType(/* isAppInNightMode= */ true, /* isForceInvertEnabled= */ false,
                UiModeManager.FORCE_INVERT_TYPE_OFF, ForceDarkType.NONE);
        verifyForceDarkType(/* isAppInNightMode= */ false, /* isForceInvertEnabled= */ true,
                UiModeManager.FORCE_INVERT_TYPE_DARK, ForceDarkType.FORCE_INVERT_COLOR_DARK);
        verifyForceDarkType(/* isAppInNightMode= */ false, /* isForceInvertEnabled= */ false,
                UiModeManager.FORCE_INVERT_TYPE_OFF, ForceDarkType.NONE);

        waitForSystemNightModeActivated(false);

        verifyForceDarkType(/* isAppInNightMode= */ true, /* isForceInvertEnabled= */ true,
                UiModeManager.FORCE_INVERT_TYPE_OFF, ForceDarkType.NONE);
        verifyForceDarkType(/* isAppInNightMode= */ true, /* isForceInvertEnabled= */ false,
                UiModeManager.FORCE_INVERT_TYPE_OFF, ForceDarkType.NONE);
        verifyForceDarkType(/* isAppInNightMode= */ false, /* isForceInvertEnabled= */ true,
                UiModeManager.FORCE_INVERT_TYPE_OFF, ForceDarkType.NONE);
        verifyForceDarkType(/* isAppInNightMode= */ false, /* isForceInvertEnabled= */ false,
                UiModeManager.FORCE_INVERT_TYPE_OFF, ForceDarkType.NONE);
    }

    @Test
    @EnableFlags(FLAG_FORCE_INVERT_COLOR)
    public void forceInvertOffForceDarkOff_forceDarkModeDisabled() {
        mSetFlagsRule.enableFlags(FLAG_FORCE_INVERT_COLOR);
        ShellIdentityUtils.invokeWithShellPermissions(() -> {
            Settings.Secure.putInt(
                    sContext.getContentResolver(),
@@ -1562,15 +1556,14 @@ public class ViewRootImplTest {
        });

        sInstrumentation.runOnMainSync(() ->
                mViewRootImpl.updateConfiguration(sContext.getDisplayNoVerify().getDisplayId())
        );
                mViewRootImpl.updateConfiguration(sContext.getDisplayNoVerify().getDisplayId()));

        assertThat(mViewRootImpl.determineForceDarkType()).isEqualTo(ForceDarkType.NONE);
    }

    @Test
    @EnableFlags(FLAG_FORCE_INVERT_COLOR)
    public void forceInvertOffForceDarkOn_forceDarkModeEnabled() {
        mSetFlagsRule.enableFlags(FLAG_FORCE_INVERT_COLOR);
        ShellIdentityUtils.invokeWithShellPermissions(() -> {
            Settings.Secure.putInt(
                    sContext.getContentResolver(),
@@ -1582,8 +1575,7 @@ public class ViewRootImplTest {
        });

        sInstrumentation.runOnMainSync(() ->
                mViewRootImpl.updateConfiguration(sContext.getDisplayNoVerify().getDisplayId())
        );
                mViewRootImpl.updateConfiguration(sContext.getDisplayNoVerify().getDisplayId()));

        assertThat(mViewRootImpl.determineForceDarkType()).isEqualTo(ForceDarkType.FORCE_DARK);
    }
@@ -1790,4 +1782,39 @@ public class ViewRootImplTest {
                    () -> view.getViewTreeObserver().removeOnDrawListener(listener));
        }
    }

    private void waitForSystemNightModeActivated(boolean active) {
        ShellIdentityUtils.invokeWithShellPermissions(() ->
                sInstrumentation.runOnMainSync(() -> {
                    var uiModeManager = sContext.getSystemService(UiModeManager.class);
                    uiModeManager.setNightModeActivated(active);
                }));
        sInstrumentation.waitForIdleSync();
    }

    private void verifyForceDarkType(boolean isAppInNightMode, boolean isForceInvertEnabled,
            @ForceInvertType int expectedForceInvertType,
            @ForceDarkTypeDef int expectedForceDarkType) {
        var uiModeManager = sContext.getSystemService(UiModeManager.class);
        ShellIdentityUtils.invokeWithShellPermissions(() -> {
            uiModeManager.setApplicationNightMode(
                    isAppInNightMode ? MODE_NIGHT_YES : MODE_NIGHT_NO);
            Settings.Secure.putInt(
                    sContext.getContentResolver(),
                    Settings.Secure.ACCESSIBILITY_FORCE_INVERT_COLOR_ENABLED,
                    isForceInvertEnabled ? 1 : 0);
        });

        sInstrumentation.runOnMainSync(() ->
                mViewRootImpl.updateConfiguration(sContext.getDisplayNoVerify().getDisplayId()));
        try {
            TestUtils.waitUntil("Waiting for force invert state changed",
                    () -> (uiModeManager.getForceInvertState() == expectedForceInvertType));
        } catch (Exception e) {
            Log.e(TAG, "Unexpected error trying to apply force invert state. " + e);
            e.printStackTrace();
        }

        mExpect.that(mViewRootImpl.determineForceDarkType()).isEqualTo(expectedForceDarkType);
    }
}