From e30529d71427904e957cfff6e8d53c214675d91f Mon Sep 17 00:00:00 2001 From: Fabian Kozynski Date: Thu, 10 Jun 2021 11:31:25 -0400 Subject: [PATCH] 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 --- .../com/android/systemui/qs/QSTileHost.java | 13 +- .../systemui/qs/external/CustomTile.java | 77 +++++++-- .../qs/external/CustomTileStatePersister.kt | 122 ++++++++++++++ .../systemui/qs/external/TileServices.java | 2 +- .../systemui/qs/tileimpl/QSFactoryImpl.java | 3 +- .../systemui/qs/tileimpl/QSTileImpl.java | 29 ++++ .../android/systemui/qs/QSFragmentTest.java | 3 +- .../android/systemui/qs/QSTileHostTest.java | 19 ++- .../external/CustomTileStatePersisterTest.kt | 159 ++++++++++++++++++ .../systemui/qs/external/CustomTileTest.kt | 101 ++++++++++- .../qs/external/TileServicesTest.java | 3 +- .../systemui/qs/tileimpl/QSTileImplTest.java | 3 + .../systemui/qs/tiles/AlarmTileTest.kt | 2 + .../systemui/qs/tiles/BatterySaverTileTest.kt | 3 + .../systemui/qs/tiles/CastTileTest.java | 3 + .../qs/tiles/DeviceControlsTileTest.kt | 5 +- .../systemui/qs/tiles/NfcTileTest.java | 3 + .../qs/tiles/QuickAccessWalletTileTest.java | 3 + .../qs/tiles/ReduceBrightColorsTileTest.java | 3 + .../qs/tiles/ScreenRecordTileTest.java | 3 + 20 files changed, 535 insertions(+), 24 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/qs/external/CustomTileStatePersister.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileStatePersisterTest.kt 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