diff --git a/core/java/android/app/IUiModeManager.aidl b/core/java/android/app/IUiModeManager.aidl index a3e0845af0cec..f5809ba627ff1 100644 --- a/core/java/android/app/IUiModeManager.aidl +++ b/core/java/android/app/IUiModeManager.aidl @@ -68,4 +68,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 83247875c8a55..d8c030d7aacf6 100644 --- a/core/java/android/app/UiModeManager.java +++ b/core/java/android/app/UiModeManager.java @@ -473,4 +473,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 d58f83fb8a3b4..1044ede009b15 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 9d71896ef08b1..e6d303b8f5b93 100644 --- a/services/core/java/com/android/server/UiModeManagerService.java +++ b/services/core/java/com/android/server/UiModeManagerService.java @@ -56,8 +56,8 @@ import android.service.vr.IVrManager; import android.service.vr.IVrStateCallbacks; import android.util.ArraySet; 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; @@ -65,6 +65,7 @@ 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; @@ -72,6 +73,8 @@ import java.util.HashMap; import java.util.Map; import java.util.Set; +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; @@ -85,6 +88,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 Map mCarModePackagePriority = new HashMap<>(); private boolean mCarModeEnabled = false; @@ -119,6 +126,7 @@ final class UiModeManagerService extends SystemService { private TwilightManager mTwilightManager; private NotificationManager mNotificationManager; private StatusBarManager mStatusBarManager; + private WindowManagerInternal mWindowManager; private PowerManager.WakeLock mWakeLock; @@ -128,6 +136,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); @@ -189,8 +208,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. } } } @@ -227,8 +261,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)); } }; @@ -247,6 +283,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 @@ -313,6 +350,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(); @@ -347,8 +394,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; @@ -474,14 +524,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 { @@ -521,6 +587,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) { @@ -1022,6 +1116,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; + } +}