Loading core/java/android/app/UiModeManager.java +17 −0 Original line number Diff line number Diff line Loading @@ -50,6 +50,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.ref.WeakReference; import java.time.LocalTime; import java.util.Arrays; import java.util.Comparator; import java.util.List; import java.util.Map; Loading Loading @@ -1559,6 +1560,22 @@ public class UiModeManager { return sGlobals.getForceInvertState(); } /** * Returns {@code true} if the provided context allows applying force invert, otherwise * {@code false}. * * @hide */ public static boolean isForceInvertAllowed(Context context) { final String packageName = context.getPackageName(); final String[] packageBlocklist = context.getResources().getStringArray( com.android.internal.R.array.config_forceInvertPackageBlocklist); if (packageName != null && Arrays.asList(packageBlocklist).contains(packageName)) { return false; } return true; } /** * Registers a {@link ForceInvertStateChangeListener} for the current user. * Loading core/java/android/view/ViewRootImpl.java +14 −17 Original line number Diff line number Diff line Loading @@ -2036,22 +2036,15 @@ public final class ViewRootImpl implements ViewParent, } } 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 both // force invert and the system's dark theme are enabled. if (shouldApplyForceInvertDark()) { // We will use HWUI color area detection to determine if it should actually be // inverted. Checking light theme simply gives the developer a way to "opt-out" // of force invert. final boolean isLightTheme = a.getBoolean(R.styleable.Theme_isLightTheme, false); if (isLightTheme) { // Do not apply force invert dark theme to an app that already declares itself as // supporting dark theme (isLightTheme=false). This gives the developer a way to // opt out of allowing this behavior, while also guaranteeing that apps with a // properly configured dark theme are unaffected by force invert dark theme. For // self-declared light theme apps HWUI then performs its own "color area" // calculation to determine if the app actually renders with light colors. if (a.getBoolean(R.styleable.Theme_isLightTheme, false)) { return ForceDarkType.FORCE_INVERT_COLOR_DARK; } else { return ForceDarkType.NONE; } } } Loading @@ -2062,11 +2055,15 @@ public final class ViewRootImpl implements ViewParent, } private boolean shouldApplyForceInvertDark() { if (!forceInvertColor()) { return false; } final UiModeManager uiModeManager = mContext.getSystemService(UiModeManager.class); if (uiModeManager == null) { return false; } return uiModeManager.getForceInvertState() == UiModeManager.FORCE_INVERT_TYPE_DARK; return uiModeManager.getForceInvertState() == UiModeManager.FORCE_INVERT_TYPE_DARK && UiModeManager.isForceInvertAllowed(mContext); } private void updateForceDarkMode() { Loading core/res/res/values/config.xml +7 −0 Original line number Diff line number Diff line Loading @@ -4909,6 +4909,13 @@ --> </string-array> <string-array translatable="false" name="config_forceInvertPackageBlocklist"> <!-- <item>com.example.package.first</item> <item>com.example.package.second</item> --> </string-array> <!-- Warning: This API can be dangerous when not implemented properly. In particular, escrow token must NOT be retrievable from device storage. In other words, either escrow token is not stored on device or its ciphertext is stored on device while Loading core/res/res/values/symbols.xml +1 −0 Original line number Diff line number Diff line Loading @@ -3861,6 +3861,7 @@ <java-symbol type="string" name="config_defaultAccessibilityNotificationSound" /> <java-symbol type="string" name="accessibility_shortcut_spoken_feedback" /> <java-symbol type="array" name="config_trustedAccessibilityServices" /> <java-symbol type="array" name="config_forceInvertPackageBlocklist" /> <java-symbol type="string" name="accessibility_select_shortcut_menu_title" /> <java-symbol type="string" name="accessibility_edit_shortcut_menu_button_title" /> Loading core/tests/coretests/src/android/view/ViewRootImplTest.java +40 −5 Original line number Diff line number Diff line Loading @@ -77,6 +77,7 @@ import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.platform.test.flag.junit.SetFlagsRule; import android.provider.Settings; import android.sysprop.ViewProperties; import android.testing.TestableContext; import android.util.DisplayMetrics; import android.util.Log; import android.util.MergedConfiguration; Loading Loading @@ -1529,6 +1530,27 @@ public class ViewRootImplTest { == ForceDarkType.FORCE_INVERT_COLOR_DARK)); } @Test @EnableFlags(FLAG_FORCE_INVERT_COLOR) public void determineForceDarkType_isBlocklistedPackage_returnsNone() throws Exception { TestableContext testableContext = new TestableContext(sContext); sInstrumentation.runOnMainSync(() -> mViewRootImpl = new ViewRootImpl(testableContext, testableContext.getDisplayNoVerify())); // Set up configurations for force invert color, but with this context belonging to a // blocklisted package. waitForSystemNightModeActivated(testableContext, true); enableForceInvertColor(testableContext, true); testableContext.getOrCreateTestableResources().addOverride( com.android.internal.R.array.config_forceInvertPackageBlocklist, new String[]{testableContext.getPackageName()}); setUpViewAttributes(testableContext, /* isLightTheme= */ true, /* isForceDarkAllowed= */ false); TestUtils.waitUntil("Waiting for ForceDarkType to be ready", () -> (mViewRootImpl.determineForceDarkType() == ForceDarkType.NONE)); } @Test @EnableFlags(FLAG_FORCE_INVERT_COLOR) public void determineForceDarkType_notLightTheme_returnsNone() throws Exception { Loading Loading @@ -1810,18 +1832,26 @@ public class ViewRootImplTest { } private void waitForSystemNightModeActivated(boolean active) { waitForSystemNightModeActivated(sContext, active); } private void waitForSystemNightModeActivated(Context context, boolean active) { ShellIdentityUtils.invokeWithShellPermissions(() -> sInstrumentation.runOnMainSync(() -> { var uiModeManager = sContext.getSystemService(UiModeManager.class); var uiModeManager = context.getSystemService(UiModeManager.class); uiModeManager.setNightModeActivated(active); })); sInstrumentation.waitForIdleSync(); } private void enableForceInvertColor(boolean enabled) { enableForceInvertColor(sContext, enabled); } private void enableForceInvertColor(Context context, boolean enabled) { ShellIdentityUtils.invokeWithShellPermissions(() -> { Settings.Secure.putInt( sContext.getContentResolver(), context.getContentResolver(), Settings.Secure.ACCESSIBILITY_FORCE_INVERT_COLOR_ENABLED, enabled ? 1 : 0 ); Loading @@ -1829,6 +1859,11 @@ public class ViewRootImplTest { } private void setUpViewAttributes(boolean isLightTheme, boolean isForceDarkAllowed) { setUpViewAttributes(sContext, isLightTheme, isForceDarkAllowed); } private void setUpViewAttributes(Context context, boolean isLightTheme, boolean isForceDarkAllowed) { ShellIdentityUtils.invokeWithShellPermissions(() -> { int themeId; if (isForceDarkAllowed) { Loading @@ -1844,17 +1879,17 @@ public class ViewRootImplTest { themeId = R.style.ForceDarkAllowedFalse_Dark; } } sContext.setTheme(themeId); context.setTheme(themeId); }); sInstrumentation.runOnMainSync(() -> { View view = new View(sContext); View view = new View(context); WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams( TYPE_APPLICATION_OVERLAY); layoutParams.token = new Binder(); view.setLayoutParams(layoutParams); mViewRootImpl.setView(view, layoutParams, /* panelParentView= */ null); mViewRootImpl.updateConfiguration(sContext.getDisplayNoVerify().getDisplayId()); mViewRootImpl.updateConfiguration(context.getDisplayNoVerify().getDisplayId()); }); } } Loading
core/java/android/app/UiModeManager.java +17 −0 Original line number Diff line number Diff line Loading @@ -50,6 +50,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.ref.WeakReference; import java.time.LocalTime; import java.util.Arrays; import java.util.Comparator; import java.util.List; import java.util.Map; Loading Loading @@ -1559,6 +1560,22 @@ public class UiModeManager { return sGlobals.getForceInvertState(); } /** * Returns {@code true} if the provided context allows applying force invert, otherwise * {@code false}. * * @hide */ public static boolean isForceInvertAllowed(Context context) { final String packageName = context.getPackageName(); final String[] packageBlocklist = context.getResources().getStringArray( com.android.internal.R.array.config_forceInvertPackageBlocklist); if (packageName != null && Arrays.asList(packageBlocklist).contains(packageName)) { return false; } return true; } /** * Registers a {@link ForceInvertStateChangeListener} for the current user. * Loading
core/java/android/view/ViewRootImpl.java +14 −17 Original line number Diff line number Diff line Loading @@ -2036,22 +2036,15 @@ public final class ViewRootImpl implements ViewParent, } } 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 both // force invert and the system's dark theme are enabled. if (shouldApplyForceInvertDark()) { // We will use HWUI color area detection to determine if it should actually be // inverted. Checking light theme simply gives the developer a way to "opt-out" // of force invert. final boolean isLightTheme = a.getBoolean(R.styleable.Theme_isLightTheme, false); if (isLightTheme) { // Do not apply force invert dark theme to an app that already declares itself as // supporting dark theme (isLightTheme=false). This gives the developer a way to // opt out of allowing this behavior, while also guaranteeing that apps with a // properly configured dark theme are unaffected by force invert dark theme. For // self-declared light theme apps HWUI then performs its own "color area" // calculation to determine if the app actually renders with light colors. if (a.getBoolean(R.styleable.Theme_isLightTheme, false)) { return ForceDarkType.FORCE_INVERT_COLOR_DARK; } else { return ForceDarkType.NONE; } } } Loading @@ -2062,11 +2055,15 @@ public final class ViewRootImpl implements ViewParent, } private boolean shouldApplyForceInvertDark() { if (!forceInvertColor()) { return false; } final UiModeManager uiModeManager = mContext.getSystemService(UiModeManager.class); if (uiModeManager == null) { return false; } return uiModeManager.getForceInvertState() == UiModeManager.FORCE_INVERT_TYPE_DARK; return uiModeManager.getForceInvertState() == UiModeManager.FORCE_INVERT_TYPE_DARK && UiModeManager.isForceInvertAllowed(mContext); } private void updateForceDarkMode() { Loading
core/res/res/values/config.xml +7 −0 Original line number Diff line number Diff line Loading @@ -4909,6 +4909,13 @@ --> </string-array> <string-array translatable="false" name="config_forceInvertPackageBlocklist"> <!-- <item>com.example.package.first</item> <item>com.example.package.second</item> --> </string-array> <!-- Warning: This API can be dangerous when not implemented properly. In particular, escrow token must NOT be retrievable from device storage. In other words, either escrow token is not stored on device or its ciphertext is stored on device while Loading
core/res/res/values/symbols.xml +1 −0 Original line number Diff line number Diff line Loading @@ -3861,6 +3861,7 @@ <java-symbol type="string" name="config_defaultAccessibilityNotificationSound" /> <java-symbol type="string" name="accessibility_shortcut_spoken_feedback" /> <java-symbol type="array" name="config_trustedAccessibilityServices" /> <java-symbol type="array" name="config_forceInvertPackageBlocklist" /> <java-symbol type="string" name="accessibility_select_shortcut_menu_title" /> <java-symbol type="string" name="accessibility_edit_shortcut_menu_button_title" /> Loading
core/tests/coretests/src/android/view/ViewRootImplTest.java +40 −5 Original line number Diff line number Diff line Loading @@ -77,6 +77,7 @@ import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.platform.test.flag.junit.SetFlagsRule; import android.provider.Settings; import android.sysprop.ViewProperties; import android.testing.TestableContext; import android.util.DisplayMetrics; import android.util.Log; import android.util.MergedConfiguration; Loading Loading @@ -1529,6 +1530,27 @@ public class ViewRootImplTest { == ForceDarkType.FORCE_INVERT_COLOR_DARK)); } @Test @EnableFlags(FLAG_FORCE_INVERT_COLOR) public void determineForceDarkType_isBlocklistedPackage_returnsNone() throws Exception { TestableContext testableContext = new TestableContext(sContext); sInstrumentation.runOnMainSync(() -> mViewRootImpl = new ViewRootImpl(testableContext, testableContext.getDisplayNoVerify())); // Set up configurations for force invert color, but with this context belonging to a // blocklisted package. waitForSystemNightModeActivated(testableContext, true); enableForceInvertColor(testableContext, true); testableContext.getOrCreateTestableResources().addOverride( com.android.internal.R.array.config_forceInvertPackageBlocklist, new String[]{testableContext.getPackageName()}); setUpViewAttributes(testableContext, /* isLightTheme= */ true, /* isForceDarkAllowed= */ false); TestUtils.waitUntil("Waiting for ForceDarkType to be ready", () -> (mViewRootImpl.determineForceDarkType() == ForceDarkType.NONE)); } @Test @EnableFlags(FLAG_FORCE_INVERT_COLOR) public void determineForceDarkType_notLightTheme_returnsNone() throws Exception { Loading Loading @@ -1810,18 +1832,26 @@ public class ViewRootImplTest { } private void waitForSystemNightModeActivated(boolean active) { waitForSystemNightModeActivated(sContext, active); } private void waitForSystemNightModeActivated(Context context, boolean active) { ShellIdentityUtils.invokeWithShellPermissions(() -> sInstrumentation.runOnMainSync(() -> { var uiModeManager = sContext.getSystemService(UiModeManager.class); var uiModeManager = context.getSystemService(UiModeManager.class); uiModeManager.setNightModeActivated(active); })); sInstrumentation.waitForIdleSync(); } private void enableForceInvertColor(boolean enabled) { enableForceInvertColor(sContext, enabled); } private void enableForceInvertColor(Context context, boolean enabled) { ShellIdentityUtils.invokeWithShellPermissions(() -> { Settings.Secure.putInt( sContext.getContentResolver(), context.getContentResolver(), Settings.Secure.ACCESSIBILITY_FORCE_INVERT_COLOR_ENABLED, enabled ? 1 : 0 ); Loading @@ -1829,6 +1859,11 @@ public class ViewRootImplTest { } private void setUpViewAttributes(boolean isLightTheme, boolean isForceDarkAllowed) { setUpViewAttributes(sContext, isLightTheme, isForceDarkAllowed); } private void setUpViewAttributes(Context context, boolean isLightTheme, boolean isForceDarkAllowed) { ShellIdentityUtils.invokeWithShellPermissions(() -> { int themeId; if (isForceDarkAllowed) { Loading @@ -1844,17 +1879,17 @@ public class ViewRootImplTest { themeId = R.style.ForceDarkAllowedFalse_Dark; } } sContext.setTheme(themeId); context.setTheme(themeId); }); sInstrumentation.runOnMainSync(() -> { View view = new View(sContext); View view = new View(context); WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams( TYPE_APPLICATION_OVERLAY); layoutParams.token = new Binder(); view.setLayoutParams(layoutParams); mViewRootImpl.setView(view, layoutParams, /* panelParentView= */ null); mViewRootImpl.updateConfiguration(sContext.getDisplayNoVerify().getDisplayId()); mViewRootImpl.updateConfiguration(context.getDisplayNoVerify().getDisplayId()); }); } }