Merge "Store state of active TileService" into sc-dev am: 22a01eca60
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/14937370 Change-Id: Id8897654ed8092472bc5ff0ea1317beb69cc57e0
This commit is contained in:
@@ -45,7 +45,9 @@ import com.android.systemui.plugins.qs.QSFactory;
|
||||
import com.android.systemui.plugins.qs.QSTile;
|
||||
import com.android.systemui.plugins.qs.QSTileView;
|
||||
import com.android.systemui.qs.external.CustomTile;
|
||||
import com.android.systemui.qs.external.CustomTileStatePersister;
|
||||
import com.android.systemui.qs.external.TileLifecycleManager;
|
||||
import com.android.systemui.qs.external.TileServiceKey;
|
||||
import com.android.systemui.qs.external.TileServices;
|
||||
import com.android.systemui.qs.logging.QSLogger;
|
||||
import com.android.systemui.settings.UserTracker;
|
||||
@@ -93,6 +95,7 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D
|
||||
private final QSLogger mQSLogger;
|
||||
private final UiEventLogger mUiEventLogger;
|
||||
private final InstanceIdSequence mInstanceIdSequence;
|
||||
private final CustomTileStatePersister mCustomTileStatePersister;
|
||||
|
||||
private final List<Callback> mCallbacks = new ArrayList<>();
|
||||
private AutoTileManager mAutoTiles;
|
||||
@@ -119,7 +122,8 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D
|
||||
QSLogger qsLogger,
|
||||
UiEventLogger uiEventLogger,
|
||||
UserTracker userTracker,
|
||||
SecureSettings secureSettings) {
|
||||
SecureSettings secureSettings,
|
||||
CustomTileStatePersister customTileStatePersister) {
|
||||
mIconController = iconController;
|
||||
mContext = context;
|
||||
mUserContext = context;
|
||||
@@ -139,6 +143,7 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D
|
||||
mDumpManager.registerDumpable(TAG, this);
|
||||
mUserTracker = userTracker;
|
||||
mSecureSettings = secureSettings;
|
||||
mCustomTileStatePersister = customTileStatePersister;
|
||||
|
||||
mainHandler.post(() -> {
|
||||
// This is technically a hack to avoid circular dependency of
|
||||
@@ -418,6 +423,11 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D
|
||||
changeTiles(mTileSpecs, newSpecs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the tiles triggered by the user editing.
|
||||
* <p>
|
||||
* This is not called on device start, or on user change.
|
||||
*/
|
||||
public void changeTiles(List<String> previousTiles, List<String> newTiles) {
|
||||
final List<String> copy = new ArrayList<>(previousTiles);
|
||||
final int NP = copy.size();
|
||||
@@ -433,6 +443,7 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D
|
||||
mBroadcastDispatcher);
|
||||
lifecycleManager.onStopListening();
|
||||
lifecycleManager.onTileRemoved();
|
||||
mCustomTileStatePersister.removeState(new TileServiceKey(component, mCurrentUser));
|
||||
TileLifecycleManager.setTileAdded(mContext, component, false);
|
||||
lifecycleManager.flushMessagesAndUnbind();
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ import android.widget.Switch;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.android.internal.logging.MetricsLogger;
|
||||
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
|
||||
@@ -85,6 +86,7 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener
|
||||
private final IQSTileService mService;
|
||||
private final TileServiceManager mServiceManager;
|
||||
private final int mUser;
|
||||
private final CustomTileStatePersister mCustomTileStatePersister;
|
||||
private android.graphics.drawable.Icon mDefaultIcon;
|
||||
private CharSequence mDefaultLabel;
|
||||
|
||||
@@ -94,6 +96,8 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener
|
||||
private boolean mIsTokenGranted;
|
||||
private boolean mIsShowingDialog;
|
||||
|
||||
private final TileServiceKey mKey;
|
||||
|
||||
private CustomTile(
|
||||
QSHost host,
|
||||
Looper backgroundLooper,
|
||||
@@ -104,7 +108,8 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener
|
||||
ActivityStarter activityStarter,
|
||||
QSLogger qsLogger,
|
||||
String action,
|
||||
Context userContext
|
||||
Context userContext,
|
||||
CustomTileStatePersister customTileStatePersister
|
||||
) {
|
||||
super(host, backgroundLooper, mainHandler, falsingManager, metricsLogger,
|
||||
statusBarStateController, activityStarter, qsLogger);
|
||||
@@ -113,15 +118,29 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener
|
||||
mTile = new Tile();
|
||||
mUserContext = userContext;
|
||||
mUser = mUserContext.getUserId();
|
||||
updateDefaultTileAndIcon();
|
||||
mKey = new TileServiceKey(mComponent, mUser);
|
||||
|
||||
mServiceManager = host.getTileServices().getTileWrapper(this);
|
||||
mService = mServiceManager.getTileService();
|
||||
mCustomTileStatePersister = customTileStatePersister;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleInitialize() {
|
||||
updateDefaultTileAndIcon();
|
||||
if (mServiceManager.isToggleableTile()) {
|
||||
// Replace states with BooleanState
|
||||
resetStates();
|
||||
}
|
||||
|
||||
mService = mServiceManager.getTileService();
|
||||
mServiceManager.setTileChangeListener(this);
|
||||
if (mServiceManager.isActiveTile()) {
|
||||
Tile t = mCustomTileStatePersister.readState(mKey);
|
||||
if (t != null) {
|
||||
applyTileState(t, /* overwriteNulls */ false);
|
||||
mServiceManager.clearPendingBind();
|
||||
refreshState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -191,7 +210,7 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener
|
||||
|
||||
@Override
|
||||
public void onTileChanged(ComponentName tile) {
|
||||
updateDefaultTileAndIcon();
|
||||
mHandler.post(this::updateDefaultTileAndIcon);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -213,16 +232,44 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener
|
||||
}
|
||||
|
||||
public Tile getQsTile() {
|
||||
// TODO(b/191145007) Move to background thread safely
|
||||
updateDefaultTileAndIcon();
|
||||
return mTile;
|
||||
}
|
||||
|
||||
public void updateState(Tile tile) {
|
||||
mTile.setIcon(tile.getIcon());
|
||||
mTile.setLabel(tile.getLabel());
|
||||
mTile.setSubtitle(tile.getSubtitle());
|
||||
mTile.setContentDescription(tile.getContentDescription());
|
||||
mTile.setStateDescription(tile.getStateDescription());
|
||||
/**
|
||||
* Update state of {@link this#mTile} from a remote {@link TileService}.
|
||||
* @param tile tile populated with state to apply
|
||||
*/
|
||||
public void updateTileState(Tile tile) {
|
||||
// This comes from a binder call IQSService.updateQsTile
|
||||
mHandler.post(() -> handleUpdateTileState(tile));
|
||||
}
|
||||
|
||||
private void handleUpdateTileState(Tile tile) {
|
||||
applyTileState(tile, /* overwriteNulls */ true);
|
||||
if (mServiceManager.isActiveTile()) {
|
||||
mCustomTileStatePersister.persistState(mKey, tile);
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private void applyTileState(Tile tile, boolean overwriteNulls) {
|
||||
if (tile.getIcon() != null || overwriteNulls) {
|
||||
mTile.setIcon(tile.getIcon());
|
||||
}
|
||||
if (tile.getLabel() != null || overwriteNulls) {
|
||||
mTile.setLabel(tile.getLabel());
|
||||
}
|
||||
if (tile.getSubtitle() != null || overwriteNulls) {
|
||||
mTile.setSubtitle(tile.getSubtitle());
|
||||
}
|
||||
if (tile.getContentDescription() != null || overwriteNulls) {
|
||||
mTile.setContentDescription(tile.getContentDescription());
|
||||
}
|
||||
if (tile.getStateDescription() != null || overwriteNulls) {
|
||||
mTile.setStateDescription(tile.getStateDescription());
|
||||
}
|
||||
mTile.setState(tile.getState());
|
||||
}
|
||||
|
||||
@@ -459,6 +506,7 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener
|
||||
final StatusBarStateController mStatusBarStateController;
|
||||
final ActivityStarter mActivityStarter;
|
||||
final QSLogger mQSLogger;
|
||||
final CustomTileStatePersister mCustomTileStatePersister;
|
||||
|
||||
Context mUserContext;
|
||||
String mSpec = "";
|
||||
@@ -472,7 +520,8 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener
|
||||
MetricsLogger metricsLogger,
|
||||
StatusBarStateController statusBarStateController,
|
||||
ActivityStarter activityStarter,
|
||||
QSLogger qsLogger
|
||||
QSLogger qsLogger,
|
||||
CustomTileStatePersister customTileStatePersister
|
||||
) {
|
||||
mQSHostLazy = hostLazy;
|
||||
mBackgroundLooper = backgroundLooper;
|
||||
@@ -482,6 +531,7 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener
|
||||
mStatusBarStateController = statusBarStateController;
|
||||
mActivityStarter = activityStarter;
|
||||
mQSLogger = qsLogger;
|
||||
mCustomTileStatePersister = customTileStatePersister;
|
||||
}
|
||||
|
||||
Builder setSpec(@NonNull String spec) {
|
||||
@@ -509,7 +559,8 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener
|
||||
mActivityStarter,
|
||||
mQSLogger,
|
||||
action,
|
||||
mUserContext
|
||||
mUserContext,
|
||||
mCustomTileStatePersister
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
122
packages/SystemUI/src/com/android/systemui/qs/external/CustomTileStatePersister.kt
vendored
Normal file
122
packages/SystemUI/src/com/android/systemui/qs/external/CustomTileStatePersister.kt
vendored
Normal file
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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.external
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.service.quicksettings.Tile
|
||||
import android.util.Log
|
||||
import com.android.internal.annotations.VisibleForTesting
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import javax.inject.Inject
|
||||
|
||||
data class TileServiceKey(val componentName: ComponentName, val user: Int) {
|
||||
private val string = "${componentName.flattenToString()}:$user"
|
||||
override fun toString() = string
|
||||
}
|
||||
private const val STATE = "state"
|
||||
private const val LABEL = "label"
|
||||
private const val SUBTITLE = "subtitle"
|
||||
private const val CONTENT_DESCRIPTION = "content_description"
|
||||
private const val STATE_DESCRIPTION = "state_description"
|
||||
|
||||
/**
|
||||
* Persists and retrieves state for [CustomTile].
|
||||
*
|
||||
* This class will persists to a fixed [SharedPreference] file a state for a pair of [ComponentName]
|
||||
* and user id ([TileServiceKey]).
|
||||
*
|
||||
* It persists the state from a [Tile] necessary to present the view in the same state when
|
||||
* retrieved, with the exception of the icon.
|
||||
*/
|
||||
class CustomTileStatePersister @Inject constructor(context: Context) {
|
||||
companion object {
|
||||
private const val FILE_NAME = "custom_tiles_state"
|
||||
}
|
||||
|
||||
private val sharedPreferences = context.getSharedPreferences(FILE_NAME, 0)
|
||||
|
||||
/**
|
||||
* Read the state from [SharedPreferences].
|
||||
*
|
||||
* Returns `null` if the tile has no saved state.
|
||||
*
|
||||
* Any fields that have not been saved will be set to `null`
|
||||
*/
|
||||
fun readState(key: TileServiceKey): Tile? {
|
||||
val state = sharedPreferences.getString(key.toString(), null) ?: return null
|
||||
return try {
|
||||
readTileFromString(state)
|
||||
} catch (e: JSONException) {
|
||||
Log.e("TileServicePersistence", "Bad saved state: $state", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists the state into [SharedPreferences].
|
||||
*
|
||||
* The implementation does not store fields that are `null` or icons.
|
||||
*/
|
||||
fun persistState(key: TileServiceKey, tile: Tile) {
|
||||
val state = writeToString(tile)
|
||||
|
||||
sharedPreferences.edit().putString(key.toString(), state).apply()
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the state for a given tile, user pair.
|
||||
*
|
||||
* Used when the tile is removed by the user.
|
||||
*/
|
||||
fun removeState(key: TileServiceKey) {
|
||||
sharedPreferences.edit().remove(key.toString()).apply()
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun readTileFromString(stateString: String): Tile {
|
||||
val json = JSONObject(stateString)
|
||||
return Tile().apply {
|
||||
state = json.getInt(STATE)
|
||||
label = json.getStringOrNull(LABEL)
|
||||
subtitle = json.getStringOrNull(SUBTITLE)
|
||||
contentDescription = json.getStringOrNull(CONTENT_DESCRIPTION)
|
||||
stateDescription = json.getStringOrNull(STATE_DESCRIPTION)
|
||||
}
|
||||
}
|
||||
|
||||
// Properties with null values will not be saved to the Json string in any way. This makes sure
|
||||
// to properly retrieve a null in that case.
|
||||
private fun JSONObject.getStringOrNull(name: String): String? {
|
||||
return if (has(name)) getString(name) else null
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun writeToString(tile: Tile): String {
|
||||
// Not storing the icon
|
||||
return with(tile) {
|
||||
JSONObject()
|
||||
.put(STATE, state)
|
||||
.put(LABEL, label)
|
||||
.put(SUBTITLE, subtitle)
|
||||
.put(CONTENT_DESCRIPTION, contentDescription)
|
||||
.put(STATE_DESCRIPTION, stateDescription)
|
||||
.toString()
|
||||
}
|
||||
}
|
||||
@@ -204,7 +204,7 @@ public class TileServices extends IQSService.Stub {
|
||||
tileServiceManager.clearPendingBind();
|
||||
tileServiceManager.setLastUpdate(System.currentTimeMillis());
|
||||
}
|
||||
customTile.updateState(tile);
|
||||
customTile.updateTileState(tile);
|
||||
customTile.refreshState();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,7 +160,8 @@ public class QSFactoryImpl implements QSFactory {
|
||||
public QSTile createTile(String tileSpec) {
|
||||
QSTileImpl tile = createTileInternal(tileSpec);
|
||||
if (tile != null) {
|
||||
tile.handleStale(); // Tile was just created, must be stale.
|
||||
tile.initialize();
|
||||
tile.postStale(); // Tile was just created, must be stale.
|
||||
}
|
||||
return tile;
|
||||
}
|
||||
|
||||
@@ -159,6 +159,15 @@ public abstract class QSTileImpl<TState extends State> implements QSTile, Lifecy
|
||||
*/
|
||||
abstract public int getMetricsCategory();
|
||||
|
||||
/**
|
||||
* Performs initialization of the tile
|
||||
*
|
||||
* Use this to perform initialization of the tile. Empty by default.
|
||||
*/
|
||||
protected void handleInitialize() {
|
||||
|
||||
}
|
||||
|
||||
protected QSTileImpl(
|
||||
QSHost host,
|
||||
Looper backgroundLooper,
|
||||
@@ -347,6 +356,15 @@ public abstract class QSTileImpl<TState extends State> implements QSTile, Lifecy
|
||||
mHandler.sendEmptyMessage(H.DESTROY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules initialization of the tile.
|
||||
*
|
||||
* Should be called upon creation of the tile, before performing other operations
|
||||
*/
|
||||
public void initialize() {
|
||||
mHandler.sendEmptyMessage(H.INITIALIZE);
|
||||
}
|
||||
|
||||
public TState getState() {
|
||||
return mState;
|
||||
}
|
||||
@@ -370,6 +388,13 @@ public abstract class QSTileImpl<TState extends State> implements QSTile, Lifecy
|
||||
mCallbacks.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Posts a stale message to the background thread.
|
||||
*/
|
||||
public void postStale() {
|
||||
mHandler.sendEmptyMessage(H.STALE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles secondary click on the tile.
|
||||
*
|
||||
@@ -582,6 +607,7 @@ public abstract class QSTileImpl<TState extends State> implements QSTile, Lifecy
|
||||
private static final int SET_LISTENING = 13;
|
||||
@VisibleForTesting
|
||||
protected static final int STALE = 14;
|
||||
private static final int INITIALIZE = 15;
|
||||
|
||||
@VisibleForTesting
|
||||
protected H(Looper looper) {
|
||||
@@ -640,6 +666,9 @@ public abstract class QSTileImpl<TState extends State> implements QSTile, Lifecy
|
||||
} else if (msg.what == STALE) {
|
||||
name = "handleStale";
|
||||
handleStale();
|
||||
} else if (msg.what == INITIALIZE) {
|
||||
name = "initialize";
|
||||
handleInitialize();
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unknown msg: " + msg.what);
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ import com.android.systemui.dump.DumpManager;
|
||||
import com.android.systemui.media.MediaHost;
|
||||
import com.android.systemui.plugins.statusbar.StatusBarStateController;
|
||||
import com.android.systemui.qs.dagger.QSFragmentComponent;
|
||||
import com.android.systemui.qs.external.CustomTileStatePersister;
|
||||
import com.android.systemui.qs.logging.QSLogger;
|
||||
import com.android.systemui.qs.tileimpl.QSFactoryImpl;
|
||||
import com.android.systemui.settings.UserTracker;
|
||||
@@ -132,7 +133,7 @@ public class QSFragmentTest extends SysuiBaseFragmentTest {
|
||||
() -> mock(AutoTileManager.class), mock(DumpManager.class),
|
||||
mock(BroadcastDispatcher.class), Optional.of(mock(StatusBar.class)),
|
||||
mock(QSLogger.class), mock(UiEventLogger.class), mock(UserTracker.class),
|
||||
mock(SecureSettings.class));
|
||||
mock(SecureSettings.class), mock(CustomTileStatePersister.class));
|
||||
qs.setHost(host);
|
||||
|
||||
qs.setListening(true);
|
||||
|
||||
@@ -59,6 +59,8 @@ import com.android.systemui.plugins.qs.QSFactory;
|
||||
import com.android.systemui.plugins.qs.QSTile;
|
||||
import com.android.systemui.plugins.statusbar.StatusBarStateController;
|
||||
import com.android.systemui.qs.external.CustomTile;
|
||||
import com.android.systemui.qs.external.CustomTileStatePersister;
|
||||
import com.android.systemui.qs.external.TileServiceKey;
|
||||
import com.android.systemui.qs.logging.QSLogger;
|
||||
import com.android.systemui.qs.tileimpl.QSTileImpl;
|
||||
import com.android.systemui.settings.UserTracker;
|
||||
@@ -125,6 +127,8 @@ public class QSTileHostTest extends SysuiTestCase {
|
||||
private UserTracker mUserTracker;
|
||||
@Mock
|
||||
private SecureSettings mSecureSettings;
|
||||
@Mock
|
||||
private CustomTileStatePersister mCustomTileStatePersister;
|
||||
|
||||
private Handler mHandler;
|
||||
private TestableLooper mLooper;
|
||||
@@ -145,7 +149,7 @@ public class QSTileHostTest extends SysuiTestCase {
|
||||
mQSTileHost = new TestQSTileHost(mContext, mIconController, mDefaultFactory, mHandler,
|
||||
mLooper.getLooper(), mPluginManager, mTunerService, mAutoTiles, mDumpManager,
|
||||
mBroadcastDispatcher, mStatusBar, mQSLogger, mUiEventLogger, mUserTracker,
|
||||
mSecureSettings);
|
||||
mSecureSettings, mCustomTileStatePersister);
|
||||
setUpTileFactory();
|
||||
|
||||
when(mSecureSettings.getStringForUser(eq(QSTileHost.TILES_SETTING), anyInt()))
|
||||
@@ -371,6 +375,14 @@ public class QSTileHostTest extends SysuiTestCase {
|
||||
verify(mQSLogger, never()).logTileDestroyed(isNull(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCustomTileRemoved_stateDeleted() {
|
||||
mQSTileHost.changeTiles(List.of(CUSTOM_TILE_SPEC), List.of());
|
||||
|
||||
verify(mCustomTileStatePersister)
|
||||
.removeState(new TileServiceKey(CUSTOM_TILE, mQSTileHost.getUserId()));
|
||||
}
|
||||
|
||||
private class TestQSTileHost extends QSTileHost {
|
||||
TestQSTileHost(Context context, StatusBarIconController iconController,
|
||||
QSFactory defaultFactory, Handler mainHandler, Looper bgLooper,
|
||||
@@ -378,10 +390,11 @@ public class QSTileHostTest extends SysuiTestCase {
|
||||
Provider<AutoTileManager> autoTiles, DumpManager dumpManager,
|
||||
BroadcastDispatcher broadcastDispatcher, StatusBar statusBar, QSLogger qsLogger,
|
||||
UiEventLogger uiEventLogger, UserTracker userTracker,
|
||||
SecureSettings secureSettings) {
|
||||
SecureSettings secureSettings, CustomTileStatePersister customTileStatePersister) {
|
||||
super(context, iconController, defaultFactory, mainHandler, bgLooper, pluginManager,
|
||||
tunerService, autoTiles, dumpManager, broadcastDispatcher,
|
||||
Optional.of(statusBar), qsLogger, uiEventLogger, userTracker, secureSettings);
|
||||
Optional.of(statusBar), qsLogger, uiEventLogger, userTracker, secureSettings,
|
||||
customTileStatePersister);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
159
packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileStatePersisterTest.kt
vendored
Normal file
159
packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileStatePersisterTest.kt
vendored
Normal file
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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.external
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.service.quicksettings.Tile
|
||||
import android.testing.AndroidTestingRunner
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.util.mockito.capture
|
||||
import com.android.systemui.util.mockito.eq
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.Answers
|
||||
import org.mockito.ArgumentCaptor
|
||||
import org.mockito.ArgumentMatchers.any
|
||||
import org.mockito.ArgumentMatchers.anyInt
|
||||
import org.mockito.ArgumentMatchers.anyString
|
||||
import org.mockito.Captor
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito.`when`
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.MockitoAnnotations
|
||||
|
||||
@SmallTest
|
||||
@RunWith(AndroidTestingRunner::class)
|
||||
class CustomTileStatePersisterTest : SysuiTestCase() {
|
||||
|
||||
companion object {
|
||||
private val TEST_COMPONENT = ComponentName("pkg", "cls")
|
||||
private const val TEST_USER = 0
|
||||
private val KEY = TileServiceKey(TEST_COMPONENT, TEST_USER)
|
||||
|
||||
private const val TEST_STATE = Tile.STATE_INACTIVE
|
||||
private const val TEST_LABEL = "test_label"
|
||||
private const val TEST_SUBTITLE = "test_subtitle"
|
||||
private const val TEST_CONTENT_DESCRIPTION = "test_content_description"
|
||||
private const val TEST_STATE_DESCRIPTION = "test_state_description"
|
||||
|
||||
private fun Tile.isEqualTo(other: Tile): Boolean {
|
||||
return state == other.state &&
|
||||
label == other.label &&
|
||||
subtitle == other.subtitle &&
|
||||
contentDescription == other.contentDescription &&
|
||||
stateDescription == other.stateDescription
|
||||
}
|
||||
}
|
||||
|
||||
@Mock
|
||||
private lateinit var mockContext: Context
|
||||
@Mock
|
||||
private lateinit var sharedPreferences: SharedPreferences
|
||||
@Mock(answer = Answers.RETURNS_SELF)
|
||||
private lateinit var editor: SharedPreferences.Editor
|
||||
private lateinit var tile: Tile
|
||||
private lateinit var customTileStatePersister: CustomTileStatePersister
|
||||
|
||||
@Captor
|
||||
private lateinit var stringCaptor: ArgumentCaptor<String>
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
`when`(mockContext.getSharedPreferences(anyString(), anyInt()))
|
||||
.thenReturn(sharedPreferences)
|
||||
`when`(sharedPreferences.edit()).thenReturn(editor)
|
||||
|
||||
tile = Tile()
|
||||
customTileStatePersister = CustomTileStatePersister(mockContext)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testWriteState() {
|
||||
tile.apply {
|
||||
state = TEST_STATE
|
||||
label = TEST_LABEL
|
||||
subtitle = TEST_SUBTITLE
|
||||
contentDescription = TEST_CONTENT_DESCRIPTION
|
||||
stateDescription = TEST_STATE_DESCRIPTION
|
||||
}
|
||||
|
||||
customTileStatePersister.persistState(KEY, tile)
|
||||
|
||||
verify(editor).putString(eq(KEY.toString()), capture(stringCaptor))
|
||||
|
||||
assertThat(tile.isEqualTo(readTileFromString(stringCaptor.value))).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testReadState() {
|
||||
tile.apply {
|
||||
state = TEST_STATE
|
||||
label = TEST_LABEL
|
||||
subtitle = TEST_SUBTITLE
|
||||
contentDescription = TEST_CONTENT_DESCRIPTION
|
||||
stateDescription = TEST_STATE_DESCRIPTION
|
||||
}
|
||||
|
||||
`when`(sharedPreferences.getString(eq(KEY.toString()), any()))
|
||||
.thenReturn(writeToString(tile))
|
||||
|
||||
assertThat(tile.isEqualTo(customTileStatePersister.readState(KEY)!!)).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testReadStateDefault() {
|
||||
`when`(sharedPreferences.getString(any(), any())).thenAnswer {
|
||||
it.getArgument(1)
|
||||
}
|
||||
|
||||
assertThat(customTileStatePersister.readState(KEY)).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testStoreNulls() {
|
||||
assertThat(tile.label).isNull()
|
||||
|
||||
customTileStatePersister.persistState(KEY, tile)
|
||||
|
||||
verify(editor).putString(eq(KEY.toString()), capture(stringCaptor))
|
||||
|
||||
assertThat(readTileFromString(stringCaptor.value).label).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testReadNulls() {
|
||||
assertThat(tile.label).isNull()
|
||||
|
||||
`when`(sharedPreferences.getString(eq(KEY.toString()), any()))
|
||||
.thenReturn(writeToString(tile))
|
||||
|
||||
assertThat(customTileStatePersister.readState(KEY)!!.label).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRemoveState() {
|
||||
customTileStatePersister.removeState(KEY)
|
||||
|
||||
verify(editor).remove(KEY.toString())
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@ 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.logging.QSLogger
|
||||
import com.android.systemui.util.mockito.any
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
@@ -48,8 +49,9 @@ import org.mockito.ArgumentMatchers.anyInt
|
||||
import org.mockito.ArgumentMatchers.anyString
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito.`when`
|
||||
import org.mockito.Mockito.any
|
||||
import org.mockito.Mockito.mock
|
||||
import org.mockito.Mockito.never
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.MockitoAnnotations
|
||||
|
||||
@SmallTest
|
||||
@@ -76,6 +78,7 @@ class CustomTileTest : SysuiTestCase() {
|
||||
@Mock private lateinit var packageManager: PackageManager
|
||||
@Mock private lateinit var applicationInfo: ApplicationInfo
|
||||
@Mock private lateinit var serviceInfo: ServiceInfo
|
||||
@Mock private lateinit var customTileStatePersister: CustomTileStatePersister
|
||||
|
||||
private lateinit var customTile: CustomTile
|
||||
private lateinit var testableLooper: TestableLooper
|
||||
@@ -108,10 +111,13 @@ class CustomTileTest : SysuiTestCase() {
|
||||
metricsLogger,
|
||||
statusBarStateController,
|
||||
activityStarter,
|
||||
qsLogger
|
||||
qsLogger,
|
||||
customTileStatePersister
|
||||
)
|
||||
|
||||
customTile = CustomTile.create(customTileBuilder, TILE_SPEC, mContext)
|
||||
customTile.initialize()
|
||||
testableLooper.processAllMessages()
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -123,6 +129,8 @@ class CustomTileTest : SysuiTestCase() {
|
||||
`when`(userContext.userId).thenReturn(10)
|
||||
|
||||
val tile = CustomTile.create(customTileBuilder, TILE_SPEC, userContext)
|
||||
tile.initialize()
|
||||
testableLooper.processAllMessages()
|
||||
|
||||
assertEquals(10, tile.user)
|
||||
}
|
||||
@@ -131,6 +139,8 @@ class CustomTileTest : SysuiTestCase() {
|
||||
fun testToggleableTileHasBooleanState() {
|
||||
`when`(tileServiceManager.isToggleableTile).thenReturn(true)
|
||||
customTile = CustomTile.create(customTileBuilder, TILE_SPEC, mContext)
|
||||
customTile.initialize()
|
||||
testableLooper.processAllMessages()
|
||||
|
||||
assertTrue(customTile.state is QSTile.BooleanState)
|
||||
assertTrue(customTile.newTileState() is QSTile.BooleanState)
|
||||
@@ -146,6 +156,9 @@ class CustomTileTest : SysuiTestCase() {
|
||||
fun testValueUpdatedInBooleanTile() {
|
||||
`when`(tileServiceManager.isToggleableTile).thenReturn(true)
|
||||
customTile = CustomTile.create(customTileBuilder, TILE_SPEC, mContext)
|
||||
customTile.initialize()
|
||||
testableLooper.processAllMessages()
|
||||
|
||||
customTile.qsTile.icon = mock(Icon::class.java)
|
||||
`when`(customTile.qsTile.icon.loadDrawable(any(Context::class.java)))
|
||||
.thenReturn(mock(Drawable::class.java))
|
||||
@@ -173,4 +186,88 @@ class CustomTileTest : SysuiTestCase() {
|
||||
.thenReturn(null)
|
||||
customTile.handleUpdateState(customTile.newTileState(), null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNoLoadStateTileNotActive() {
|
||||
// Not active by default
|
||||
testableLooper.processAllMessages()
|
||||
|
||||
verify(customTileStatePersister, never()).readState(any())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNoPersistedStateTileNotActive() {
|
||||
// Not active by default
|
||||
val t = Tile().apply {
|
||||
state = Tile.STATE_INACTIVE
|
||||
}
|
||||
customTile.updateTileState(t)
|
||||
testableLooper.processAllMessages()
|
||||
|
||||
verify(customTileStatePersister, never()).persistState(any(), any())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPersistedStateRetrieved() {
|
||||
val state = Tile.STATE_INACTIVE
|
||||
val label = "test_label"
|
||||
val subtitle = "test_subtitle"
|
||||
val contentDescription = "test_content_description"
|
||||
val stateDescription = "test_state_description"
|
||||
|
||||
val t = Tile().apply {
|
||||
this.state = state
|
||||
this.label = label
|
||||
this.subtitle = subtitle
|
||||
this.contentDescription = contentDescription
|
||||
this.stateDescription = stateDescription
|
||||
}
|
||||
`when`(tileServiceManager.isActiveTile).thenReturn(true)
|
||||
`when`(customTileStatePersister
|
||||
.readState(TileServiceKey(componentName, customTile.user))).thenReturn(t)
|
||||
val tile = CustomTile.create(customTileBuilder, TILE_SPEC, mContext)
|
||||
tile.initialize()
|
||||
testableLooper.processAllMessages()
|
||||
|
||||
// Make sure we have an icon in the tile because we don't have a default icon
|
||||
// This should not be overridden by the retrieved tile that has null icon.
|
||||
tile.qsTile.icon = mock(Icon::class.java)
|
||||
`when`(tile.qsTile.icon.loadDrawable(any(Context::class.java)))
|
||||
.thenReturn(mock(Drawable::class.java))
|
||||
|
||||
tile.refreshState()
|
||||
|
||||
testableLooper.processAllMessages()
|
||||
|
||||
val tileState = tile.state
|
||||
|
||||
assertEquals(state, tileState.state)
|
||||
assertEquals(label, tileState.label)
|
||||
assertEquals(subtitle, tileState.secondaryLabel)
|
||||
assertEquals(contentDescription, tileState.contentDescription)
|
||||
assertEquals(stateDescription, tileState.stateDescription)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testStoreStateOnChange() {
|
||||
val t = Tile().apply {
|
||||
state = Tile.STATE_INACTIVE
|
||||
label = "test_label"
|
||||
subtitle = "test_subtitle"
|
||||
contentDescription = "test_content_description"
|
||||
stateDescription = "test_state_description"
|
||||
}
|
||||
`when`(tileServiceManager.isActiveTile).thenReturn(true)
|
||||
|
||||
val tile = CustomTile.create(customTileBuilder, TILE_SPEC, mContext)
|
||||
tile.initialize()
|
||||
testableLooper.processAllMessages()
|
||||
|
||||
tile.updateTileState(t)
|
||||
|
||||
testableLooper.processAllMessages()
|
||||
|
||||
verify(customTileStatePersister)
|
||||
.persistState(TileServiceKey(componentName, customTile.user), t)
|
||||
}
|
||||
}
|
||||
@@ -118,7 +118,8 @@ public class TileServicesTest extends SysuiTestCase {
|
||||
mQSLogger,
|
||||
mUiEventLogger,
|
||||
mUserTracker,
|
||||
mSecureSettings);
|
||||
mSecureSettings,
|
||||
mock(CustomTileStatePersister.class));
|
||||
mTileService = new TestTileServices(host, Looper.getMainLooper(), mBroadcastDispatcher,
|
||||
mUserTracker);
|
||||
}
|
||||
|
||||
@@ -121,6 +121,9 @@ public class QSTileImplTest extends SysuiTestCase {
|
||||
|
||||
mTile = new TileImpl(mHost, mTestableLooper.getLooper(), mainHandler, mFalsingManager,
|
||||
mMetricsLogger, mStatusBarStateController, mActivityStarter, mQsLogger);
|
||||
mTile.initialize();
|
||||
mTestableLooper.processAllMessages();
|
||||
|
||||
mTile.setTileSpec(SPEC);
|
||||
}
|
||||
|
||||
|
||||
@@ -80,6 +80,8 @@ class AlarmTileTest : SysuiTestCase() {
|
||||
nextAlarmController
|
||||
)
|
||||
|
||||
tile.initialize()
|
||||
|
||||
verify(nextAlarmController).observe(eq(tile), capture(callbackCaptor))
|
||||
tile.refreshState()
|
||||
testableLooper.processAllMessages()
|
||||
|
||||
@@ -87,6 +87,9 @@ class BatterySaverTileTest : SysuiTestCase() {
|
||||
qsLogger,
|
||||
batteryController,
|
||||
secureSettings)
|
||||
|
||||
tile.initialize()
|
||||
testableLooper.processAllMessages()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -112,11 +112,14 @@ public class CastTileTest extends SysuiTestCase {
|
||||
mNetworkController,
|
||||
mHotspotController
|
||||
);
|
||||
mCastTile.initialize();
|
||||
|
||||
// We are not setting the mocks to listening, so we trigger a first refresh state to
|
||||
// set the initial state
|
||||
mCastTile.refreshState();
|
||||
|
||||
mTestableLooper.processAllMessages();
|
||||
|
||||
mCastTile.handleSetListening(true);
|
||||
ArgumentCaptor<NetworkController.SignalCallback> signalCallbackArgumentCaptor =
|
||||
ArgumentCaptor.forClass(NetworkController.SignalCallback.class);
|
||||
|
||||
@@ -378,7 +378,10 @@ class DeviceControlsTileTest : SysuiTestCase() {
|
||||
qsLogger,
|
||||
controlsComponent,
|
||||
keyguardStateController
|
||||
)
|
||||
).also {
|
||||
it.initialize()
|
||||
testableLooper.processAllMessages()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -92,6 +92,9 @@ public class NfcTileTest extends SysuiTestCase {
|
||||
mQSLogger,
|
||||
mBroadcastDispatcher
|
||||
);
|
||||
|
||||
mNfcTile.initialize();
|
||||
mTestableLooper.processAllMessages();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -155,6 +155,9 @@ public class QuickAccessWalletTileTest extends SysuiTestCase {
|
||||
mPackageManager,
|
||||
mSecureSettings,
|
||||
mController);
|
||||
|
||||
mTile.initialize();
|
||||
mTestableLooper.processAllMessages();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -89,6 +89,9 @@ public class ReduceBrightColorsTileTest extends SysuiTestCase {
|
||||
mActivityStarter,
|
||||
mQSLogger
|
||||
);
|
||||
|
||||
mTile.initialize();
|
||||
mTestableLooper.processAllMessages();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -91,6 +91,9 @@ public class ScreenRecordTileTest extends SysuiTestCase {
|
||||
mController,
|
||||
mKeyguardDismissUtil
|
||||
);
|
||||
|
||||
mTile.initialize();
|
||||
mTestableLooper.processAllMessages();
|
||||
}
|
||||
|
||||
// Test that the tile is inactive and labeled correctly when the controller is neither starting
|
||||
|
||||
Reference in New Issue
Block a user