Merge "Store state of active TileService" into sc-dev

This commit is contained in:
Fabian Kozynski
2021-06-17 14:40:52 +00:00
committed by Android (Google) Code Review
20 changed files with 535 additions and 24 deletions

View File

@@ -45,7 +45,9 @@ import com.android.systemui.plugins.qs.QSFactory;
import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.plugins.qs.QSTile;
import com.android.systemui.plugins.qs.QSTileView; import com.android.systemui.plugins.qs.QSTileView;
import com.android.systemui.qs.external.CustomTile; 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.TileLifecycleManager;
import com.android.systemui.qs.external.TileServiceKey;
import com.android.systemui.qs.external.TileServices; import com.android.systemui.qs.external.TileServices;
import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.qs.logging.QSLogger;
import com.android.systemui.settings.UserTracker; 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 QSLogger mQSLogger;
private final UiEventLogger mUiEventLogger; private final UiEventLogger mUiEventLogger;
private final InstanceIdSequence mInstanceIdSequence; private final InstanceIdSequence mInstanceIdSequence;
private final CustomTileStatePersister mCustomTileStatePersister;
private final List<Callback> mCallbacks = new ArrayList<>(); private final List<Callback> mCallbacks = new ArrayList<>();
private AutoTileManager mAutoTiles; private AutoTileManager mAutoTiles;
@@ -119,7 +122,8 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D
QSLogger qsLogger, QSLogger qsLogger,
UiEventLogger uiEventLogger, UiEventLogger uiEventLogger,
UserTracker userTracker, UserTracker userTracker,
SecureSettings secureSettings) { SecureSettings secureSettings,
CustomTileStatePersister customTileStatePersister) {
mIconController = iconController; mIconController = iconController;
mContext = context; mContext = context;
mUserContext = context; mUserContext = context;
@@ -139,6 +143,7 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D
mDumpManager.registerDumpable(TAG, this); mDumpManager.registerDumpable(TAG, this);
mUserTracker = userTracker; mUserTracker = userTracker;
mSecureSettings = secureSettings; mSecureSettings = secureSettings;
mCustomTileStatePersister = customTileStatePersister;
mainHandler.post(() -> { mainHandler.post(() -> {
// This is technically a hack to avoid circular dependency of // 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); 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) { public void changeTiles(List<String> previousTiles, List<String> newTiles) {
final List<String> copy = new ArrayList<>(previousTiles); final List<String> copy = new ArrayList<>(previousTiles);
final int NP = copy.size(); final int NP = copy.size();
@@ -433,6 +443,7 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D
mBroadcastDispatcher); mBroadcastDispatcher);
lifecycleManager.onStopListening(); lifecycleManager.onStopListening();
lifecycleManager.onTileRemoved(); lifecycleManager.onTileRemoved();
mCustomTileStatePersister.removeState(new TileServiceKey(component, mCurrentUser));
TileLifecycleManager.setTileAdded(mContext, component, false); TileLifecycleManager.setTileAdded(mContext, component, false);
lifecycleManager.flushMessagesAndUnbind(); lifecycleManager.flushMessagesAndUnbind();
} }

View File

@@ -46,6 +46,7 @@ import android.widget.Switch;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 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 IQSTileService mService;
private final TileServiceManager mServiceManager; private final TileServiceManager mServiceManager;
private final int mUser; private final int mUser;
private final CustomTileStatePersister mCustomTileStatePersister;
private android.graphics.drawable.Icon mDefaultIcon; private android.graphics.drawable.Icon mDefaultIcon;
private CharSequence mDefaultLabel; private CharSequence mDefaultLabel;
@@ -94,6 +96,8 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener
private boolean mIsTokenGranted; private boolean mIsTokenGranted;
private boolean mIsShowingDialog; private boolean mIsShowingDialog;
private final TileServiceKey mKey;
private CustomTile( private CustomTile(
QSHost host, QSHost host,
Looper backgroundLooper, Looper backgroundLooper,
@@ -104,7 +108,8 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener
ActivityStarter activityStarter, ActivityStarter activityStarter,
QSLogger qsLogger, QSLogger qsLogger,
String action, String action,
Context userContext Context userContext,
CustomTileStatePersister customTileStatePersister
) { ) {
super(host, backgroundLooper, mainHandler, falsingManager, metricsLogger, super(host, backgroundLooper, mainHandler, falsingManager, metricsLogger,
statusBarStateController, activityStarter, qsLogger); statusBarStateController, activityStarter, qsLogger);
@@ -113,15 +118,29 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener
mTile = new Tile(); mTile = new Tile();
mUserContext = userContext; mUserContext = userContext;
mUser = mUserContext.getUserId(); mUser = mUserContext.getUserId();
updateDefaultTileAndIcon(); mKey = new TileServiceKey(mComponent, mUser);
mServiceManager = host.getTileServices().getTileWrapper(this); mServiceManager = host.getTileServices().getTileWrapper(this);
mService = mServiceManager.getTileService();
mCustomTileStatePersister = customTileStatePersister;
}
@Override
protected void handleInitialize() {
updateDefaultTileAndIcon();
if (mServiceManager.isToggleableTile()) { if (mServiceManager.isToggleableTile()) {
// Replace states with BooleanState // Replace states with BooleanState
resetStates(); resetStates();
} }
mService = mServiceManager.getTileService();
mServiceManager.setTileChangeListener(this); mServiceManager.setTileChangeListener(this);
if (mServiceManager.isActiveTile()) {
Tile t = mCustomTileStatePersister.readState(mKey);
if (t != null) {
applyTileState(t, /* overwriteNulls */ false);
mServiceManager.clearPendingBind();
refreshState();
}
}
} }
@Override @Override
@@ -191,7 +210,7 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener
@Override @Override
public void onTileChanged(ComponentName tile) { public void onTileChanged(ComponentName tile) {
updateDefaultTileAndIcon(); mHandler.post(this::updateDefaultTileAndIcon);
} }
@Override @Override
@@ -213,16 +232,44 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener
} }
public Tile getQsTile() { public Tile getQsTile() {
// TODO(b/191145007) Move to background thread safely
updateDefaultTileAndIcon(); updateDefaultTileAndIcon();
return mTile; return mTile;
} }
public void updateState(Tile tile) { /**
mTile.setIcon(tile.getIcon()); * Update state of {@link this#mTile} from a remote {@link TileService}.
mTile.setLabel(tile.getLabel()); * @param tile tile populated with state to apply
mTile.setSubtitle(tile.getSubtitle()); */
mTile.setContentDescription(tile.getContentDescription()); public void updateTileState(Tile tile) {
mTile.setStateDescription(tile.getStateDescription()); // 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()); mTile.setState(tile.getState());
} }
@@ -459,6 +506,7 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener
final StatusBarStateController mStatusBarStateController; final StatusBarStateController mStatusBarStateController;
final ActivityStarter mActivityStarter; final ActivityStarter mActivityStarter;
final QSLogger mQSLogger; final QSLogger mQSLogger;
final CustomTileStatePersister mCustomTileStatePersister;
Context mUserContext; Context mUserContext;
String mSpec = ""; String mSpec = "";
@@ -472,7 +520,8 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener
MetricsLogger metricsLogger, MetricsLogger metricsLogger,
StatusBarStateController statusBarStateController, StatusBarStateController statusBarStateController,
ActivityStarter activityStarter, ActivityStarter activityStarter,
QSLogger qsLogger QSLogger qsLogger,
CustomTileStatePersister customTileStatePersister
) { ) {
mQSHostLazy = hostLazy; mQSHostLazy = hostLazy;
mBackgroundLooper = backgroundLooper; mBackgroundLooper = backgroundLooper;
@@ -482,6 +531,7 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener
mStatusBarStateController = statusBarStateController; mStatusBarStateController = statusBarStateController;
mActivityStarter = activityStarter; mActivityStarter = activityStarter;
mQSLogger = qsLogger; mQSLogger = qsLogger;
mCustomTileStatePersister = customTileStatePersister;
} }
Builder setSpec(@NonNull String spec) { Builder setSpec(@NonNull String spec) {
@@ -509,7 +559,8 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener
mActivityStarter, mActivityStarter,
mQSLogger, mQSLogger,
action, action,
mUserContext mUserContext,
mCustomTileStatePersister
); );
} }
} }

