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); + } +}