Merge "Creates Dream Tile." into tm-qpr-dev

This commit is contained in:
Justin Koh
2022-05-03 22:44:12 +00:00
committed by Android (Google) Code Review
8 changed files with 458 additions and 5 deletions

View File

@@ -87,7 +87,7 @@
<!-- Tiles native to System UI. Order should match "quick_settings_tiles_default" -->
<string name="quick_settings_tiles_stock" translatable="false">
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
</string>
<!-- The tiles to display in QuickSettings -->

View File

@@ -578,6 +578,8 @@
<!-- QuickSettings: Location [CHAR LIMIT=NONE] -->
<string name="quick_settings_location_label">Location</string>
<!-- QuickSettings: Location (Off) [CHAR LIMIT=NONE] -->
<!-- QuickSettings: Screen saver [CHAR LIMIT=NONE] -->
<string name="quick_settings_screensaver_label">Screen saver</string>
<!-- QuickSettings: Camera [CHAR LIMIT=NONE] -->
<string name="quick_settings_camera_label">Camera access</string>
<!-- QuickSettings: Microphone [CHAR LIMIT=NONE] -->

View File

@@ -308,4 +308,14 @@
<item>Off</item>
<item>On</item>
</string-array>
<!-- State names for dream (screensaver) tile: unavailable, off, on.
This subtitle is shown when the tile is in that particular state but does not set its own
subtitle, so some of these may never appear on screen. They should still be translated as
if they could appear. [CHAR LIMIT=32] -->
<string-array name="tile_states_dream">
<item>Unavailable</item>
<item>Off</item>
<item>On</item>
</string-array>
</resources>

View File

@@ -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<QuickAccessWalletTile> mQuickAccessWalletTileProvider;
private final Provider<QRCodeScannerTile> mQRCodeScannerTileProvider;
private final Provider<OneHandedModeTile> mOneHandedModeTileProvider;
private final Provider<DreamTile> mDreamTileProvider;
private final Lazy<QSHost> mQsHostLazy;
private final Provider<CustomTile.Builder> mCustomTileBuilderProvider;
@@ -132,7 +134,8 @@ public class QSFactoryImpl implements QSFactory {
Provider<QuickAccessWalletTile> quickAccessWalletTileProvider,
Provider<QRCodeScannerTile> qrCodeScannerTileProvider,
Provider<OneHandedModeTile> oneHandedModeTileProvider,
Provider<ColorCorrectionTile> colorCorrectionTileProvider) {
Provider<ColorCorrectionTile> colorCorrectionTileProvider,
Provider<DreamTile> 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

View File

@@ -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 {

View File

@@ -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<QSTile.BooleanState> {
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;
}
}

View File

@@ -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]
}

View File

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