View 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()
}
}

View File

@@ -204,7 +204,7 @@ public class TileServices extends IQSService.Stub {
tileServiceManager.clearPendingBind(); tileServiceManager.clearPendingBind();
tileServiceManager.setLastUpdate(System.currentTimeMillis()); tileServiceManager.setLastUpdate(System.currentTimeMillis());
} }
customTile.updateState(tile); customTile.updateTileState(tile);
customTile.refreshState(); customTile.refreshState();
} }
} }

View File

@@ -160,7 +160,8 @@ public class QSFactoryImpl implements QSFactory {
public QSTile createTile(String tileSpec) { public QSTile createTile(String tileSpec) {
QSTileImpl tile = createTileInternal(tileSpec); QSTileImpl tile = createTileInternal(tileSpec);
if (tile != null) { 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; return tile;
} }

View File

@@ -159,6 +159,15 @@ public abstract class QSTileImpl<TState extends State> implements QSTile, Lifecy
*/ */
abstract public int getMetricsCategory(); 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( protected QSTileImpl(
QSHost host, QSHost host,
Looper backgroundLooper, Looper backgroundLooper,
@@ -347,6 +356,15 @@ public abstract class QSTileImpl<TState extends State> implements QSTile, Lifecy
mHandler.sendEmptyMessage(H.DESTROY); 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() { public TState getState() {
return mState; return mState;
} }
@@ -370,6 +388,13 @@ public abstract class QSTileImpl<TState extends State> implements QSTile, Lifecy
mCallbacks.clear(); mCallbacks.clear();
} }
/**
* Posts a stale message to the background thread.
*/
public void postStale() {
mHandler.sendEmptyMessage(H.STALE);
}
/** /**
* Handles secondary click on the tile. * 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; private static final int SET_LISTENING = 13;
@VisibleForTesting @VisibleForTesting
protected static final int STALE = 14; protected static final int STALE = 14;
private static final int INITIALIZE = 15;
@VisibleForTesting @VisibleForTesting
protected H(Looper looper) { protected H(Looper looper) {
@@ -640,6 +666,9 @@ public abstract class QSTileImpl<TState extends State> implements QSTile, Lifecy
} else if (msg.what == STALE) { } else if (msg.what == STALE) {
name = "handleStale"; name = "handleStale";
handleStale(); handleStale();
} else if (msg.what == INITIALIZE) {
name = "initialize";
handleInitialize();
} else { } else {
throw new IllegalArgumentException("Unknown msg: " + msg.what); throw new IllegalArgumentException("Unknown msg: " + msg.what);
} }

View File

@@ -46,6 +46,7 @@ import com.android.systemui.dump.DumpManager;
import com.android.systemui.media.MediaHost; import com.android.systemui.media.MediaHost;
import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.qs.dagger.QSFragmentComponent; 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.logging.QSLogger;
import com.android.systemui.qs.tileimpl.QSFactoryImpl; import com.android.systemui.qs.tileimpl.QSFactoryImpl;
import com.android.systemui.settings.UserTracker; import com.android.systemui.settings.UserTracker;
@@ -132,7 +133,7 @@ public class QSFragmentTest extends SysuiBaseFragmentTest {
() -> mock(AutoTileManager.class), mock(DumpManager.class), () -> mock(AutoTileManager.class), mock(DumpManager.class),
mock(BroadcastDispatcher.class), Optional.of(mock(StatusBar.class)), mock(BroadcastDispatcher.class), Optional.of(mock(StatusBar.class)),
mock(QSLogger.class), mock(UiEventLogger.class), mock(UserTracker.class), mock(QSLogger.class), mock(UiEventLogger.class), mock(UserTracker.class),
mock(SecureSettings.class)); mock(SecureSettings.class), mock(CustomTileStatePersister.class));
qs.setHost(host); qs.setHost(host);
qs.setListening(true); qs.setListening(true);

View File

@@ -59,6 +59,8 @@ import com.android.systemui.plugins.qs.QSFactory;
import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.plugins.qs.QSTile;
import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.qs.external.CustomTile; 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.logging.QSLogger;
import com.android.systemui.qs.tileimpl.QSTileImpl; import com.android.systemui.qs.tileimpl.QSTileImpl;
import com.android.systemui.settings.UserTracker; import com.android.systemui.settings.UserTracker;
@@ -125,6 +127,8 @@ public class QSTileHostTest extends SysuiTestCase {
private UserTracker mUserTracker; private UserTracker mUserTracker;
@Mock @Mock
private SecureSettings mSecureSettings; private SecureSettings mSecureSettings;
@Mock
private CustomTileStatePersister mCustomTileStatePersister;
private Handler mHandler; private Handler mHandler;
private TestableLooper mLooper; private TestableLooper mLooper;
@@ -145,7 +149,7 @@ public class QSTileHostTest extends SysuiTestCase {
mQSTileHost = new TestQSTileHost(mContext, mIconController, mDefaultFactory, mHandler, mQSTileHost = new TestQSTileHost(mContext, mIconController, mDefaultFactory, mHandler,
mLooper.getLooper(), mPluginManager, mTunerService, mAutoTiles, mDumpManager, mLooper.getLooper(), mPluginManager, mTunerService, mAutoTiles, mDumpManager,
mBroadcastDispatcher, mStatusBar, mQSLogger, mUiEventLogger, mUserTracker, mBroadcastDispatcher, mStatusBar, mQSLogger, mUiEventLogger, mUserTracker,
mSecureSettings); mSecureSettings, mCustomTileStatePersister);
setUpTileFactory(); setUpTileFactory();
when(mSecureSettings.getStringForUser(eq(QSTileHost.TILES_SETTING), anyInt())) when(mSecureSettings.getStringForUser(eq(QSTileHost.TILES_SETTING), anyInt()))
@@ -371,6 +375,14 @@ public class QSTileHostTest extends SysuiTestCase {
verify(mQSLogger, never()).logTileDestroyed(isNull(), anyString()); 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 { private class TestQSTileHost extends QSTileHost {
TestQSTileHost(Context context, StatusBarIconController iconController, TestQSTileHost(Context context, StatusBarIconController iconController,
QSFactory defaultFactory, Handler mainHandler, Looper bgLooper, QSFactory defaultFactory, Handler mainHandler, Looper bgLooper,
@@ -378,10 +390,11 @@ public class QSTileHostTest extends SysuiTestCase {
Provider<AutoTileManager> autoTiles, DumpManager dumpManager, Provider<AutoTileManager> autoTiles, DumpManager dumpManager,
BroadcastDispatcher broadcastDispatcher, StatusBar statusBar, QSLogger qsLogger, BroadcastDispatcher broadcastDispatcher, StatusBar statusBar, QSLogger qsLogger,
UiEventLogger uiEventLogger, UserTracker userTracker, UiEventLogger uiEventLogger, UserTracker userTracker,
SecureSettings secureSettings) { SecureSettings secureSettings, CustomTileStatePersister customTileStatePersister) {
super(context, iconController, defaultFactory, mainHandler, bgLooper, pluginManager, super(context, iconController, defaultFactory, mainHandler, bgLooper, pluginManager,
tunerService, autoTiles, dumpManager, broadcastDispatcher, tunerService, autoTiles, dumpManager, broadcastDispatcher,
Optional.of(statusBar), qsLogger, uiEventLogger, userTracker, secureSettings); Optional.of(statusBar), qsLogger, uiEventLogger, userTracker, secureSettings,
customTileStatePersister);
} }
@Override @Override

View 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())
}
}

View File

@@ -38,6 +38,7 @@ import com.android.systemui.plugins.qs.QSTile
import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.qs.QSHost import com.android.systemui.qs.QSHost
import com.android.systemui.qs.logging.QSLogger import com.android.systemui.qs.logging.QSLogger
import com.android.systemui.util.mockito.any
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
@@ -48,8 +49,9 @@ import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.anyString import org.mockito.ArgumentMatchers.anyString
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito.`when` import org.mockito.Mockito.`when`
import org.mockito.Mockito.any
import org.mockito.Mockito.mock import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations import org.mockito.MockitoAnnotations
@SmallTest @SmallTest
@@ -76,6 +78,7 @@ class CustomTileTest : SysuiTestCase() {
@Mock private lateinit var packageManager: PackageManager @Mock private lateinit var packageManager: PackageManager
@Mock private lateinit var applicationInfo: ApplicationInfo @Mock private lateinit var applicationInfo: ApplicationInfo
@Mock private lateinit var serviceInfo: ServiceInfo @Mock private lateinit var serviceInfo: ServiceInfo
@Mock private lateinit var customTileStatePersister: CustomTileStatePersister
private lateinit var customTile: CustomTile private lateinit var customTile: CustomTile
private lateinit var testableLooper: TestableLooper private lateinit var testableLooper: TestableLooper
@@ -108,10 +111,13 @@ class CustomTileTest : SysuiTestCase() {
metricsLogger, metricsLogger,
statusBarStateController, statusBarStateController,
activityStarter, activityStarter,
qsLogger qsLogger,
customTileStatePersister
) )
customTile = CustomTile.create(customTileBuilder, TILE_SPEC, mContext) customTile = CustomTile.create(customTileBuilder, TILE_SPEC, mContext)
customTile.initialize()
testableLooper.processAllMessages()
} }
@Test @Test
@@ -123,6 +129,8 @@ class CustomTileTest : SysuiTestCase() {
`when`(userContext.userId).thenReturn(10) `when`(userContext.userId).thenReturn(10)
val tile = CustomTile.create(customTileBuilder, TILE_SPEC, userContext) val tile = CustomTile.create(customTileBuilder, TILE_SPEC, userContext)
tile.initialize()
testableLooper.processAllMessages()
assertEquals(10, tile.user) assertEquals(10, tile.user)
} }
@@ -131,6 +139,8 @@ class CustomTileTest : SysuiTestCase() {
fun testToggleableTileHasBooleanState() { fun testToggleableTileHasBooleanState() {
`when`(tileServiceManager.isToggleableTile).thenReturn(true) `when`(tileServiceManager.isToggleableTile).thenReturn(true)
customTile = CustomTile.create(customTileBuilder, TILE_SPEC, mContext) customTile = CustomTile.create(customTileBuilder, TILE_SPEC, mContext)
customTile.initialize()
testableLooper.processAllMessages()
assertTrue(customTile.state is QSTile.BooleanState) assertTrue(customTile.state is QSTile.BooleanState)
assertTrue(customTile.newTileState() is QSTile.BooleanState) assertTrue(customTile.newTileState() is QSTile.BooleanState)
@@ -146,6 +156,9 @@ class CustomTileTest : SysuiTestCase() {
fun testValueUpdatedInBooleanTile() { fun testValueUpdatedInBooleanTile() {
`when`(tileServiceManager.isToggleableTile).thenReturn(true) `when`(tileServiceManager.isToggleableTile).thenReturn(true)
customTile = CustomTile.create(customTileBuilder, TILE_SPEC, mContext) customTile = CustomTile.create(customTileBuilder, TILE_SPEC, mContext)
customTile.initialize()
testableLooper.processAllMessages()
customTile.qsTile.icon = mock(Icon::class.java) customTile.qsTile.icon = mock(Icon::class.java)
`when`(customTile.qsTile.icon.loadDrawable(any(Context::class.java))) `when`(customTile.qsTile.icon.loadDrawable(any(Context::class.java)))
.thenReturn(mock(Drawable::class.java)) .thenReturn(mock(Drawable::class.java))
@@ -173,4 +186,88 @@ class CustomTileTest : SysuiTestCase() {
.thenReturn(null) .thenReturn(null)
customTile.handleUpdateState(customTile.newTileState(), 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)
}
} }

View File

@@ -118,7 +118,8 @@ public class TileServicesTest extends SysuiTestCase {
mQSLogger, mQSLogger,
mUiEventLogger, mUiEventLogger,
mUserTracker, mUserTracker,
mSecureSettings); mSecureSettings,
mock(CustomTileStatePersister.class));
mTileService = new TestTileServices(host, Looper.getMainLooper(), mBroadcastDispatcher, mTileService = new TestTileServices(host, Looper.getMainLooper(), mBroadcastDispatcher,
mUserTracker); mUserTracker);
} }

View File

@@ -121,6 +121,9 @@ public class QSTileImplTest extends SysuiTestCase {
mTile = new TileImpl(mHost, mTestableLooper.getLooper(), mainHandler, mFalsingManager, mTile = new TileImpl(mHost, mTestableLooper.getLooper(), mainHandler, mFalsingManager,
mMetricsLogger, mStatusBarStateController, mActivityStarter, mQsLogger); mMetricsLogger, mStatusBarStateController, mActivityStarter, mQsLogger);
mTile.initialize();
mTestableLooper.processAllMessages();
mTile.setTileSpec(SPEC); mTile.setTileSpec(SPEC);
} }

View File

@@ -80,6 +80,8 @@ class AlarmTileTest : SysuiTestCase() {
nextAlarmController nextAlarmController
) )
tile.initialize()
verify(nextAlarmController).observe(eq(tile), capture(callbackCaptor)) verify(nextAlarmController).observe(eq(tile), capture(callbackCaptor))
tile.refreshState() tile.refreshState()
testableLooper.processAllMessages() testableLooper.processAllMessages()

View File

@@ -87,6 +87,9 @@ class BatterySaverTileTest : SysuiTestCase() {
qsLogger, qsLogger,
batteryController, batteryController,
secureSettings) secureSettings)
tile.initialize()
testableLooper.processAllMessages()
} }
@Test @Test

View File

@@ -112,11 +112,14 @@ public class CastTileTest extends SysuiTestCase {
mNetworkController, mNetworkController,
mHotspotController mHotspotController
); );
mCastTile.initialize();
// We are not setting the mocks to listening, so we trigger a first refresh state to // We are not setting the mocks to listening, so we trigger a first refresh state to
// set the initial state // set the initial state
mCastTile.refreshState(); mCastTile.refreshState();
mTestableLooper.processAllMessages();
mCastTile.handleSetListening(true); mCastTile.handleSetListening(true);
ArgumentCaptor<NetworkController.SignalCallback> signalCallbackArgumentCaptor = ArgumentCaptor<NetworkController.SignalCallback> signalCallbackArgumentCaptor =
ArgumentCaptor.forClass(NetworkController.SignalCallback.class); ArgumentCaptor.forClass(NetworkController.SignalCallback.class);

View File

@@ -378,7 +378,10 @@ class DeviceControlsTileTest : SysuiTestCase() {
qsLogger, qsLogger,
controlsComponent, controlsComponent,
keyguardStateController keyguardStateController
) ).also {
it.initialize()
testableLooper.processAllMessages()
}
} }
} }

View File

@@ -92,6 +92,9 @@ public class NfcTileTest extends SysuiTestCase {
mQSLogger, mQSLogger,
mBroadcastDispatcher mBroadcastDispatcher
); );
mNfcTile.initialize();
mTestableLooper.processAllMessages();
} }
@Test @Test

View File

@@ -155,6 +155,9 @@ public class QuickAccessWalletTileTest extends SysuiTestCase {
mPackageManager, mPackageManager,
mSecureSettings, mSecureSettings,
mController); mController);
mTile.initialize();
mTestableLooper.processAllMessages();
} }
@Test @Test

View File

@@ -89,6 +89,9 @@ public class ReduceBrightColorsTileTest extends SysuiTestCase {
mActivityStarter, mActivityStarter,
mQSLogger mQSLogger
); );
mTile.initialize();
mTestableLooper.processAllMessages();
} }
@Test @Test

View File

@@ -91,6 +91,9 @@ public class ScreenRecordTileTest extends SysuiTestCase {
mController, mController,
mKeyguardDismissUtil mKeyguardDismissUtil
); );
mTile.initialize();
mTestableLooper.processAllMessages();
} }
// Test that the tile is inactive and labeled correctly when the controller is neither starting // Test that the tile is inactive and labeled correctly when the controller is neither starting