diff options
6 files changed, 313 insertions, 12 deletions
diff --git a/core/java/android/app/IUiModeManager.aidl b/core/java/android/app/IUiModeManager.aidl index cae54b6c0611..1380f0cdc341 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 46316e1a254b..d8cfb7e3cb76 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 6ffea1be32ef..f28c3196daba 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -894,6 +894,10 @@ <string name="quick_settings_ui_mode_night_label">Dark theme</string> <!-- QuickSettings: Label for the dark theme tile when enabled by battery saver. [CHAR LIMIT=40] --> <string name="quick_settings_ui_mode_night_label_battery_saver">Dark theme\nBattery saver</string> + <!-- QuickSettings: Secondary text for when the Dark Mode will be enabled at sunset. [CHAR LIMIT=20] --> + <string name="quick_settings_dark_mode_secondary_label_on_at_sunset">On at sunset</string> + <!-- QuickSettings: Secondary text for when the Dark Mode will be on until sunrise. [CHAR LIMIT=20] --> + <string name="quick_settings_dark_mode_secondary_label_until_sunrise">Until sunrise</string> <!-- QuickSettings: NFC tile [CHAR LIMIT=NONE] --> <string name="quick_settings_nfc_label">NFC</string> 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 dd0ea5ef17f4..dc9a2ce1beef 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<QSTile.BooleanState> 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 6b038979a98d..56db8898f7fc 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 000000000000..338f837b9b44 --- /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<BroadcastReceiver> 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; + } +} |