diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 2d028109be3ed..08f460838ee07 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -87,7 +87,7 @@
- internet,bt,flashlight,dnd,alarm,airplane,controls,wallet,rotation,battery,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction
+ internet,bt,flashlight,dnd,alarm,airplane,controls,wallet,rotation,battery,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 4115f4fb5a38d..9ac76c7f7b417 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -578,6 +578,8 @@
Location
+
+ Screen saver
Camera access
diff --git a/packages/SystemUI/res/values/tiles_states_strings.xml b/packages/SystemUI/res/values/tiles_states_strings.xml
index e2734164161f8..c8095510e2c2a 100644
--- a/packages/SystemUI/res/values/tiles_states_strings.xml
+++ b/packages/SystemUI/res/values/tiles_states_strings.xml
@@ -308,4 +308,14 @@
- Off
- On
+
+
+
+ - Unavailable
+ - Off
+ - On
+
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java
index 1488231275c3a..a92c7e3c8554f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java
@@ -39,6 +39,7 @@ import com.android.systemui.qs.tiles.ColorInversionTile;
import com.android.systemui.qs.tiles.DataSaverTile;
import com.android.systemui.qs.tiles.DeviceControlsTile;
import com.android.systemui.qs.tiles.DndTile;
+import com.android.systemui.qs.tiles.DreamTile;
import com.android.systemui.qs.tiles.FlashlightTile;
import com.android.systemui.qs.tiles.HotspotTile;
import com.android.systemui.qs.tiles.InternetTile;
@@ -96,6 +97,7 @@ public class QSFactoryImpl implements QSFactory {
private final Provider mQuickAccessWalletTileProvider;
private final Provider mQRCodeScannerTileProvider;
private final Provider mOneHandedModeTileProvider;
+ private final Provider mDreamTileProvider;
private final Lazy mQsHostLazy;
private final Provider mCustomTileBuilderProvider;
@@ -132,7 +134,8 @@ public class QSFactoryImpl implements QSFactory {
Provider quickAccessWalletTileProvider,
Provider qrCodeScannerTileProvider,
Provider oneHandedModeTileProvider,
- Provider colorCorrectionTileProvider) {
+ Provider colorCorrectionTileProvider,
+ Provider dreamTileProvider) {
mQsHostLazy = qsHostLazy;
mCustomTileBuilderProvider = customTileBuilderProvider;
@@ -165,6 +168,7 @@ public class QSFactoryImpl implements QSFactory {
mQRCodeScannerTileProvider = qrCodeScannerTileProvider;
mOneHandedModeTileProvider = oneHandedModeTileProvider;
mColorCorrectionTileProvider = colorCorrectionTileProvider;
+ mDreamTileProvider = dreamTileProvider;
}
/** Creates a tile with a type based on {@code tileSpec} */
@@ -238,6 +242,8 @@ public class QSFactoryImpl implements QSFactory {
return mOneHandedModeTileProvider.get();
case "color_correction":
return mColorCorrectionTileProvider.get();
+ case "dream":
+ return mDreamTileProvider.get();
}
// Custom tiles
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
index 59164dea9c454..5147d59340394 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
@@ -670,7 +670,8 @@ internal object SubtitleArrayMapping {
"qr_code_scanner" to R.array.tile_states_qr_code_scanner,
"alarm" to R.array.tile_states_alarm,
"onehanded" to R.array.tile_states_onehanded,
- "color_correction" to R.array.tile_states_color_correction
+ "color_correction" to R.array.tile_states_color_correction,
+ "dream" to R.array.tile_states_dream
)
fun getSubtitleId(spec: String?): Int {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DreamTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/DreamTile.java
new file mode 100644
index 0000000000000..f24773a5da1d1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DreamTile.java
@@ -0,0 +1,228 @@
+/*
+ * Copyright (C) 2022 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.systemui.qs.tiles;
+
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.provider.Settings;
+import android.service.dreams.IDreamManager;
+import android.service.quicksettings.Tile;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.internal.logging.MetricsLogger;
+import com.android.systemui.R;
+import com.android.systemui.broadcast.BroadcastDispatcher;
+import com.android.systemui.dagger.qualifiers.Background;
+import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.plugins.FalsingManager;
+import com.android.systemui.plugins.qs.QSTile;
+import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.qs.QSHost;
+import com.android.systemui.qs.SettingObserver;
+import com.android.systemui.qs.logging.QSLogger;
+import com.android.systemui.qs.tileimpl.QSTileImpl;
+import com.android.systemui.util.settings.SecureSettings;
+
+import javax.inject.Inject;
+
+/** Quick settings tile: Screensaver (dream) **/
+public class DreamTile extends QSTileImpl {
+
+ private static final String LOG_TAG = "QSDream";
+ private final IDreamManager mDreamManager;
+ private final SecureSettings mSecureSettings;
+ private final BroadcastDispatcher mBroadcastDispatcher;
+ private final SettingObserver mEnabledSettingObserver;
+ private final SettingObserver mDreamSettingObserver;
+
+ private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ refreshState();
+ }
+ };
+
+ @Inject
+ public DreamTile(
+ QSHost host,
+ @Background Looper backgroundLooper,
+ @Main Handler mainHandler,
+ FalsingManager falsingManager,
+ MetricsLogger metricsLogger,
+ StatusBarStateController statusBarStateController,
+ ActivityStarter activityStarter,
+ QSLogger qsLogger,
+ IDreamManager dreamManager,
+ SecureSettings secureSettings,
+ BroadcastDispatcher broadcastDispatcher
+ ) {
+ super(host, backgroundLooper, mainHandler, falsingManager, metricsLogger,
+ statusBarStateController, activityStarter, qsLogger);
+ mDreamManager = dreamManager;
+ mSecureSettings = secureSettings;
+ mBroadcastDispatcher = broadcastDispatcher;
+ mEnabledSettingObserver = new SettingObserver(secureSettings, mHandler,
+ Settings.Secure.SCREENSAVER_ENABLED) {
+ @Override
+ protected void handleValueChanged(int value, boolean observedChange) {
+ refreshState();
+ }
+ };
+ mDreamSettingObserver = new SettingObserver(secureSettings, mHandler,
+ Settings.Secure.SCREENSAVER_COMPONENTS) {
+ @Override
+ protected void handleValueChanged(int value, boolean observedChange) {
+ refreshState();
+ }
+ };
+ }
+
+ @Override
+ public void handleSetListening(boolean listening) {
+ super.handleSetListening(listening);
+
+ if (listening) {
+ final IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_DREAMING_STARTED);
+ filter.addAction(Intent.ACTION_DREAMING_STOPPED);
+ mBroadcastDispatcher.registerReceiver(mReceiver, filter);
+ } else {
+ mBroadcastDispatcher.unregisterReceiver(mReceiver);
+ }
+ mEnabledSettingObserver.setListening(listening);
+ mDreamSettingObserver.setListening(listening);
+ }
+
+ @Override
+ public BooleanState newTileState() {
+ return new BooleanState();
+ }
+
+ @Override
+ protected void handleClick(@Nullable View view) {
+ try {
+ if (mDreamManager.isDreaming()) {
+ mDreamManager.awaken();
+ } else {
+ mDreamManager.dream();
+ }
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, "Can't dream", e);
+ }
+ }
+
+ @Override
+ protected void handleLongClick(@Nullable View view) {
+ try {
+ // Need to wake on long click so bouncer->settings works.
+ mDreamManager.awaken();
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, "Can't awaken", e);
+ }
+ super.handleLongClick(view);
+ }
+
+ @Override
+ protected void handleUpdateState(BooleanState state, Object arg) {
+ state.label = getTileLabel();
+ state.secondaryLabel = getActiveDreamName();
+ state.contentDescription = getContentDescription(state.secondaryLabel);
+
+ if (getActiveDream() == null || !isScreensaverEnabled()) {
+ state.state = Tile.STATE_UNAVAILABLE;
+ } else {
+ state.state = isDreaming() ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE;
+ }
+ }
+
+ @Nullable
+ @Override
+ public Intent getLongClickIntent() {
+ return new Intent(Settings.ACTION_DREAM_SETTINGS);
+ }
+
+ @Override
+ public CharSequence getTileLabel() {
+ return mContext.getString(R.string.quick_settings_screensaver_label);
+ }
+
+ @Override
+ public boolean isAvailable() {
+ // For now, only present on userdebug devices.
+ return Build.isDebuggable();
+ }
+
+ @VisibleForTesting
+ protected CharSequence getContentDescription(CharSequence dreamName) {
+ return !TextUtils.isEmpty(dreamName)
+ ? getTileLabel() + ", " + dreamName : getTileLabel();
+ }
+
+ private boolean isDreaming() {
+ try {
+ return mDreamManager.isDreaming();
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, "Can't check if dreaming", e);
+ return false;
+ }
+ }
+
+ private ComponentName getActiveDream() {
+ try {
+ final ComponentName[] dreams = mDreamManager.getDreamComponents();
+ return dreams != null && dreams.length > 0 ? dreams[0] : null;
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed to get active dream", e);
+ return null;
+ }
+ }
+
+ private CharSequence getActiveDreamName() {
+ final ComponentName componentName = getActiveDream();
+ if (componentName != null) {
+ PackageManager pm = mContext.getPackageManager();
+ try {
+ ServiceInfo ri = pm.getServiceInfo(componentName, 0);
+ if (ri != null) {
+ return ri.loadLabel(pm);
+ }
+ } catch (PackageManager.NameNotFoundException exc) {
+ return null; // uninstalled?
+ }
+ }
+ return null;
+ }
+
+ private boolean isScreensaverEnabled() {
+ return mEnabledSettingObserver.getValue() == 1;
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt
index 0f2c2647f20ff..ca3182affcc18 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt
@@ -34,6 +34,7 @@ import com.android.systemui.qs.tiles.ColorInversionTile
import com.android.systemui.qs.tiles.DataSaverTile
import com.android.systemui.qs.tiles.DeviceControlsTile
import com.android.systemui.qs.tiles.DndTile
+import com.android.systemui.qs.tiles.DreamTile
import com.android.systemui.qs.tiles.FlashlightTile
import com.android.systemui.qs.tiles.HotspotTile
import com.android.systemui.qs.tiles.InternetTile
@@ -89,7 +90,8 @@ private val specMap = mapOf(
"wallet" to QuickAccessWalletTile::class.java,
"qr_code_scanner" to QRCodeScannerTile::class.java,
"onehanded" to OneHandedModeTile::class.java,
- "color_correction" to ColorCorrectionTile::class.java
+ "color_correction" to ColorCorrectionTile::class.java,
+ "dream" to DreamTile::class.java
)
@RunWith(AndroidTestingRunner::class)
@@ -129,6 +131,7 @@ class QSFactoryImplTest : SysuiTestCase() {
@Mock private lateinit var qrCodeScannerTile: QRCodeScannerTile
@Mock private lateinit var oneHandedModeTile: OneHandedModeTile
@Mock private lateinit var colorCorrectionTile: ColorCorrectionTile
+ @Mock private lateinit var dreamTile: DreamTile
private lateinit var factory: QSFactoryImpl
@@ -171,7 +174,8 @@ class QSFactoryImplTest : SysuiTestCase() {
{ quickAccessWalletTile },
{ qrCodeScannerTile },
{ oneHandedModeTile },
- { colorCorrectionTile }
+ { colorCorrectionTile },
+ { dreamTile }
)
// When adding/removing tiles, fix also [specMap]
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DreamTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DreamTileTest.java
new file mode 100644
index 0000000000000..f4a6df860e22f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DreamTileTest.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2022 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.systemui.qs.tiles;
+
+import static junit.framework.TestCase.assertEquals;
+
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.ComponentName;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.provider.Settings;
+import android.service.dreams.IDreamManager;
+import android.service.quicksettings.Tile;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.logging.MetricsLogger;
+import com.android.systemui.R;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.broadcast.BroadcastDispatcher;
+import com.android.systemui.classifier.FalsingManagerFake;
+import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.qs.QSTileHost;
+import com.android.systemui.qs.logging.QSLogger;
+import com.android.systemui.util.settings.FakeSettings;
+import com.android.systemui.util.settings.SecureSettings;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@SmallTest
+public class DreamTileTest extends SysuiTestCase {
+
+ @Mock
+ private ActivityStarter mActivityStarter;
+ @Mock
+ private QSTileHost mHost;
+ @Mock
+ private MetricsLogger mMetricsLogger;
+ @Mock
+ private QSLogger mQSLogger;
+ @Mock
+ private StatusBarStateController mStatusBarStateController;
+ @Mock
+ private IDreamManager mDreamManager;
+ @Mock
+ private BroadcastDispatcher mBroadcastDispatcher;
+
+ private TestableLooper mTestableLooper;
+
+ private DreamTile mTile;
+
+ private SecureSettings mSecureSettings;
+
+ private static final ComponentName COLORS_DREAM_COMPONENT_NAME = new ComponentName(
+ "com.android.dreams", ".Colors");
+
+ private static final int DEFAULT_USER = 0;
+
+ private final String mExpectedTileLabel = mContext.getResources().getString(
+ R.string.quick_settings_screensaver_label);
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ mSecureSettings = new FakeSettings();
+ mTestableLooper = TestableLooper.get(this);
+
+ when(mHost.getUserId()).thenReturn(DEFAULT_USER);
+ when(mHost.getContext()).thenReturn(mContext);
+
+ mTile = spy(new DreamTile(
+ mHost,
+ mTestableLooper.getLooper(),
+ new Handler(mTestableLooper.getLooper()),
+ new FalsingManagerFake(),
+ mMetricsLogger,
+ mStatusBarStateController,
+ mActivityStarter,
+ mQSLogger,
+ mDreamManager,
+ mSecureSettings,
+ mBroadcastDispatcher));
+
+ mTestableLooper.processAllMessages();
+ mTile.initialize();
+ }
+
+ @Test
+ public void testNotAvailable() throws RemoteException {
+ // Should not be available if screensaver is disabled
+ setScreensaverEnabled(false);
+
+ mTile.refreshState();
+ mTestableLooper.processAllMessages();
+ assertEquals(Tile.STATE_UNAVAILABLE, mTile.getState().state);
+
+ // Should not be available if component is not set
+ mSecureSettings.putInt(Settings.Secure.SCREENSAVER_ENABLED, 1);
+ when(mDreamManager.getDreamComponents()).thenReturn(null);
+
+ mTestableLooper.processAllMessages();
+ assertEquals(Tile.STATE_UNAVAILABLE, mTile.getState().state);
+ assertEquals(mExpectedTileLabel, mTile.getState().contentDescription);
+ }
+
+ @Test
+ public void testInactiveWhenDreaming() throws RemoteException {
+ setScreensaverEnabled(true);
+
+ when(mDreamManager.getDreamComponents()).thenReturn(new ComponentName[]{
+ COLORS_DREAM_COMPONENT_NAME
+ });
+ when(mDreamManager.isDreaming()).thenReturn(false);
+
+ mTile.refreshState();
+ mTestableLooper.processAllMessages();
+ assertEquals(Tile.STATE_INACTIVE, mTile.getState().state);
+ }
+
+ @Test
+ public void testActive() throws RemoteException {
+ setScreensaverEnabled(true);
+
+ when(mDreamManager.getDreamComponents()).thenReturn(new ComponentName[]{
+ COLORS_DREAM_COMPONENT_NAME
+ });
+ when(mDreamManager.isDreaming()).thenReturn(true);
+
+ mTile.refreshState();
+ mTestableLooper.processAllMessages();
+ assertEquals(Tile.STATE_ACTIVE, mTile.getState().state);
+ }
+
+ @Test
+ public void testClick() throws RemoteException {
+ // Set the AOSP dream enabled as the base setup.
+ setScreensaverEnabled(true);
+ when(mDreamManager.getDreamComponents()).thenReturn(new ComponentName[]{
+ COLORS_DREAM_COMPONENT_NAME
+ });
+ when(mDreamManager.isDreaming()).thenReturn(false);
+
+ mTile.refreshState();
+ mTestableLooper.processAllMessages();
+ assertEquals(Tile.STATE_INACTIVE, mTile.getState().state);
+
+ // Now click
+ mTile.handleClick(null /* view */);
+
+ verify(mDreamManager).dream();
+
+ when(mDreamManager.isDreaming()).thenReturn(true);
+ mTile.refreshState();
+ mTestableLooper.processAllMessages();
+ assertEquals(Tile.STATE_ACTIVE, mTile.getState().state);
+
+ // Click again to see that other method is called
+ mTile.handleClick(null /* view */);
+
+ verify(mDreamManager).awaken();
+ }
+
+ @Test
+ public void testContentDescription() {
+ assertEquals(mExpectedTileLabel, mTile.getContentDescription(null));
+
+ final String testDreamName = "MyDream";
+ assertEquals(mExpectedTileLabel + ", " + testDreamName,
+ mTile.getContentDescription(testDreamName));
+ }
+
+ private void setScreensaverEnabled(boolean enabled) {
+ mSecureSettings.putIntForUser(Settings.Secure.SCREENSAVER_ENABLED, enabled ? 1 : 0,
+ DEFAULT_USER);
+ }
+}