From 381e3300e8269be5fe7a6d7201a878d9928da3af Mon Sep 17 00:00:00 2001 From: Jay Aliomer Date: Thu, 24 Oct 2019 13:18:06 -0400 Subject: [PATCH] Dark theme twilight mode backend and tile When the user has Dark theme scheduling turned on (b/141567787), the quick settings tile subtext should reflect the following: If Dark theme is on: "Until sunrise" If Dark theme is off: "On at sunset" If the user does not have Dark theme scheduled, the text should not appear. This matches the appearance of the Night Light QS tile. All starting windows snapshots will be deleted and splash screens will be used instead. Test: atest UiModeManagerServiceTest Fix: 143874807 Merged-In: Ie2ce64b5c5544fffe76be7ec3f971eb7461f8c4d Change-Id: Ie2ce64b5c5544fffe76be7ec3f971eb7461f8c4d (cherry picked from commit 8b2671b029aee961b57419bb290b682c58d702af) --- core/java/android/app/IUiModeManager.aidl | 5 + core/java/android/app/UiModeManager.java | 14 ++ packages/SystemUI/res/values/strings.xml | 4 + .../systemui/qs/tiles/UiModeNightTile.java | 21 ++- .../android/server/UiModeManagerService.java | 120 ++++++++++++- .../server/UiModeManagerServiceTest.java | 161 ++++++++++++++++++ 6 files changed, 313 insertions(+), 12 deletions(-) create mode 100644 services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java diff --git a/core/java/android/app/IUiModeManager.aidl b/core/java/android/app/IUiModeManager.aidl index cae54b6c0611f..1380f0cdc3411 100644 --- a/core/java/android/app/IUiModeManager.aidl +++ b/core/java/android/app/IUiModeManager.aidl @@ -61,4 +61,9 @@ interface IUiModeManager { * Tells if Night mode is locked or not. */ boolean isNightModeLocked(); + + /** + * @hide + */ + boolean setNightModeActivated(boolean active); } diff --git a/core/java/android/app/UiModeManager.java b/core/java/android/app/UiModeManager.java index 46316e1a254bb..d8cfb7e3cb76b 100644 --- a/core/java/android/app/UiModeManager.java +++ b/core/java/android/app/UiModeManager.java @@ -317,4 +317,18 @@ public class UiModeManager { } return true; } + + /** + * @hide* + */ + public boolean setNightModeActivated(boolean active) { + if (mService != null) { + try { + return mService.setNightModeActivated(active); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + return false; + } } diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 6ffea1be32ef9..f28c3196daba9 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -894,6 +894,10 @@ Dark theme Dark theme\nBattery saver + + On at sunset + + Until sunrise NFC diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/UiModeNightTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/UiModeNightTile.java index dd0ea5ef17f49..dc9a2ce1beef5 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/UiModeNightTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/UiModeNightTile.java @@ -21,7 +21,7 @@ import android.content.Intent; import android.content.res.Configuration; import android.provider.Settings; import android.service.quicksettings.Tile; -import android.widget.Switch; +import android.text.TextUtils; import com.android.internal.logging.nano.MetricsProto; import com.android.systemui.R; @@ -79,24 +79,33 @@ public class UiModeNightTile extends QSTileImpl implements return; } boolean newState = !mState.value; - mUiModeManager.setNightMode(newState ? UiModeManager.MODE_NIGHT_YES - : UiModeManager.MODE_NIGHT_NO); + mUiModeManager.setNightModeActivated(newState); refreshState(newState); } @Override protected void handleUpdateState(BooleanState state, Object arg) { + int uiMode = mUiModeManager.getNightMode(); boolean powerSave = mBatteryController.isPowerSave(); + boolean isAuto = uiMode == UiModeManager.MODE_NIGHT_AUTO; boolean nightMode = (mContext.getResources().getConfiguration().uiMode - & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES; + & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES; + if (isAuto) { + state.secondaryLabel = mContext.getResources().getString(nightMode + ? R.string.quick_settings_dark_mode_secondary_label_until_sunrise + : R.string.quick_settings_dark_mode_secondary_label_on_at_sunset); + } else { + state.secondaryLabel = null; + } state.value = nightMode; state.label = mContext.getString(powerSave ? R.string.quick_settings_ui_mode_night_label_battery_saver : R.string.quick_settings_ui_mode_night_label); - state.contentDescription = state.label; state.icon = mIcon; - state.expandedAccessibilityClassName = Switch.class.getName(); + state.contentDescription = TextUtils.isEmpty(state.secondaryLabel) + ? state.label + : TextUtils.concat(state.label, ", ", state.secondaryLabel); if (powerSave) { state.state = Tile.STATE_UNAVAILABLE; } else { diff --git a/services/core/java/com/android/server/UiModeManagerService.java b/services/core/java/com/android/server/UiModeManagerService.java index 6b038979a98d4..56db8898f7fc7 100644 --- a/services/core/java/com/android/server/UiModeManagerService.java +++ b/services/core/java/com/android/server/UiModeManagerService.java @@ -53,8 +53,8 @@ import android.service.dreams.Sandman; import android.service.vr.IVrManager; import android.service.vr.IVrStateCallbacks; import android.util.Slog; - import com.android.internal.R; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.DisableCarModeActivity; import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; import com.android.internal.notification.SystemNotificationChannels; @@ -62,10 +62,13 @@ import com.android.internal.util.DumpUtils; import com.android.server.twilight.TwilightListener; import com.android.server.twilight.TwilightManager; import com.android.server.twilight.TwilightState; +import com.android.server.wm.WindowManagerInternal; import java.io.FileDescriptor; import java.io.PrintWriter; +import static android.content.Intent.ACTION_SCREEN_OFF; + final class UiModeManagerService extends SystemService { private static final String TAG = UiModeManager.class.getSimpleName(); private static final boolean LOG = false; @@ -79,6 +82,10 @@ final class UiModeManagerService extends SystemService { private int mLastBroadcastState = Intent.EXTRA_DOCK_STATE_UNDOCKED; private int mNightMode = UiModeManager.MODE_NIGHT_NO; + // we use the override auto mode + // for example: force night mode off in the night time while in auto mode + private int mNightModeOverride = mNightMode; + protected static final String OVERRIDE_NIGHT_MODE = Secure.UI_NIGHT_MODE + "_override"; private boolean mCarModeEnabled = false; private boolean mCharging = false; @@ -112,6 +119,7 @@ final class UiModeManagerService extends SystemService { private TwilightManager mTwilightManager; private NotificationManager mNotificationManager; private StatusBarManager mStatusBarManager; + private WindowManagerInternal mWindowManager; private PowerManager.WakeLock mWakeLock; @@ -121,6 +129,17 @@ final class UiModeManagerService extends SystemService { super(context); } + @VisibleForTesting + protected UiModeManagerService(Context context, WindowManagerInternal wm, + PowerManager.WakeLock wl, TwilightManager tm, + boolean setupWizardComplete) { + super(context); + mWindowManager = wm; + mWakeLock = wl; + mTwilightManager = tm; + mSetupWizardComplete = setupWizardComplete; + } + private static Intent buildHomeIntent(String category) { Intent intent = new Intent(Intent.ACTION_MAIN); intent.addCategory(category); @@ -182,8 +201,23 @@ final class UiModeManagerService extends SystemService { public void onTwilightStateChanged(@Nullable TwilightState state) { synchronized (mLock) { if (mNightMode == UiModeManager.MODE_NIGHT_AUTO) { - updateComputedNightModeLocked(); - updateLocked(0, 0); + final IntentFilter intentFilter = + new IntentFilter(ACTION_SCREEN_OFF); + getContext().registerReceiver(mOnScreenOffHandler, intentFilter); + } + } + } + }; + + private final BroadcastReceiver mOnScreenOffHandler = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + synchronized (mLock) { + updateLocked(0, 0); + try { + getContext().unregisterReceiver(mOnScreenOffHandler); + } catch (IllegalArgumentException e) { + // we ignore this exception if the receiver is unregistered already. } } } @@ -220,8 +254,10 @@ final class UiModeManagerService extends SystemService { private final ContentObserver mDarkThemeObserver = new ContentObserver(mHandler) { @Override public void onChange(boolean selfChange, Uri uri) { - final int mode = Secure.getIntForUser(getContext().getContentResolver(), - Secure.UI_NIGHT_MODE, mNightMode, 0); + int mode = Secure.getIntForUser(getContext().getContentResolver(), Secure.UI_NIGHT_MODE, + mNightMode, 0); + mode = mode == UiModeManager.MODE_NIGHT_AUTO + ? UiModeManager.MODE_NIGHT_YES : UiModeManager.MODE_NIGHT_NO; SystemProperties.set(SYSTEM_PROPERTY_DEVICE_THEME, Integer.toString(mode)); } }; @@ -240,6 +276,7 @@ final class UiModeManagerService extends SystemService { final PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); mWakeLock = powerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK, TAG); + mWindowManager = LocalServices.getService(WindowManagerInternal.class); // If setup isn't complete for this user listen for completion so we can unblock // being able to send a night mode configuration change event @@ -306,6 +343,16 @@ final class UiModeManagerService extends SystemService { false, mDarkThemeObserver, 0); } + @VisibleForTesting + protected IUiModeManager getService() { + return mService; + } + + @VisibleForTesting + protected Configuration getConfiguration() { + return mConfiguration; + } + // Records whether setup wizard has happened or not and adds an observer for this user if not. private void verifySetupWizardCompleted() { final Context context = getContext(); @@ -340,8 +387,11 @@ final class UiModeManagerService extends SystemService { if (mSetupWizardComplete) { mNightMode = Secure.getIntForUser(context.getContentResolver(), Secure.UI_NIGHT_MODE, defaultNightMode, userId); + mNightModeOverride = Secure.getIntForUser(context.getContentResolver(), + OVERRIDE_NIGHT_MODE, defaultNightMode, userId); } else { mNightMode = defaultNightMode; + mNightModeOverride = defaultNightMode; } return oldNightMode != mNightMode; @@ -424,14 +474,30 @@ final class UiModeManagerService extends SystemService { try { synchronized (mLock) { if (mNightMode != mode) { + if (mNightMode == UiModeManager.MODE_NIGHT_AUTO) { + try { + getContext().unregisterReceiver(mOnScreenOffHandler); + } catch (IllegalArgumentException e) { + // we ignore this exception if the receiver is unregistered already. + } + } // Only persist setting if not in car mode if (!mCarModeEnabled) { Secure.putIntForUser(getContext().getContentResolver(), Secure.UI_NIGHT_MODE, mode, user); + Secure.putIntForUser(getContext().getContentResolver(), + OVERRIDE_NIGHT_MODE, mNightModeOverride, user); } mNightMode = mode; - updateLocked(0, 0); + mNightModeOverride = mode; + //on screen off will update configuration instead + if (mNightMode != UiModeManager.MODE_NIGHT_AUTO) { + updateLocked(0, 0); + } else { + getContext().registerReceiver( + mOnScreenOffHandler, new IntentFilter(ACTION_SCREEN_OFF)); + } } } } finally { @@ -471,6 +537,34 @@ final class UiModeManagerService extends SystemService { if (!DumpUtils.checkDumpPermission(getContext(), TAG, pw)) return; dumpImpl(pw); } + + @Override + public boolean setNightModeActivated(boolean active) { + synchronized (mLock) { + final long ident = Binder.clearCallingIdentity(); + try { + if (mNightMode == UiModeManager.MODE_NIGHT_AUTO) { + try { + getContext().unregisterReceiver(mOnScreenOffHandler); + } catch (IllegalArgumentException e) { + } + mNightModeOverride = active + ? UiModeManager.MODE_NIGHT_YES : UiModeManager.MODE_NIGHT_NO; + } else if (mNightMode == UiModeManager.MODE_NIGHT_NO + && active) { + mNightMode = UiModeManager.MODE_NIGHT_YES; + } else if (mNightMode == UiModeManager.MODE_NIGHT_YES + && !active) { + mNightMode = UiModeManager.MODE_NIGHT_NO; + } + updateConfigurationLocked(); + sendConfigurationLocked(); + return true; + } finally { + Binder.restoreCallingIdentity(ident); + } + } + } }; void dumpImpl(PrintWriter pw) { @@ -848,6 +942,20 @@ final class UiModeManagerService extends SystemService { if (state != null) { mComputedNightMode = state.isNight(); } + if (mNightModeOverride == UiModeManager.MODE_NIGHT_YES && !mComputedNightMode) { + mComputedNightMode = true; + return; + } + if (mNightModeOverride == UiModeManager.MODE_NIGHT_NO && mComputedNightMode) { + mComputedNightMode = false; + return; + } + + mNightModeOverride = mNightMode; + final int user = UserHandle.getCallingUserId(); + Secure.putIntForUser(getContext().getContentResolver(), + OVERRIDE_NIGHT_MODE, mNightModeOverride, user); + } } diff --git a/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java new file mode 100644 index 0000000000000..338f837b9b442 --- /dev/null +++ b/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 20019 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.server; + +import android.app.IUiModeManager; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.os.PowerManager; +import android.os.RemoteException; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import com.android.server.twilight.TwilightManager; +import com.android.server.wm.WindowManagerInternal; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; + +import java.util.HashSet; +import java.util.Set; + +import static android.app.UiModeManager.MODE_NIGHT_AUTO; +import static android.app.UiModeManager.MODE_NIGHT_NO; +import static android.app.UiModeManager.MODE_NIGHT_YES; +import static junit.framework.TestCase.assertFalse; +import static junit.framework.TestCase.assertTrue; +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +public class UiModeManagerServiceTest extends UiServiceTestCase { + private UiModeManagerService mUiManagerService; + private IUiModeManager mService; + @Mock + private ContentResolver mContentResolver; + @Mock + private WindowManagerInternal mWindowManager; + @Mock + private Context mContext; + @Mock + private Resources mResources; + @Mock + TwilightManager mTwilightManager; + @Mock + PowerManager.WakeLock mWakeLock; + private Set mScreenOffRecievers; + + @Before + public void setUp() { + mUiManagerService = new UiModeManagerService(mContext, mWindowManager, mWakeLock, + mTwilightManager, true); + mScreenOffRecievers = new HashSet<>(); + mService = mUiManagerService.getService(); + when(mContext.checkCallingOrSelfPermission(anyString())) + .thenReturn(PackageManager.PERMISSION_GRANTED); + when(mContext.getResources()).thenReturn(mResources); + when(mContext.getContentResolver()).thenReturn(mContentResolver); + when(mContext.registerReceiver(any(), any())).then(inv -> { + mScreenOffRecievers.add(inv.getArgument(0)); + return null; + }); + } + + @Test + public void setAutoMode_screenOffRegistered() throws RemoteException { + try { + mService.setNightMode(MODE_NIGHT_NO); + } catch (SecurityException e) { /* we should ignore this update config exception*/ } + mService.setNightMode(MODE_NIGHT_AUTO); + verify(mContext).registerReceiver(any(BroadcastReceiver.class), any()); + } + + @Test + public void setAutoMode_screenOffUnRegistered() throws RemoteException { + try { + mService.setNightMode(MODE_NIGHT_AUTO); + } catch (SecurityException e) { /* we should ignore this update config exception*/ } + try { + mService.setNightMode(MODE_NIGHT_NO); + } catch (SecurityException e) { /*we should ignore this update config exception*/ } + given(mContext.registerReceiver(any(), any())).willThrow(SecurityException.class); + verify(mContext).unregisterReceiver(any(BroadcastReceiver.class)); + } + + @Test + public void setNightModeActive_fromNightModeYesToNoWhenFalse() throws RemoteException { + try { + mService.setNightMode(MODE_NIGHT_YES); + } catch (SecurityException e) { /* we should ignore this update config exception*/ } + try { + mService.setNightModeActivated(false); + } catch (SecurityException e) { /* we should ignore this update config exception*/ } + assertEquals(MODE_NIGHT_NO, mService.getNightMode()); + } + + @Test + public void setNightModeActive_fromNightModeNoToYesWhenTrue() throws RemoteException { + try { + mService.setNightMode(MODE_NIGHT_NO); + } catch (SecurityException e) { /* we should ignore this update config exception*/ } + try { + mService.setNightModeActivated(true); + } catch (SecurityException e) { /* we should ignore this update config exception*/ } + assertEquals(MODE_NIGHT_YES, mService.getNightMode()); + } + + @Test + public void setNightModeActive_autoNightModeNoChanges() throws RemoteException { + try { + mService.setNightMode(MODE_NIGHT_AUTO); + } catch (SecurityException e) { /* we should ignore this update config exception*/ } + try { + mService.setNightModeActivated(true); + } catch (SecurityException e) { /* we should ignore this update config exception*/ } + assertEquals(MODE_NIGHT_AUTO, mService.getNightMode()); + } + + @Test + public void isNightModeActive_nightModeYes() throws RemoteException { + try { + mService.setNightMode(MODE_NIGHT_YES); + } catch (SecurityException e) { /* we should ignore this update config exception*/ } + assertTrue(isNightModeActivated()); + } + + @Test + public void isNightModeActive_nightModeNo() throws RemoteException { + try { + mService.setNightMode(MODE_NIGHT_NO); + } catch (SecurityException e) { /* we should ignore this update config exception*/ } + assertFalse(isNightModeActivated()); + } + + private boolean isNightModeActivated() { + return (mUiManagerService.getConfiguration().uiMode + & Configuration.UI_MODE_NIGHT_YES) != 0; + } +}