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

Commit d7e0295d authored by Daniel Norman's avatar Daniel Norman Committed by Candice Lo
Browse files

feat(force invert): Check the window background color for ForceInvert

Check the window background color before force-inverting an app

Bug: 372558459
Flag: android.view.accessibility.force_invert_color
Test: atest ViewRootImplTest
Change-Id: I608de6284ccf553f95b3452102b5c06d348df3e4
parent 90670ab7
Loading
Loading
Loading
Loading
+15 −4
Original line number Diff line number Diff line
@@ -189,6 +189,7 @@ import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Region;
import android.graphics.RenderNode;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.hardware.SyncFence;
@@ -292,6 +293,7 @@ import com.android.internal.os.SomeArgs;
import com.android.internal.policy.DecorView;
import com.android.internal.policy.PhoneFallbackEventHandler;
import com.android.internal.protolog.ProtoLog;
import com.android.internal.util.ContrastColorUtil;
import com.android.internal.util.FastPrintWriter;
import com.android.internal.view.BaseSurfaceHolder;
import com.android.internal.view.RootViewSurfaceTaker;
@@ -2078,12 +2080,21 @@ public final class ViewRootImpl implements ViewParent,
                // preference for dark mode in configuration.uiMode. Instead, we assume that both
                // force invert and the system's dark theme are enabled.
                if (shouldApplyForceInvertDark()) {
                    final boolean isLightTheme =
                        a.getBoolean(R.styleable.Theme_isLightTheme, false);
                    // TODO: b/372558459 - Also check the background ColorDrawable color lightness
                    // TODO: b/368725782 - Use hwui color area detection instead of / in
                    //  addition to these heuristics.
                    if (isLightTheme) {
                    final boolean isLightTheme =
                            a.getBoolean(R.styleable.Theme_isLightTheme, false);
                    final boolean isBackgroundColorLight;
                    if (mView != null && mView.getBackground()
                            instanceof ColorDrawable colorDrawable) {
                        isBackgroundColorLight =
                                !ContrastColorUtil.isColorDarkLab(colorDrawable.getColor());
                    } else {
                        // Treat unknown as light, so that only isLightTheme is used to determine
                        // force dark treatment.
                        isBackgroundColorLight = true;
                    }
                    if (isLightTheme && isBackgroundColorLight) {
                        return ForceDarkType.FORCE_INVERT_COLOR_DARK;
                    } else {
                        return ForceDarkType.NONE;
+7 −2
Original line number Diff line number Diff line
@@ -41,8 +41,6 @@ import android.text.style.TextAppearanceSpan;
import android.util.Log;
import android.util.Pair;

import com.android.internal.annotations.VisibleForTesting;

import java.util.Arrays;
import java.util.WeakHashMap;

@@ -381,6 +379,13 @@ public class ContrastColorUtil {
        return calculateLuminance(color) <= 0.17912878474;
    }

    /** Like {@link #isColorDark(int)} but converts to LAB before checking the L component. */
    public static boolean isColorDarkLab(int color) {
        final double[] result = ColorUtilsFromCompat.getTempDouble3Array();
        ColorUtilsFromCompat.colorToLAB(color, result);
        return result[0] < 50;
    }

    private int processColor(int color) {
        return Color.argb(Color.alpha(color),
                255 - Color.red(color),
+97 −44
Original line number Diff line number Diff line
@@ -16,8 +16,6 @@

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;
@@ -69,10 +67,9 @@ 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.Color;
import android.graphics.ForceDarkType;
import android.graphics.ForceDarkType.ForceDarkTypeDef;
import android.graphics.Rect;
import android.hardware.display.DisplayManagerGlobal;
import android.os.Binder;
@@ -101,8 +98,6 @@ 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;
@@ -131,8 +126,6 @@ public class ViewRootImplTest {

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

    private ViewRootImpl mViewRootImpl;
    private View mView;
@@ -1516,29 +1509,83 @@ public class ViewRootImplTest {
    }

    @Test
    @RequiresFlagsEnabled(FLAG_FORCE_INVERT_COLOR)
    public void updateConfiguration_returnsExpectedForceDarkMode() {
    @EnableFlags(FLAG_FORCE_INVERT_COLOR)
    public void determineForceDarkType_systemLightMode_returnsNone() throws Exception {
        waitForSystemNightModeActivated(false);

        TestUtils.waitUntil("Waiting for ForceDarkType to be ready",
                () -> (mViewRootImpl.determineForceDarkType() == ForceDarkType.NONE));

    }

    @Test
    @EnableFlags(FLAG_FORCE_INVERT_COLOR)
    public void determineForceDarkType_systemNightModeAndDisableForceInvertColor_returnsNone()
            throws Exception {
        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);
        enableForceInvertColor(false);

        waitForSystemNightModeActivated(false);
        TestUtils.waitUntil("Waiting for ForceDarkType to be ready",
                () -> (mViewRootImpl.determineForceDarkType() == ForceDarkType.NONE));
    }

        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
            determineForceDarkType_isLightThemeAndIsLightBackground_returnsForceInvertColorDark()
            throws Exception {
        // Set up configurations for force invert color
        waitForSystemNightModeActivated(true);
        enableForceInvertColor(true);

        setUpViewAttributes(/* isLightTheme= */ true, /* isLightBackground = */ true);

        TestUtils.waitUntil("Waiting for ForceDarkType to be ready",
                () -> (mViewRootImpl.determineForceDarkType()
                        == ForceDarkType.FORCE_INVERT_COLOR_DARK));
    }

    @Test
    @EnableFlags(FLAG_FORCE_INVERT_COLOR)
    public void determineForceDarkType_isLightThemeAndNotLightBackground_returnsNone()
            throws Exception {
        // Set up configurations for force invert color
        waitForSystemNightModeActivated(true);
        enableForceInvertColor(true);

        setUpViewAttributes(/* isLightTheme= */ true, /* isLightBackground = */ false);

        TestUtils.waitUntil("Waiting for ForceDarkType to be ready",
                () -> (mViewRootImpl.determineForceDarkType() == ForceDarkType.NONE));
    }

    @Test
    @EnableFlags(FLAG_FORCE_INVERT_COLOR)
    public void determineForceDarkType_notLightThemeAndIsLightBackground_returnsNone()
            throws Exception {
        // Set up configurations for force invert color
        waitForSystemNightModeActivated(true);
        enableForceInvertColor(true);

        setUpViewAttributes(/* isLightTheme= */ false, /* isLightBackground = */ true);

        TestUtils.waitUntil("Waiting for ForceDarkType to be ready",
                () -> (mViewRootImpl.determineForceDarkType() == ForceDarkType.NONE));
    }

    @Test
    @EnableFlags(FLAG_FORCE_INVERT_COLOR)
    public void determineForceDarkType_notLightThemeAndNotLightBackground_returnsNone()
            throws Exception {
        // Set up configurations for force invert color
        waitForSystemNightModeActivated(true);
        enableForceInvertColor(true);

        setUpViewAttributes(/* isLightTheme= */ false, /* isLightBackground = */ false);

        TestUtils.waitUntil("Waiting for ForceDarkType to be ready",
                () -> (mViewRootImpl.determineForceDarkType() == ForceDarkType.NONE));
    }

    @Test
@@ -1792,29 +1839,35 @@ public class ViewRootImplTest {
        sInstrumentation.waitForIdleSync();
    }

    private void verifyForceDarkType(boolean isAppInNightMode, boolean isForceInvertEnabled,
            @ForceInvertType int expectedForceInvertType,
            @ForceDarkTypeDef int expectedForceDarkType) {
        var uiModeManager = sContext.getSystemService(UiModeManager.class);
    private void enableForceInvertColor(boolean enabled) {
        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);
                    enabled ? 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);
    private void setUpViewAttributes(boolean isLightTheme, boolean isLightBackground) {
        ShellIdentityUtils.invokeWithShellPermissions(() -> {
            sContext.setTheme(isLightTheme ? android.R.style.Theme_DeviceDefault_Light
                    : android.R.style.Theme_DeviceDefault);
        });

        sInstrumentation.runOnMainSync(() -> {
            View view = new View(sContext);
            WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(
                    TYPE_APPLICATION_OVERLAY);
            layoutParams.token = new Binder();
            view.setLayoutParams(layoutParams);
            if (isLightBackground) {
                view.setBackgroundColor(Color.WHITE);
            } else {
                view.setBackgroundColor(Color.BLACK);
            }
            mViewRootImpl.setView(view, layoutParams, /* panelParentView= */ null);
            mViewRootImpl.updateConfiguration(sContext.getDisplayNoVerify().getDisplayId());
        });
    }
}