diff options
8 files changed, 694 insertions, 45 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java index 77652c9ddf2f..ac46c85c10a4 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java @@ -29,6 +29,7 @@ import android.util.Log; import androidx.annotation.MainThread; import androidx.annotation.Nullable; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.InstanceId; import com.android.internal.logging.InstanceIdSequence; import com.android.internal.logging.UiEventLogger; @@ -47,6 +48,7 @@ import com.android.systemui.qs.external.TileLifecycleManager; import com.android.systemui.qs.external.TileServiceKey; import com.android.systemui.qs.external.TileServiceRequestController; import com.android.systemui.qs.logging.QSLogger; +import com.android.systemui.settings.UserFileManager; import com.android.systemui.settings.UserTracker; import com.android.systemui.shared.plugins.PluginManager; import com.android.systemui.statusbar.phone.AutoTileManager; @@ -88,6 +90,10 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D public static final int POSITION_AT_END = -1; public static final String TILES_SETTING = Secure.QS_TILES; + // Shared prefs that hold tile lifecycle info. + @VisibleForTesting + static final String TILES = "tiles_prefs"; + private final Context mContext; private final LinkedHashMap<String, QSTile> mTiles = new LinkedHashMap<>(); protected final ArrayList<String> mTileSpecs = new ArrayList<>(); @@ -99,6 +105,7 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D private final InstanceIdSequence mInstanceIdSequence; private final CustomTileStatePersister mCustomTileStatePersister; private final Executor mMainExecutor; + private final UserFileManager mUserFileManager; private final List<Callback> mCallbacks = new ArrayList<>(); @Nullable @@ -135,7 +142,8 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D SecureSettings secureSettings, CustomTileStatePersister customTileStatePersister, TileServiceRequestController.Builder tileServiceRequestControllerBuilder, - TileLifecycleManager.Factory tileLifecycleManagerFactory + TileLifecycleManager.Factory tileLifecycleManagerFactory, + UserFileManager userFileManager ) { mIconController = iconController; mContext = context; @@ -148,6 +156,7 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D mMainExecutor = mainExecutor; mTileServiceRequestController = tileServiceRequestControllerBuilder.create(this); mTileLifeCycleManagerFactory = tileLifecycleManagerFactory; + mUserFileManager = userFileManager; mInstanceIdSequence = new InstanceIdSequence(MAX_QS_INSTANCE_ID); mCentralSurfacesOptional = centralSurfacesOptional; @@ -392,6 +401,11 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D */ @Override public void removeTile(String spec) { + if (spec.startsWith(CustomTile.PREFIX)) { + // If the tile is removed (due to it not actually existing), mark it as removed. That + // way it will be marked as newly added if it appears in the future. + setTileAdded(CustomTile.getComponentFromSpec(spec), mCurrentUser, false); + } mMainExecutor.execute(() -> changeTileSpecs(tileSpecs-> tileSpecs.remove(spec))); } @@ -515,7 +529,7 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D lifecycleManager.onStopListening(); lifecycleManager.onTileRemoved(); mCustomTileStatePersister.removeState(new TileServiceKey(component, mCurrentUser)); - TileLifecycleManager.setTileAdded(mContext, component, false); + setTileAdded(component, mCurrentUser, false); lifecycleManager.flushMessagesAndUnbind(); } } @@ -552,6 +566,36 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D throw new RuntimeException("Default factory didn't create view for " + tile.getTileSpec()); } + /** + * Check if a particular {@link CustomTile} has been added for a user and has not been removed + * since. + * @param componentName the {@link ComponentName} of the + * {@link android.service.quicksettings.TileService} associated with the + * tile. + * @param userId the user to check + */ + public boolean isTileAdded(ComponentName componentName, int userId) { + return mUserFileManager + .getSharedPreferences(TILES, 0, userId) + .getBoolean(componentName.flattenToString(), false); + } + + /** + * Persists whether a particular {@link CustomTile} has been added and it's currently in the + * set of selected tiles ({@link #mTiles}. + * @param componentName the {@link ComponentName} of the + * {@link android.service.quicksettings.TileService} associated + * with the tile. + * @param userId the user for this tile + * @param added {@code true} if the tile is being added, {@code false} otherwise + */ + public void setTileAdded(ComponentName componentName, int userId, boolean added) { + mUserFileManager.getSharedPreferences(TILES, 0, userId) + .edit() + .putBoolean(componentName.flattenToString(), added) + .apply(); + } + protected static List<String> loadTileSpecs(Context context, String tileList) { final Resources res = context.getResources(); diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java index a49d3fd16591..3e445ddfc2a1 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java +++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java @@ -127,6 +127,10 @@ public class TileLifecycleManager extends BroadcastReceiver implements TileLifecycleManager create(Intent intent, UserHandle userHandle); } + public int getUserId() { + return mUser.getIdentifier(); + } + public ComponentName getComponent() { return mIntent.getComponent(); } @@ -507,13 +511,4 @@ public class TileLifecycleManager extends BroadcastReceiver implements public interface TileChangeListener { void onTileChanged(ComponentName tile); } - - public static boolean isTileAdded(Context context, ComponentName component) { - return context.getSharedPreferences(TILES, 0).getBoolean(component.flattenToString(), false); - } - - public static void setTileAdded(Context context, ComponentName component, boolean added) { - context.getSharedPreferences(TILES, 0).edit().putBoolean(component.flattenToString(), - added).commit(); - } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java index cfc57db2eeb8..e86bd7a30490 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java +++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java @@ -109,9 +109,9 @@ public class TileServiceManager { void startLifecycleManagerAndAddTile() { mStarted = true; ComponentName component = mStateManager.getComponent(); - Context context = mServices.getContext(); - if (!TileLifecycleManager.isTileAdded(context, component)) { - TileLifecycleManager.setTileAdded(context, component, true); + final int userId = mStateManager.getUserId(); + if (!mServices.getHost().isTileAdded(component, userId)) { + mServices.getHost().setTileAdded(component, userId, true); mStateManager.onTileAdded(); mStateManager.flushMessagesAndUnbind(); } 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 7dbc561fbfc1..3c58b6fc1354 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java @@ -22,6 +22,7 @@ import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.mock; @@ -32,11 +33,13 @@ import static org.mockito.Mockito.when; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; import android.database.ContentObserver; import android.os.Handler; import android.os.Looper; import android.os.UserHandle; import android.testing.AndroidTestingRunner; +import android.util.SparseArray; import android.view.View; import androidx.annotation.Nullable; @@ -60,12 +63,14 @@ import com.android.systemui.qs.external.TileServiceKey; import com.android.systemui.qs.external.TileServiceRequestController; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.qs.tileimpl.QSTileImpl; +import com.android.systemui.settings.UserFileManager; import com.android.systemui.settings.UserTracker; import com.android.systemui.shared.plugins.PluginManager; import com.android.systemui.statusbar.phone.AutoTileManager; import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.statusbar.phone.StatusBarIconController; import com.android.systemui.tuner.TunerService; +import com.android.systemui.util.FakeSharedPreferences; import com.android.systemui.util.concurrency.FakeExecutor; import com.android.systemui.util.settings.FakeSettings; import com.android.systemui.util.settings.SecureSettings; @@ -76,6 +81,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.mockito.stubbing.Answer; import java.io.PrintWriter; import java.io.StringWriter; @@ -130,6 +136,10 @@ public class QSTileHostTest extends SysuiTestCase { private TileLifecycleManager.Factory mTileLifecycleManagerFactory; @Mock private TileLifecycleManager mTileLifecycleManager; + @Mock + private UserFileManager mUserFileManager; + + private SparseArray<SharedPreferences> mSharedPreferencesByUser; private FakeExecutor mMainExecutor; @@ -140,17 +150,29 @@ public class QSTileHostTest extends SysuiTestCase { MockitoAnnotations.initMocks(this); mMainExecutor = new FakeExecutor(new FakeSystemClock()); + mSharedPreferencesByUser = new SparseArray<>(); + when(mTileServiceRequestControllerBuilder.create(any())) .thenReturn(mTileServiceRequestController); when(mTileLifecycleManagerFactory.create(any(Intent.class), any(UserHandle.class))) .thenReturn(mTileLifecycleManager); + when(mUserFileManager.getSharedPreferences(anyString(), anyInt(), anyInt())) + .thenAnswer((Answer<SharedPreferences>) invocation -> { + assertEquals(QSTileHost.TILES, invocation.getArgument(0)); + int userId = invocation.getArgument(2); + if (!mSharedPreferencesByUser.contains(userId)) { + mSharedPreferencesByUser.put(userId, new FakeSharedPreferences()); + } + return mSharedPreferencesByUser.get(userId); + }); mSecureSettings = new FakeSettings(); saveSetting(""); mQSTileHost = new TestQSTileHost(mContext, mIconController, mDefaultFactory, mMainExecutor, mPluginManager, mTunerService, mAutoTiles, mDumpManager, mCentralSurfaces, mQSLogger, mUiEventLogger, mUserTracker, mSecureSettings, mCustomTileStatePersister, - mTileServiceRequestControllerBuilder, mTileLifecycleManagerFactory); + mTileServiceRequestControllerBuilder, mTileLifecycleManagerFactory, + mUserFileManager); mSecureSettings.registerContentObserverForUser(SETTING, new ContentObserver(null) { @Override @@ -528,6 +550,118 @@ public class QSTileHostTest extends SysuiTestCase { assertEquals("spec1", getSetting()); } + @Test + public void testIsTileAdded_true() { + int user = mUserTracker.getUserId(); + getSharedPreferenecesForUser(user) + .edit() + .putBoolean(CUSTOM_TILE.flattenToString(), true) + .apply(); + + assertTrue(mQSTileHost.isTileAdded(CUSTOM_TILE, user)); + } + + @Test + public void testIsTileAdded_false() { + int user = mUserTracker.getUserId(); + getSharedPreferenecesForUser(user) + .edit() + .putBoolean(CUSTOM_TILE.flattenToString(), false) + .apply(); + + assertFalse(mQSTileHost.isTileAdded(CUSTOM_TILE, user)); + } + + @Test + public void testIsTileAdded_notSet() { + int user = mUserTracker.getUserId(); + + assertFalse(mQSTileHost.isTileAdded(CUSTOM_TILE, user)); + } + + @Test + public void testIsTileAdded_differentUser() { + int user = mUserTracker.getUserId(); + mUserFileManager.getSharedPreferences(QSTileHost.TILES, 0, user) + .edit() + .putBoolean(CUSTOM_TILE.flattenToString(), true) + .apply(); + + assertFalse(mQSTileHost.isTileAdded(CUSTOM_TILE, user + 1)); + } + + @Test + public void testSetTileAdded_true() { + int user = mUserTracker.getUserId(); + mQSTileHost.setTileAdded(CUSTOM_TILE, user, true); + + assertTrue(getSharedPreferenecesForUser(user) + .getBoolean(CUSTOM_TILE.flattenToString(), false)); + } + + @Test + public void testSetTileAdded_false() { + int user = mUserTracker.getUserId(); + mQSTileHost.setTileAdded(CUSTOM_TILE, user, false); + + assertFalse(getSharedPreferenecesForUser(user) + .getBoolean(CUSTOM_TILE.flattenToString(), false)); + } + + @Test + public void testSetTileAdded_differentUser() { + int user = mUserTracker.getUserId(); + mQSTileHost.setTileAdded(CUSTOM_TILE, user, true); + + assertFalse(getSharedPreferenecesForUser(user + 1) + .getBoolean(CUSTOM_TILE.flattenToString(), false)); + } + + @Test + public void testSetTileRemoved_afterCustomTileChangedByUser() { + int user = mUserTracker.getUserId(); + saveSetting(CUSTOM_TILE_SPEC); + + // This will be done by TileServiceManager + mQSTileHost.setTileAdded(CUSTOM_TILE, user, true); + + mQSTileHost.changeTilesByUser(mQSTileHost.mTileSpecs, List.of("spec1")); + assertFalse(getSharedPreferenecesForUser(user) + .getBoolean(CUSTOM_TILE.flattenToString(), false)); + } + + @Test + public void testSetTileRemoved_removedByUser() { + int user = mUserTracker.getUserId(); + saveSetting(CUSTOM_TILE_SPEC); + + // This will be done by TileServiceManager + mQSTileHost.setTileAdded(CUSTOM_TILE, user, true); + + mQSTileHost.removeTileByUser(CUSTOM_TILE); + mMainExecutor.runAllReady(); + assertFalse(getSharedPreferenecesForUser(user) + .getBoolean(CUSTOM_TILE.flattenToString(), false)); + } + + @Test + public void testSetTileRemoved_removedBySystem() { + int user = mUserTracker.getUserId(); + saveSetting("spec1" + CUSTOM_TILE_SPEC); + + // This will be done by TileServiceManager + mQSTileHost.setTileAdded(CUSTOM_TILE, user, true); + + mQSTileHost.removeTile(CUSTOM_TILE_SPEC); + mMainExecutor.runAllReady(); + assertFalse(getSharedPreferenecesForUser(user) + .getBoolean(CUSTOM_TILE.flattenToString(), false)); + } + + private SharedPreferences getSharedPreferenecesForUser(int user) { + return mUserFileManager.getSharedPreferences(QSTileHost.TILES, 0, user); + } + private class TestQSTileHost extends QSTileHost { TestQSTileHost(Context context, StatusBarIconController iconController, QSFactory defaultFactory, Executor mainExecutor, @@ -537,11 +671,13 @@ public class QSTileHostTest extends SysuiTestCase { UserTracker userTracker, SecureSettings secureSettings, CustomTileStatePersister customTileStatePersister, TileServiceRequestController.Builder tileServiceRequestControllerBuilder, - TileLifecycleManager.Factory tileLifecycleManagerFactory) { + TileLifecycleManager.Factory tileLifecycleManagerFactory, + UserFileManager userFileManager) { super(context, iconController, defaultFactory, mainExecutor, pluginManager, tunerService, autoTiles, dumpManager, Optional.of(centralSurfaces), qsLogger, uiEventLogger, userTracker, secureSettings, customTileStatePersister, - tileServiceRequestControllerBuilder, tileLifecycleManagerFactory); + tileServiceRequestControllerBuilder, tileLifecycleManagerFactory, + userFileManager); } @Override diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceManagerTest.java index 573980d015a9..8aa625a7ea20 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceManagerTest.java @@ -19,6 +19,14 @@ import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -31,6 +39,7 @@ import android.test.suitebuilder.annotation.SmallTest; import androidx.test.runner.AndroidJUnit4; import com.android.systemui.SysuiTestCase; +import com.android.systemui.qs.QSTileHost; import com.android.systemui.settings.UserTracker; import org.junit.After; @@ -38,37 +47,45 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; -import org.mockito.Mockito; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; @SmallTest @RunWith(AndroidJUnit4.class) public class TileServiceManagerTest extends SysuiTestCase { + @Mock private TileServices mTileServices; + @Mock private TileLifecycleManager mTileLifecycle; + @Mock + private UserTracker mUserTracker; + @Mock + private QSTileHost mQSTileHost; + @Mock + private Context mMockContext; + private HandlerThread mThread; private Handler mHandler; private TileServiceManager mTileServiceManager; - private UserTracker mUserTracker; - private Context mMockContext; + private ComponentName mComponentName; @Before public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); mThread = new HandlerThread("TestThread"); mThread.start(); mHandler = Handler.createAsync(mThread.getLooper()); - mTileServices = Mockito.mock(TileServices.class); - mUserTracker = Mockito.mock(UserTracker.class); - Mockito.when(mUserTracker.getUserId()).thenReturn(UserHandle.USER_SYSTEM); - Mockito.when(mUserTracker.getUserHandle()).thenReturn(UserHandle.SYSTEM); - - mMockContext = Mockito.mock(Context.class); - Mockito.when(mTileServices.getContext()).thenReturn(mMockContext); - mTileLifecycle = Mockito.mock(TileLifecycleManager.class); - Mockito.when(mTileLifecycle.isActiveTile()).thenReturn(false); - ComponentName componentName = new ComponentName(mContext, - TileServiceManagerTest.class); - Mockito.when(mTileLifecycle.getComponent()).thenReturn(componentName); + when(mUserTracker.getUserId()).thenReturn(UserHandle.USER_SYSTEM); + when(mUserTracker.getUserHandle()).thenReturn(UserHandle.SYSTEM); + + when(mTileServices.getContext()).thenReturn(mMockContext); + when(mTileServices.getHost()).thenReturn(mQSTileHost); + when(mTileLifecycle.getUserId()).thenAnswer(invocation -> mUserTracker.getUserId()); + when(mTileLifecycle.isActiveTile()).thenReturn(false); + + mComponentName = new ComponentName(mContext, TileServiceManagerTest.class); + when(mTileLifecycle.getComponent()).thenReturn(mComponentName); mTileServiceManager = new TileServiceManager(mTileServices, mHandler, mUserTracker, mTileLifecycle); } @@ -80,17 +97,44 @@ public class TileServiceManagerTest extends SysuiTestCase { } @Test + public void testSetTileAddedIfNotAdded() { + when(mQSTileHost.isTileAdded(eq(mComponentName), anyInt())).thenReturn(false); + mTileServiceManager.startLifecycleManagerAndAddTile(); + + verify(mQSTileHost).setTileAdded(mComponentName, mUserTracker.getUserId(), true); + } + + @Test + public void testNotSetTileAddedIfAdded() { + when(mQSTileHost.isTileAdded(eq(mComponentName), anyInt())).thenReturn(true); + mTileServiceManager.startLifecycleManagerAndAddTile(); + + verify(mQSTileHost, never()).setTileAdded(eq(mComponentName), anyInt(), eq(true)); + } + + @Test + public void testSetTileAddedCorrectUser() { + int user = 10; + when(mUserTracker.getUserId()).thenReturn(user); + when(mQSTileHost.isTileAdded(eq(mComponentName), anyInt())).thenReturn(false); + mTileServiceManager.startLifecycleManagerAndAddTile(); + + verify(mQSTileHost).setTileAdded(mComponentName, user, true); + } + + @Test public void testUninstallReceiverExported() { + mTileServiceManager.startLifecycleManagerAndAddTile(); ArgumentCaptor<IntentFilter> intentFilterCaptor = ArgumentCaptor.forClass(IntentFilter.class); - Mockito.verify(mMockContext).registerReceiverAsUser( - Mockito.any(), - Mockito.any(), + verify(mMockContext).registerReceiverAsUser( + any(), + any(), intentFilterCaptor.capture(), - Mockito.any(), - Mockito.any(), - Mockito.eq(Context.RECEIVER_EXPORTED) + any(), + any(), + eq(Context.RECEIVER_EXPORTED) ); IntentFilter filter = intentFilterCaptor.getValue(); assertTrue(filter.hasAction(Intent.ACTION_PACKAGE_REMOVED)); @@ -99,38 +143,41 @@ public class TileServiceManagerTest extends SysuiTestCase { @Test public void testSetBindRequested() { + mTileServiceManager.startLifecycleManagerAndAddTile(); // Request binding. mTileServiceManager.setBindRequested(true); mTileServiceManager.setLastUpdate(0); mTileServiceManager.calculateBindPriority(5); - Mockito.verify(mTileServices, Mockito.times(2)).recalculateBindAllowance(); + verify(mTileServices, times(2)).recalculateBindAllowance(); assertEquals(5, mTileServiceManager.getBindPriority()); // Verify same state doesn't trigger recalculating for no reason. mTileServiceManager.setBindRequested(true); - Mockito.verify(mTileServices, Mockito.times(2)).recalculateBindAllowance(); + verify(mTileServices, times(2)).recalculateBindAllowance(); mTileServiceManager.setBindRequested(false); mTileServiceManager.calculateBindPriority(5); - Mockito.verify(mTileServices, Mockito.times(3)).recalculateBindAllowance(); + verify(mTileServices, times(3)).recalculateBindAllowance(); assertEquals(Integer.MIN_VALUE, mTileServiceManager.getBindPriority()); } @Test public void testPendingClickPriority() { - Mockito.when(mTileLifecycle.hasPendingClick()).thenReturn(true); + mTileServiceManager.startLifecycleManagerAndAddTile(); + when(mTileLifecycle.hasPendingClick()).thenReturn(true); mTileServiceManager.calculateBindPriority(0); assertEquals(Integer.MAX_VALUE, mTileServiceManager.getBindPriority()); } @Test public void testBind() { + mTileServiceManager.startLifecycleManagerAndAddTile(); // Trigger binding requested and allowed. mTileServiceManager.setBindRequested(true); mTileServiceManager.setBindAllowed(true); ArgumentCaptor<Boolean> captor = ArgumentCaptor.forClass(Boolean.class); - Mockito.verify(mTileLifecycle, Mockito.times(1)).setBindService(captor.capture()); + verify(mTileLifecycle, times(1)).setBindService(captor.capture()); assertTrue((boolean) captor.getValue()); mTileServiceManager.setBindRequested(false); @@ -141,7 +188,7 @@ public class TileServiceManagerTest extends SysuiTestCase { mTileServiceManager.setBindAllowed(false); captor = ArgumentCaptor.forClass(Boolean.class); - Mockito.verify(mTileLifecycle, Mockito.times(2)).setBindService(captor.capture()); + verify(mTileLifecycle, times(2)).setBindService(captor.capture()); assertFalse((boolean) captor.getValue()); } } 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 471ddfd3f224..213eca895eb9 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 @@ -45,6 +45,7 @@ import com.android.systemui.dump.DumpManager; import com.android.systemui.qs.QSTileHost; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.qs.tileimpl.QSFactoryImpl; +import com.android.systemui.settings.UserFileManager; import com.android.systemui.settings.UserTracker; import com.android.systemui.shared.plugins.PluginManager; import com.android.systemui.statusbar.CommandQueue; @@ -118,6 +119,8 @@ public class TileServicesTest extends SysuiTestCase { private TileLifecycleManager.Factory mTileLifecycleManagerFactory; @Mock private TileLifecycleManager mTileLifecycleManager; + @Mock + private UserFileManager mUserFileManager; @Before public void setUp() throws Exception { @@ -149,7 +152,8 @@ public class TileServicesTest extends SysuiTestCase { mSecureSettings, mock(CustomTileStatePersister.class), mTileServiceRequestControllerBuilder, - mTileLifecycleManagerFactory); + mTileLifecycleManagerFactory, + mUserFileManager); mTileService = new TestTileServices(host, provider, mBroadcastDispatcher, mUserTracker, mKeyguardStateController, mCommandQueue); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/FakeSharedPreferencesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/FakeSharedPreferencesTest.kt new file mode 100644 index 000000000000..d886ffdf0e58 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/util/FakeSharedPreferencesTest.kt @@ -0,0 +1,259 @@ +/* + * Copyright (C) 2022 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.util + +import android.content.SharedPreferences +import androidx.test.filters.SmallTest +import androidx.test.runner.AndroidJUnit4 +import com.android.systemui.SysuiTestCase +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.Mock +import org.mockito.Mockito.anyString +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyNoMoreInteractions +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidJUnit4::class) +class FakeSharedPreferencesTest : SysuiTestCase() { + + @Mock + private lateinit var listener: SharedPreferences.OnSharedPreferenceChangeListener + + private lateinit var sharedPreferences: SharedPreferences + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + sharedPreferences = FakeSharedPreferences() + } + + @Test + fun testGetString_default() { + val default = "default" + val result = sharedPreferences.getString("key", default) + assertThat(result).isEqualTo(default) + } + + @Test + fun testGetStringSet_default() { + val default = setOf("one", "two") + val result = sharedPreferences.getStringSet("key", default) + assertThat(result).isEqualTo(default) + } + + @Test + fun testGetInt_default() { + val default = 10 + val result = sharedPreferences.getInt("key", default) + assertThat(result).isEqualTo(default) + } + + @Test + fun testGetLong_default() { + val default = 11L + val result = sharedPreferences.getLong("key", default) + assertThat(result).isEqualTo(default) + } + + @Test + fun testGetFloat_default() { + val default = 1.3f + val result = sharedPreferences.getFloat("key", default) + assertThat(result).isEqualTo(default) + } + + @Test + fun testGetBoolean_default() { + val default = true + val result = sharedPreferences.getBoolean("key", default) + assertThat(result).isEqualTo(default) + } + + @Test + fun testPutValuesAndRetrieve() { + val editor = sharedPreferences.edit() + val data = listOf<Data<*>>( + Data( + "keyString", + "value", + SharedPreferences.Editor::putString, + { getString(it, "") } + ), + Data( + "keyStringSet", + setOf("one", "two"), + SharedPreferences.Editor::putStringSet, + { getStringSet(it, emptySet()) } + ), + Data("keyInt", 10, SharedPreferences.Editor::putInt, { getInt(it, 0) }), + Data("keyLong", 11L, SharedPreferences.Editor::putLong, { getLong(it, 0L) }), + Data( + "keyFloat", + 1.3f, + SharedPreferences.Editor::putFloat, + { getFloat(it, 0f) } + ), + Data( + "keyBoolean", + true, + SharedPreferences.Editor::putBoolean, + { getBoolean(it, false) } + ) + ) + + data.fold(editor) { ed, d -> + d.set(ed) + } + editor.commit() + + data.forEach { + assertThat(it.get(sharedPreferences)).isEqualTo(it.value) + } + } + + @Test + fun testContains() { + sharedPreferences.edit().putInt("key", 10).commit() + + assertThat(sharedPreferences.contains("key")).isTrue() + assertThat(sharedPreferences.contains("other")).isFalse() + } + + @Test + fun testOverwrite() { + sharedPreferences.edit().putInt("key", 10).commit() + sharedPreferences.edit().putInt("key", 11).commit() + + assertThat(sharedPreferences.getInt("key", 0)).isEqualTo(11) + } + + @Test + fun testDeleteString() { + sharedPreferences.edit().putString("key", "value").commit() + sharedPreferences.edit().putString("key", null).commit() + + assertThat(sharedPreferences.contains("key")).isFalse() + } + + @Test + fun testDeleteAndReplaceString() { + sharedPreferences.edit().putString("key", "value").commit() + sharedPreferences.edit().putString("key", "other").putString("key", null).commit() + + assertThat(sharedPreferences.getString("key", "")).isEqualTo("other") + } + + @Test + fun testDeleteStringSet() { + sharedPreferences.edit().putStringSet("key", setOf("one")).commit() + sharedPreferences.edit().putStringSet("key", setOf("two")).commit() + + assertThat(sharedPreferences.getStringSet("key", emptySet())).isEqualTo(setOf("two")) + } + + @Test + fun testClear() { + sharedPreferences.edit().putInt("keyInt", 1).putString("keyString", "a").commit() + sharedPreferences.edit().clear().commit() + + assertThat(sharedPreferences.contains("keyInt")).isFalse() + assertThat(sharedPreferences.contains("keyString")).isFalse() + } + + @Test + fun testClearAndWrite() { + sharedPreferences.edit().putInt("key", 10).commit() + sharedPreferences.edit().putInt("key", 11).clear().commit() + + assertThat(sharedPreferences.getInt("key", 0)).isEqualTo(11) + } + + @Test + fun testListenerNotifiedOnChanges() { + sharedPreferences.registerOnSharedPreferenceChangeListener(listener) + + sharedPreferences.edit().putInt("keyInt", 10).putString("keyString", "value").commit() + + verify(listener).onSharedPreferenceChanged(sharedPreferences, "keyInt") + verify(listener).onSharedPreferenceChanged(sharedPreferences, "keyString") + verifyNoMoreInteractions(listener) + } + + @Test + fun testListenerNotifiedOnClear() { + sharedPreferences.edit().putInt("keyInt", 10).commit() + sharedPreferences.registerOnSharedPreferenceChangeListener(listener) + + sharedPreferences.edit().clear().commit() + + verify(listener).onSharedPreferenceChanged(sharedPreferences, null) + verifyNoMoreInteractions(listener) + } + + @Test + fun testListenerNotifiedOnRemoval() { + sharedPreferences.edit() + .putString("keyString", "a") + .putStringSet("keySet", setOf("a")) + .commit() + + sharedPreferences.registerOnSharedPreferenceChangeListener(listener) + sharedPreferences.edit().putString("keyString", null).putStringSet("keySet", null).commit() + + verify(listener).onSharedPreferenceChanged(sharedPreferences, "keyString") + verify(listener).onSharedPreferenceChanged(sharedPreferences, "keySet") + verifyNoMoreInteractions(listener) + } + + @Test + fun testListenerUnregistered() { + sharedPreferences.registerOnSharedPreferenceChangeListener(listener) + sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener) + sharedPreferences.edit().putInt("key", 10).commit() + + verify(listener, never()).onSharedPreferenceChanged(eq(sharedPreferences), anyString()) + } + + @Test + fun testSharedPreferencesOnlyModifiedOnCommit() { + sharedPreferences.edit().putInt("key", 10) + + assertThat(sharedPreferences.contains("key")).isFalse() + } + + private data class Data<T>( + val key: String, + val value: T, + private val setter: SharedPreferences.Editor.(String, T) -> SharedPreferences.Editor, + private val getter: SharedPreferences.(String) -> T + ) { + fun set(editor: SharedPreferences.Editor): SharedPreferences.Editor { + return editor.setter(key, value) + } + + fun get(sharedPreferences: SharedPreferences): T { + return sharedPreferences.getter(key) + } + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeSharedPreferences.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeSharedPreferences.kt new file mode 100644 index 000000000000..4a881a7ce898 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeSharedPreferences.kt @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2022 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.util + +import android.content.SharedPreferences + +/** + * Fake [SharedPreferences] to use within tests + * + * This will act in the same way as a real one for a particular file, but will store all the + * data in memory in the instance. + * + * [SharedPreferences.Editor.apply] and [SharedPreferences.Editor.commit] both act in the same way, + * synchronously modifying the stored data. Listeners are dispatched in the same thread, also + * synchronously. + */ +class FakeSharedPreferences : SharedPreferences { + private val data = mutableMapOf<String, Any>() + private val listeners = mutableSetOf<SharedPreferences.OnSharedPreferenceChangeListener>() + + override fun getAll(): Map<String, *> { + return data + } + + override fun getString(key: String, defValue: String?): String? { + return data.getOrDefault(key, defValue) as? String? + } + + override fun getStringSet(key: String, defValues: MutableSet<String>?): MutableSet<String>? { + return data.getOrDefault(key, defValues) as? MutableSet<String>? + } + + override fun getInt(key: String, defValue: Int): Int { + return data.getOrDefault(key, defValue) as Int + } + + override fun getLong(key: String, defValue: Long): Long { + return data.getOrDefault(key, defValue) as Long + } + + override fun getFloat(key: String, defValue: Float): Float { + return data.getOrDefault(key, defValue) as Float + } + + override fun getBoolean(key: String, defValue: Boolean): Boolean { + return data.getOrDefault(key, defValue) as Boolean + } + + override fun contains(key: String): Boolean { + return key in data + } + + override fun edit(): SharedPreferences.Editor { + return Editor() + } + + override fun registerOnSharedPreferenceChangeListener( + listener: SharedPreferences.OnSharedPreferenceChangeListener + ) { + listeners.add(listener) + } + + override fun unregisterOnSharedPreferenceChangeListener( + listener: SharedPreferences.OnSharedPreferenceChangeListener + ) { + listeners.remove(listener) + } + + private inner class Editor : SharedPreferences.Editor { + + private var clear = false + private val changes = mutableMapOf<String, Any>() + private val removals = mutableSetOf<String>() + + override fun putString(key: String, value: String?): SharedPreferences.Editor { + if (value != null) { + changes[key] = value + } else { + removals.add(key) + } + return this + } + + override fun putStringSet( + key: String, + values: MutableSet<String>? + ): SharedPreferences.Editor { + if (values != null) { + changes[key] = values + } else { + removals.add(key) + } + return this + } + + override fun putInt(key: String, value: Int): SharedPreferences.Editor { + changes[key] = value + return this + } + + override fun putLong(key: String, value: Long): SharedPreferences.Editor { + changes[key] = value + return this + } + + override fun putFloat(key: String, value: Float): SharedPreferences.Editor { + changes[key] = value + return this + } + + override fun putBoolean(key: String, value: Boolean): SharedPreferences.Editor { + changes[key] = value + return this + } + + override fun remove(key: String): SharedPreferences.Editor { + removals.add(key) + return this + } + + override fun clear(): SharedPreferences.Editor { + clear = true + return this + } + + override fun commit(): Boolean { + if (clear) { + data.clear() + } + removals.forEach { data.remove(it) } + data.putAll(changes) + val keys = removals + data.keys + if (clear || removals.isNotEmpty() || data.isNotEmpty()) { + listeners.forEach { listener -> + if (clear) { + listener.onSharedPreferenceChanged(this@FakeSharedPreferences, null) + } + keys.forEach { + listener.onSharedPreferenceChanged(this@FakeSharedPreferences, it) + } + } + } + return true + } + + override fun apply() { + commit() + } + } +} |