Store state of active TileService
These tiles don't get polled on device startup or user change. Persist their state (minus the icon) and retrieve when the CustomTile is created. Add an initialize method for QSTileImpl that gets handled in a background thread. Test: manual Test: atest com.android.systemui.qs Test: atest TileServiceTest ActiveTileServiceTest BooleanTileServiceTest Fixes: 176789277 Change-Id: Id7775f4f2df56c25c13e2192d1bb824cb3b6a22a
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;
|
||||
}
|
||||
|
||||
@@ -158,6 +158,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,
|
||||
@@ -346,6 +355,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;
|
||||
}
|
||||
@@ -369,6 +387,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.
|
||||
*
|
||||
@@ -580,6 +605,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) {
|
||||
@@ -638,6 +664,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
|
||||
|
||||
@@ -90,6 +90,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