diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java index 525bad8a0e253..6ddf2a75f491f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java @@ -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, D private final QSLogger mQSLogger; private final UiEventLogger mUiEventLogger; private final InstanceIdSequence mInstanceIdSequence; + private final CustomTileStatePersister mCustomTileStatePersister; private final List mCallbacks = new ArrayList<>(); private AutoTileManager mAutoTiles; @@ -119,7 +122,8 @@ public class QSTileHost implements QSHost, Tunable, PluginListener, 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, 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, D changeTiles(mTileSpecs, newSpecs); } + /** + * Change the tiles triggered by the user editing. + *

+ * This is not called on device start, or on user change. + */ public void changeTiles(List previousTiles, List newTiles) { final List copy = new ArrayList<>(previousTiles); final int NP = copy.size(); @@ -433,6 +443,7 @@ public class QSTileHost implements QSHost, Tunable, PluginListener, D mBroadcastDispatcher); lifecycleManager.onStopListening(); lifecycleManager.onTileRemoved(); + mCustomTileStatePersister.removeState(new TileServiceKey(component, mCurrentUser)); TileLifecycleManager.setTileAdded(mContext, component, false); lifecycleManager.flushMessagesAndUnbind(); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java index 10eea828bcb44..396eca5c1bee9 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java @@ -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 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 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 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 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 implements TileChangeListener @Override public void onTileChanged(ComponentName tile) { - updateDefaultTileAndIcon(); + mHandler.post(this::updateDefaultTileAndIcon); } @Override @@ -213,16 +232,44 @@ public class CustomTile extends QSTileImpl 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 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 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 implements TileChangeListener mStatusBarStateController = statusBarStateController; mActivityStarter = activityStarter; mQSLogger = qsLogger; + mCustomTileStatePersister = customTileStatePersister; } Builder setSpec(@NonNull String spec) { @@ -509,7 +559,8 @@ public class CustomTile extends QSTileImpl implements TileChangeListener mActivityStarter, mQSLogger, action, - mUserContext + mUserContext, + mCustomTileStatePersister ); } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/CustomTileStatePersister.kt b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTileStatePersister.kt new file mode 100644 index 0000000000000..021e632810f94 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTileStatePersister.kt @@ -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() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java index 35cf2a12745e3..a7cd11314d7e6 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java +++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java @@ -204,7 +204,7 @@ public class TileServices extends IQSService.Stub { tileServiceManager.clearPendingBind(); tileServiceManager.setLastUpdate(System.currentTimeMillis()); } - customTile.updateState(tile); + customTile.updateTileState(tile); customTile.refreshState(); } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java index 8f7c493417ecd..842fd6c62d06d 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java @@ -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; } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java index a938821a343fc..48a49e60875a5 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java @@ -158,6 +158,15 @@ public abstract class QSTileImpl 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 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 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 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 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); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java index b0e3e3e936a94..2ae4cbe17ac6d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java @@ -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); diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java index 7c73b4c44e907..69bdcbcff270e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java @@ -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 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 diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileStatePersisterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileStatePersisterTest.kt new file mode 100644 index 0000000000000..6c96576bcbc13 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileStatePersisterTest.kt @@ -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 + + @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()) + } +} \ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileTest.kt index b1c3d1da8fea9..9b5c1619ef316 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileTest.kt @@ -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) + } } \ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServicesTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServicesTest.java index 641f917bcfbe1..2b18404622912 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServicesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServicesTest.java @@ -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); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java index 80231a49bb441..ea4d7cc2529cf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java @@ -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); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/AlarmTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/AlarmTileTest.kt index 32b1f433dfcf2..5e2d8fde84da9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/AlarmTileTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/AlarmTileTest.kt @@ -80,6 +80,8 @@ class AlarmTileTest : SysuiTestCase() { nextAlarmController ) + tile.initialize() + verify(nextAlarmController).observe(eq(tile), capture(callbackCaptor)) tile.refreshState() testableLooper.processAllMessages() diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BatterySaverTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BatterySaverTileTest.kt index f17bd56d00522..1bf83513d4725 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BatterySaverTileTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BatterySaverTileTest.kt @@ -87,6 +87,9 @@ class BatterySaverTileTest : SysuiTestCase() { qsLogger, batteryController, secureSettings) + + tile.initialize() + testableLooper.processAllMessages() } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/CastTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/CastTileTest.java index 7c1a5f5ebf309..d44a526077078 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/CastTileTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/CastTileTest.java @@ -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 signalCallbackArgumentCaptor = ArgumentCaptor.forClass(NetworkController.SignalCallback.class); diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DeviceControlsTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DeviceControlsTileTest.kt index 6d1bbd9708eac..94af10a485fd2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DeviceControlsTileTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DeviceControlsTileTest.kt @@ -378,7 +378,10 @@ class DeviceControlsTileTest : SysuiTestCase() { qsLogger, controlsComponent, keyguardStateController - ) + ).also { + it.initialize() + testableLooper.processAllMessages() + } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/NfcTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/NfcTileTest.java index 99d028cd8c5c2..cfd37358dcff4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/NfcTileTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/NfcTileTest.java @@ -92,6 +92,9 @@ public class NfcTileTest extends SysuiTestCase { mQSLogger, mBroadcastDispatcher ); + + mNfcTile.initialize(); + mTestableLooper.processAllMessages(); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/QuickAccessWalletTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/QuickAccessWalletTileTest.java index e4a9aacb57abd..a50cbe5adc483 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/QuickAccessWalletTileTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/QuickAccessWalletTileTest.java @@ -155,6 +155,9 @@ public class QuickAccessWalletTileTest extends SysuiTestCase { mPackageManager, mSecureSettings, mController); + + mTile.initialize(); + mTestableLooper.processAllMessages(); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ReduceBrightColorsTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ReduceBrightColorsTileTest.java index df4908ddc4ef9..9eb688de35118 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ReduceBrightColorsTileTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ReduceBrightColorsTileTest.java @@ -89,6 +89,9 @@ public class ReduceBrightColorsTileTest extends SysuiTestCase { mActivityStarter, mQSLogger ); + + mTile.initialize(); + mTestableLooper.processAllMessages(); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java index e4af21aa7cd40..8b882e8e10abf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java @@ -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