diff options
15 files changed, 661 insertions, 47 deletions
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index e69ac0a555b6..626e219fca24 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -882,3 +882,13 @@ flag { description: "Enables Backlinks improvement feature in App Clips" bug: "300307759" } + +flag { + name: "qs_custom_tile_click_guaranteed_bug_fix" + namespace: "systemui" + description: "Guarantee that clicks on a tile always happen by postponing onStopListening until after the click." + bug: "339290820" + metadata { + purpose: PURPOSE_BUGFIX + } +}
\ No newline at end of file diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/external/CloseShadeRightAfterClickTestB339290820.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/external/CloseShadeRightAfterClickTestB339290820.kt new file mode 100644 index 000000000000..8d1aa73aa55c --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/external/CloseShadeRightAfterClickTestB339290820.kt @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2024 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.app.PendingIntent +import android.content.ComponentName +import android.content.Context +import android.content.ContextWrapper +import android.content.Intent +import android.content.ServiceConnection +import android.content.applicationContext +import android.content.packageManager +import android.os.Binder +import android.os.Handler +import android.os.RemoteException +import android.os.UserHandle +import android.platform.test.annotations.EnableFlags +import android.service.quicksettings.Tile +import android.testing.TestableContext +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.Flags.FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX +import com.android.systemui.SysuiTestCase +import com.android.systemui.concurrency.fakeExecutor +import com.android.systemui.kosmos.testCase +import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.tiles.impl.custom.packageManagerAdapterFacade +import com.android.systemui.qs.tiles.impl.custom.customTileSpec +import com.android.systemui.testKosmos +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.time.fakeSystemClock +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyString + +@RunWith(AndroidJUnit4::class) +@SmallTest +class CloseShadeRightAfterClickTestB339290820 : SysuiTestCase() { + + private val testableContext: TestableContext + private val bindDelayExecutor: FakeExecutor + private val kosmos = + testKosmos().apply { + testableContext = testCase.context + bindDelayExecutor = FakeExecutor(fakeSystemClock) + testableContext.setMockPackageManager(packageManager) + customTileSpec = TileSpec.create(testComponentName) + applicationContext = ContextWrapperDelayedBind(testableContext, bindDelayExecutor) + } + + @Before + fun setUp() { + kosmos.apply { + whenever(packageManager.getPackageUidAsUser(anyString(), anyInt(), anyInt())) + .thenReturn(Binder.getCallingUid()) + packageManagerAdapterFacade.setIsActive(true) + testableContext.addMockService(testComponentName, iQSTileService.asBinder()) + } + } + + @Test + @EnableFlags(FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX) + fun testStopListeningShortlyAfterClick_clickIsSent() { + with(kosmos) { + val tile = FakeCustomTileInterface(tileServices) + // Flush any bind from startup + FakeExecutor.exhaustExecutors(fakeExecutor, bindDelayExecutor) + + // Open QS + tile.setListening(true) + fakeExecutor.runAllReady() + tile.click() + fakeExecutor.runAllReady() + + // No clicks yet because the latch is preventing the bind + assertThat(iQSTileService.clicks).isEmpty() + + // Close QS + tile.setListening(false) + fakeExecutor.runAllReady() + // And finally bind + FakeExecutor.exhaustExecutors(fakeExecutor, bindDelayExecutor) + + assertThat(iQSTileService.clicks).containsExactly(tile.token) + } + } +} + +private val testComponentName = ComponentName("pkg", "srv") + +// This is a fake `CustomTile` that implements what we need for the test. Mainly setListening and +// click +private class FakeCustomTileInterface(tileServices: TileServices) : CustomTileInterface { + override val user: Int + get() = 0 + override val qsTile: Tile = Tile() + override val component: ComponentName = testComponentName + private var listening = false + private val serviceManager = tileServices.getTileWrapper(this) + private val serviceInterface = serviceManager.tileService + + val token = Binder() + + override fun getTileSpec(): String { + return CustomTile.toSpec(component) + } + + override fun refreshState() {} + + override fun updateTileState(tile: Tile, uid: Int) {} + + override fun onDialogShown() {} + + override fun onDialogHidden() {} + + override fun startActivityAndCollapse(pendingIntent: PendingIntent) {} + + override fun startUnlockAndRun() {} + + fun setListening(listening: Boolean) { + if (listening == this.listening) return + this.listening = listening + + try { + if (listening) { + if (!serviceManager.isActiveTile) { + serviceManager.setBindRequested(true) + serviceInterface.onStartListening() + } + } else { + serviceInterface.onStopListening() + serviceManager.setBindRequested(false) + } + } catch (e: RemoteException) { + // Called through wrapper, won't happen here. + } + } + + fun click() { + try { + if (serviceManager.isActiveTile) { + serviceManager.setBindRequested(true) + serviceInterface.onStartListening() + } + serviceInterface.onClick(token) + } catch (e: RemoteException) { + // Called through wrapper, won't happen here. + } + } +} + +private class ContextWrapperDelayedBind( + val context: Context, + val executor: FakeExecutor, +) : ContextWrapper(context) { + override fun bindServiceAsUser( + service: Intent, + conn: ServiceConnection, + flags: Int, + user: UserHandle + ): Boolean { + executor.execute { super.bindServiceAsUser(service, conn, flags, user) } + return true + } + + override fun bindServiceAsUser( + service: Intent, + conn: ServiceConnection, + flags: BindServiceFlags, + user: UserHandle + ): Boolean { + executor.execute { super.bindServiceAsUser(service, conn, flags, user) } + return true + } + + override fun bindServiceAsUser( + service: Intent?, + conn: ServiceConnection?, + flags: Int, + handler: Handler?, + user: UserHandle? + ): Boolean { + executor.execute { super.bindServiceAsUser(service, conn, flags, handler, user) } + return true + } + + override fun bindServiceAsUser( + service: Intent, + conn: ServiceConnection, + flags: BindServiceFlags, + handler: Handler, + user: UserHandle + ): Boolean { + executor.execute { super.bindServiceAsUser(service, conn, flags, handler, user) } + return true + } +} 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 2a726c2835d9..24b7a011f093 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java +++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java @@ -19,6 +19,8 @@ import static android.os.PowerWhitelistManager.REASON_TILE_ONCLICK; import static android.provider.DeviceConfig.NAMESPACE_SYSTEMUI; import static android.service.quicksettings.TileService.START_ACTIVITY_NEEDS_PENDING_INTENT; +import static com.android.systemui.Flags.qsCustomTileClickGuaranteedBugFix; + import android.app.ActivityManager; import android.app.compat.CompatChanges; import android.content.BroadcastReceiver; @@ -88,6 +90,7 @@ public class TileLifecycleManager extends BroadcastReceiver implements private static final int MSG_ON_REMOVED = 1; private static final int MSG_ON_CLICK = 2; private static final int MSG_ON_UNLOCK_COMPLETE = 3; + private static final int MSG_ON_STOP_LISTENING = 4; // Bind retry control. private static final int MAX_BIND_RETRIES = 5; @@ -368,6 +371,16 @@ public class TileLifecycleManager extends BroadcastReceiver implements onUnlockComplete(); } } + if (qsCustomTileClickGuaranteedBugFix()) { + if (queue.contains(MSG_ON_STOP_LISTENING)) { + if (mDebug) Log.d(TAG, "Handling pending onStopListening " + getComponent()); + if (mListening) { + onStopListening(); + } else { + Log.w(TAG, "Trying to stop listening when not listening " + getComponent()); + } + } + } if (queue.contains(MSG_ON_REMOVED)) { if (mDebug) Log.d(TAG, "Handling pending onRemoved " + getComponent()); if (mListening) { @@ -586,10 +599,15 @@ public class TileLifecycleManager extends BroadcastReceiver implements @Override public void onStopListening() { - if (mDebug) Log.d(TAG, "onStopListening " + getComponent()); - mListening = false; - if (isNotNullAndFailedAction(mOptionalWrapper, QSTileServiceWrapper::onStopListening)) { - handleDeath(); + if (qsCustomTileClickGuaranteedBugFix() && hasPendingClick()) { + Log.d(TAG, "Enqueue stop listening"); + queueMessage(MSG_ON_STOP_LISTENING); + } else { + if (mDebug) Log.d(TAG, "onStopListening " + getComponent()); + mListening = false; + if (isNotNullAndFailedAction(mOptionalWrapper, QSTileServiceWrapper::onStopListening)) { + handleDeath(); + } } } 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 f8bf0a684506..6bc5095ed1ea 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java +++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java @@ -15,6 +15,8 @@ */ package com.android.systemui.qs.external; +import static com.android.systemui.Flags.qsCustomTileClickGuaranteedBugFix; + import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; @@ -37,6 +39,7 @@ import com.android.systemui.settings.UserTracker; import java.util.List; import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; /** * Manages the priority which lets {@link TileServices} make decisions about which tiles @@ -72,6 +75,8 @@ public class TileServiceManager { private boolean mPendingBind = true; private boolean mStarted = false; + private final AtomicBoolean mListeningFromRequest = new AtomicBoolean(false); + TileServiceManager(TileServices tileServices, Handler handler, ComponentName component, UserTracker userTracker, TileLifecycleManager.Factory tileLifecycleManagerFactory, CustomTileAddedRepository customTileAddedRepository) { @@ -159,15 +164,30 @@ public class TileServiceManager { } } + void onStartListeningFromRequest() { + mListeningFromRequest.set(true); + mStateManager.onStartListening(); + } + public void setLastUpdate(long lastUpdate) { mLastUpdate = lastUpdate; if (mBound && isActiveTile()) { - mStateManager.onStopListening(); - setBindRequested(false); + if (qsCustomTileClickGuaranteedBugFix()) { + if (mListeningFromRequest.compareAndSet(true, false)) { + stopListeningAndUnbind(); + } + } else { + stopListeningAndUnbind(); + } } mServices.recalculateBindAllowance(); } + private void stopListeningAndUnbind() { + mStateManager.onStopListening(); + setBindRequested(false); + } + public void handleDestroy() { setBindAllowed(false); mServices.getContext().unregisterReceiver(mUninstallReceiver); 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 8278c790226b..d457e88fcf14 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java +++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java @@ -15,6 +15,8 @@ */ package com.android.systemui.qs.external; +import static com.android.systemui.Flags.qsCustomTileClickGuaranteedBugFix; + import android.app.PendingIntent; import android.content.ComponentName; import android.content.Context; @@ -222,9 +224,13 @@ public class TileServices extends IQSService.Stub { return; } service.setBindRequested(true); - try { - service.getTileService().onStartListening(); - } catch (RemoteException e) { + if (qsCustomTileClickGuaranteedBugFix()) { + service.onStartListeningFromRequest(); + } else { + try { + service.getTileService().onStartListening(); + } catch (RemoteException e) { + } } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java index f57f04000be9..68307b1b905e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java @@ -16,10 +16,12 @@ package com.android.systemui.qs.external; import static android.os.PowerExemptionManager.REASON_TILE_ONCLICK; +import static android.platform.test.flag.junit.FlagsParameterization.allCombinationsOf; import static android.service.quicksettings.TileService.START_ACTIVITY_NEEDS_PENDING_INTENT; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; +import static com.android.systemui.Flags.FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; @@ -55,13 +57,15 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.IDeviceIdleController; import android.os.UserHandle; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.FlagsParameterization; import android.service.quicksettings.IQSService; import android.service.quicksettings.IQSTileService; import android.service.quicksettings.TileService; import androidx.annotation.Nullable; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import com.android.systemui.SysuiTestCase; import com.android.systemui.broadcast.BroadcastDispatcher; @@ -73,12 +77,24 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; +import org.mockito.Mockito; import org.mockito.MockitoSession; +import java.util.List; + +import platform.test.runner.parameterized.ParameterizedAndroidJunit4; +import platform.test.runner.parameterized.Parameters; + @SmallTest -@RunWith(AndroidJUnit4.class) +@RunWith(ParameterizedAndroidJunit4.class) public class TileLifecycleManagerTest extends SysuiTestCase { + @Parameters(name = "{0}") + public static List<FlagsParameterization> getParams() { + return allCombinationsOf(FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX); + } + private final PackageManagerAdapter mMockPackageManagerAdapter = mock(PackageManagerAdapter.class); private final BroadcastDispatcher mMockBroadcastDispatcher = @@ -98,6 +114,11 @@ public class TileLifecycleManagerTest extends SysuiTestCase { private TestContextWrapper mWrappedContext; private MockitoSession mMockitoSession; + public TileLifecycleManagerTest(FlagsParameterization flags) { + super(); + mSetFlagsRule.setFlagsParameterization(flags); + } + @Before public void setUp() throws Exception { setPackageEnabled(true); @@ -263,7 +284,8 @@ public class TileLifecycleManagerTest extends SysuiTestCase { } @Test - public void testNoClickOfNotListeningAnymore() throws Exception { + @DisableFlags(FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX) + public void testNoClickIfNotListeningAnymore() throws Exception { mStateManager.onTileAdded(); mStateManager.onStartListening(); mStateManager.onClick(null); @@ -279,6 +301,42 @@ public class TileLifecycleManagerTest extends SysuiTestCase { } @Test + @EnableFlags(FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX) + public void testNoClickIfNotListeningBeforeClick() throws Exception { + mStateManager.onTileAdded(); + mStateManager.onStartListening(); + mStateManager.onStopListening(); + mStateManager.onClick(null); + mStateManager.executeSetBindService(true); + mExecutor.runAllReady(); + + verifyBind(1); + mStateManager.executeSetBindService(false); + mExecutor.runAllReady(); + assertFalse(mContext.isBound(mTileServiceComponentName)); + verify(mMockTileService, never()).onClick(null); + } + + @Test + @EnableFlags(FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX) + public void testClickIfStopListeningBeforeProcessedClick() throws Exception { + mStateManager.onTileAdded(); + mStateManager.onStartListening(); + mStateManager.onClick(null); + mStateManager.onStopListening(); + mStateManager.executeSetBindService(true); + mExecutor.runAllReady(); + + verifyBind(1); + mStateManager.executeSetBindService(false); + mExecutor.runAllReady(); + assertFalse(mContext.isBound(mTileServiceComponentName)); + InOrder inOrder = Mockito.inOrder(mMockTileService); + inOrder.verify(mMockTileService).onClick(null); + inOrder.verify(mMockTileService).onStopListening(); + } + + @Test public void testComponentEnabling() throws Exception { mStateManager.onTileAdded(); mStateManager.onStartListening(); 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 0ff29dbbfde7..1c86638c9f27 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 @@ -15,12 +15,18 @@ */ package com.android.systemui.qs.external; +import static android.platform.test.flag.junit.FlagsParameterization.allCombinationsOf; + +import static com.android.systemui.Flags.FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX; +import static com.android.systemui.util.concurrency.MockExecutorHandlerKt.mockExecutorHandler; + 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.clearInvocations; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; @@ -32,16 +38,19 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.Handler; -import android.os.HandlerThread; import android.os.UserHandle; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.FlagsParameterization; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import com.android.systemui.SysuiTestCase; import com.android.systemui.qs.QSHost; import com.android.systemui.qs.pipeline.data.repository.CustomTileAddedRepository; import com.android.systemui.settings.UserTracker; +import com.android.systemui.util.concurrency.FakeExecutor; +import com.android.systemui.util.time.FakeSystemClock; import org.junit.After; import org.junit.Before; @@ -51,10 +60,20 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.List; + +import platform.test.runner.parameterized.ParameterizedAndroidJunit4; +import platform.test.runner.parameterized.Parameters; + @SmallTest -@RunWith(AndroidJUnit4.class) +@RunWith(ParameterizedAndroidJunit4.class) public class TileServiceManagerTest extends SysuiTestCase { + @Parameters(name = "{0}") + public static List<FlagsParameterization> getParams() { + return allCombinationsOf(FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX); + } + @Mock private TileServices mTileServices; @Mock @@ -68,17 +87,22 @@ public class TileServiceManagerTest extends SysuiTestCase { @Mock private CustomTileAddedRepository mCustomTileAddedRepository; - private HandlerThread mThread; - private Handler mHandler; + private FakeExecutor mFakeExecutor; + private TileServiceManager mTileServiceManager; private ComponentName mComponentName; + public TileServiceManagerTest(FlagsParameterization flags) { + super(); + mSetFlagsRule.setFlagsParameterization(flags); + } + @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); - mThread = new HandlerThread("TestThread"); - mThread.start(); - mHandler = Handler.createAsync(mThread.getLooper()); + mFakeExecutor = new FakeExecutor(new FakeSystemClock()); + Handler handler = mockExecutorHandler(mFakeExecutor); + when(mUserTracker.getUserId()).thenReturn(UserHandle.USER_SYSTEM); when(mUserTracker.getUserHandle()).thenReturn(UserHandle.SYSTEM); @@ -90,13 +114,12 @@ public class TileServiceManagerTest extends SysuiTestCase { mComponentName = new ComponentName(mContext, TileServiceManagerTest.class); when(mTileLifecycle.getComponent()).thenReturn(mComponentName); - mTileServiceManager = new TileServiceManager(mTileServices, mHandler, mUserTracker, + mTileServiceManager = new TileServiceManager(mTileServices, handler, mUserTracker, mCustomTileAddedRepository, mTileLifecycle); } @After public void tearDown() throws Exception { - mThread.quit(); mTileServiceManager.handleDestroy(); } @@ -201,4 +224,59 @@ public class TileServiceManagerTest extends SysuiTestCase { verify(mTileLifecycle, times(2)).executeSetBindService(captor.capture()); assertFalse((boolean) captor.getValue()); } + + @Test + @DisableFlags(FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX) + public void testStopListeningAndUnbindImmediatelyAfterUpdate() { + when(mTileLifecycle.isActiveTile()).thenReturn(true); + mTileServiceManager.startLifecycleManagerAndAddTile(); + mTileServiceManager.setBindAllowed(true); + clearInvocations(mTileLifecycle); + + mTileServiceManager.setBindRequested(true); + verify(mTileLifecycle).executeSetBindService(true); + + mTileServiceManager.setLastUpdate(0); + mFakeExecutor.advanceClockToLast(); + mFakeExecutor.runAllReady(); + verify(mTileLifecycle).onStopListening(); + verify(mTileLifecycle).executeSetBindService(false); + } + + @Test + @EnableFlags(FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX) + public void testStopListeningAndUnbindImmediatelyAfterUpdate_ifRequestedFromTileService() { + when(mTileLifecycle.isActiveTile()).thenReturn(true); + mTileServiceManager.startLifecycleManagerAndAddTile(); + mTileServiceManager.setBindAllowed(true); + clearInvocations(mTileLifecycle); + + mTileServiceManager.setBindRequested(true); + mTileServiceManager.onStartListeningFromRequest(); + verify(mTileLifecycle).onStartListening(); + + mTileServiceManager.setLastUpdate(0); + mFakeExecutor.advanceClockToLast(); + mFakeExecutor.runAllReady(); + verify(mTileLifecycle).onStopListening(); + verify(mTileLifecycle).executeSetBindService(false); + } + + @Test + @EnableFlags(FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX) + public void testNotUnbindImmediatelyAfterUpdate_ifRequestedFromSystemUI() { + when(mTileLifecycle.isActiveTile()).thenReturn(true); + mTileServiceManager.startLifecycleManagerAndAddTile(); + mTileServiceManager.setBindAllowed(true); + clearInvocations(mTileLifecycle); + + mTileServiceManager.setBindRequested(true); + // The tile requests startListening (because a click happened) + + mTileServiceManager.setLastUpdate(0); + mFakeExecutor.advanceClockToLast(); + mFakeExecutor.runAllReady(); + verify(mTileLifecycle, never()).onStopListening(); + verify(mTileLifecycle, never()).executeSetBindService(false); + } } 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 b62d59d3a2f2..bcff88a49ad6 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 @@ -15,6 +15,10 @@ */ package com.android.systemui.qs.external; +import static android.platform.test.flag.junit.FlagsParameterization.allCombinationsOf; + +import static com.android.systemui.Flags.FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX; + import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertTrue; @@ -33,8 +37,10 @@ import android.os.Binder; import android.os.Handler; import android.os.RemoteException; import android.os.UserHandle; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.FlagsParameterization; import android.service.quicksettings.IQSTileService; -import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.testing.TestableLooper.RunWithLooper; @@ -64,13 +70,23 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.ArrayList; +import java.util.List; import javax.inject.Provider; +import platform.test.runner.parameterized.ParameterizedAndroidJunit4; +import platform.test.runner.parameterized.Parameters; + @SmallTest -@RunWith(AndroidTestingRunner.class) +@RunWith(ParameterizedAndroidJunit4.class) @RunWithLooper public class TileServicesTest extends SysuiTestCase { + + @Parameters(name = "{0}") + public static List<FlagsParameterization> getParams() { + return allCombinationsOf(FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX); + } + private static int NUM_FAKES = TileServices.DEFAULT_MAX_BOUND * 2; private static final ComponentName TEST_COMPONENT = @@ -106,6 +122,11 @@ public class TileServicesTest extends SysuiTestCase { @Mock private CustomTileAddedRepository mCustomTileAddedRepository; + public TileServicesTest(FlagsParameterization flags) { + super(); + mSetFlagsRule.setFlagsParameterization(flags); + } + @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); @@ -194,6 +215,7 @@ public class TileServicesTest extends SysuiTestCase { } @Test + @DisableFlags(FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX) public void testRequestListeningStatusCommand() throws RemoteException { ArgumentCaptor<CommandQueue.Callbacks> captor = ArgumentCaptor.forClass(CommandQueue.Callbacks.class); @@ -213,6 +235,26 @@ public class TileServicesTest extends SysuiTestCase { } @Test + @EnableFlags(FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX) + public void testRequestListeningStatusCommand_onStartListeningFromRequest() { + ArgumentCaptor<CommandQueue.Callbacks> captor = + ArgumentCaptor.forClass(CommandQueue.Callbacks.class); + verify(mCommandQueue).addCallback(captor.capture()); + + CustomTile mockTile = mock(CustomTile.class); + when(mockTile.getComponent()).thenReturn(TEST_COMPONENT); + + TileServiceManager manager = mTileService.getTileWrapper(mockTile); + when(manager.isActiveTile()).thenReturn(true); + when(manager.getTileService()).thenReturn(mock(IQSTileService.class)); + + captor.getValue().requestTileServiceListeningState(TEST_COMPONENT); + mTestableLooper.processAllMessages(); + verify(manager).setBindRequested(true); + verify(manager).onStartListeningFromRequest(); + } + + @Test public void testValidCustomTileStartsActivity() { CustomTile tile = mock(CustomTile.class); PendingIntent pi = mock(PendingIntent.class); @@ -263,6 +305,7 @@ public class TileServicesTest extends SysuiTestCase { } @Test + @DisableFlags(FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX) public void tileFreedForCorrectUser() throws RemoteException { verify(mCommandQueue).addCallback(mCallbacksArgumentCaptor.capture()); @@ -297,6 +340,42 @@ public class TileServicesTest extends SysuiTestCase { verify(manager1.getTileService()).onStartListening(); } + @Test + @EnableFlags(FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX) + public void tileFreedForCorrectUser_onStartListeningFromRequest() throws RemoteException { + verify(mCommandQueue).addCallback(mCallbacksArgumentCaptor.capture()); + + ComponentName componentName = new ComponentName("pkg", "cls"); + CustomTile tileUser0 = mock(CustomTile.class); + CustomTile tileUser1 = mock(CustomTile.class); + + when(tileUser0.getComponent()).thenReturn(componentName); + when(tileUser1.getComponent()).thenReturn(componentName); + when(tileUser0.getUser()).thenReturn(0); + when(tileUser1.getUser()).thenReturn(1); + + // Create a tile for user 0 + TileServiceManager manager0 = mTileService.getTileWrapper(tileUser0); + when(manager0.isActiveTile()).thenReturn(true); + // Then create a tile for user 1 + TileServiceManager manager1 = mTileService.getTileWrapper(tileUser1); + when(manager1.isActiveTile()).thenReturn(true); + + // When the tile for user 0 gets freed + mTileService.freeService(tileUser0, manager0); + // and the user is 1 + when(mUserTracker.getUserId()).thenReturn(1); + + // a call to requestListeningState + mCallbacksArgumentCaptor.getValue().requestTileServiceListeningState(componentName); + mTestableLooper.processAllMessages(); + + // will call in the correct tile + verify(manager1).setBindRequested(true); + // and set it to listening + verify(manager1).onStartListeningFromRequest(); + } + private class TestTileServices extends TileServices { TestTileServices(QSHost host, Provider<Handler> handlerProvider, BroadcastDispatcher broadcastDispatcher, UserTracker userTracker, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/FakeIQSTileService.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/FakeIQSTileService.kt index cff59807e00f..744942c0053b 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/FakeIQSTileService.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/FakeIQSTileService.kt @@ -16,11 +16,11 @@ package com.android.systemui.qs.external -import android.os.Binder import android.os.IBinder +import android.os.IInterface import android.service.quicksettings.IQSTileService -class FakeIQSTileService : IQSTileService { +class FakeIQSTileService : IQSTileService.Stub() { var isTileAdded: Boolean = false private set @@ -31,9 +31,11 @@ class FakeIQSTileService : IQSTileService { get() = mutableClicks private val mutableClicks: MutableList<IBinder?> = mutableListOf() - private val binder = Binder() + override fun queryLocalInterface(descriptor: String): IInterface { + return this + } - override fun asBinder(): IBinder = binder + override fun asBinder(): IBinder = this override fun onTileAdded() { isTileAdded = true diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileLifecycleManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileLifecycleManagerKosmos.kt new file mode 100644 index 000000000000..a0fc76b3d7de --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileLifecycleManagerKosmos.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2024 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.app.activityManager +import android.content.applicationContext +import android.os.fakeExecutorHandler +import com.android.systemui.broadcast.broadcastDispatcher +import com.android.systemui.concurrency.fakeExecutor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.qs.tiles.impl.custom.packageManagerAdapterFacade +import com.android.systemui.util.mockito.mock + +val Kosmos.tileLifecycleManagerFactory: TileLifecycleManager.Factory by + Kosmos.Fixture { + TileLifecycleManager.Factory { intent, userHandle -> + TileLifecycleManager( + fakeExecutorHandler, + applicationContext, + tileServices, + packageManagerAdapterFacade.packageManagerAdapter, + broadcastDispatcher, + intent, + userHandle, + activityManager, + mock(), + fakeExecutor, + ) + } + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileServicesKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileServicesKosmos.kt new file mode 100644 index 000000000000..3f129dac2600 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileServicesKosmos.kt @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2024 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.applicationContext +import android.os.fakeExecutorHandler +import com.android.systemui.broadcast.broadcastDispatcher +import com.android.systemui.concurrency.fakeExecutor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.qs.QSHost +import com.android.systemui.qs.pipeline.data.repository.customTileAddedRepository +import com.android.systemui.qs.pipeline.domain.interactor.panelInteractor +import com.android.systemui.settings.userTracker +import com.android.systemui.statusbar.commandQueue +import com.android.systemui.statusbar.phone.ui.StatusBarIconController +import com.android.systemui.statusbar.policy.keyguardStateController +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever + +val Kosmos.tileServices: TileServices by + Kosmos.Fixture { + val qsHost: QSHost = mock { whenever(context).thenReturn(applicationContext) } + TileServices( + qsHost, + { fakeExecutorHandler }, + broadcastDispatcher, + userTracker, + keyguardStateController, + commandQueue, + mock<StatusBarIconController>(), + panelInteractor, + tileLifecycleManagerFactory, + customTileAddedRepository, + fakeExecutor, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TilesExternalKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TilesExternalKosmos.kt index 36c2c2b6eb23..9a6730e07666 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TilesExternalKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TilesExternalKosmos.kt @@ -18,13 +18,9 @@ package com.android.systemui.qs.external import android.content.ComponentName import com.android.systemui.kosmos.Kosmos -import com.android.systemui.util.mockito.mock var Kosmos.componentName: ComponentName by Kosmos.Fixture() -/** Returns mocks */ -var Kosmos.tileLifecycleManagerFactory: TileLifecycleManager.Factory by Kosmos.Fixture { mock {} } - val Kosmos.iQSTileService: FakeIQSTileService by Kosmos.Fixture { FakeIQSTileService() } val Kosmos.tileServiceManagerFacade: FakeTileServiceManagerFacade by Kosmos.Fixture { FakeTileServiceManagerFacade(iQSTileService) } @@ -34,4 +30,3 @@ val Kosmos.tileServiceManager: TileServiceManager by val Kosmos.tileServicesFacade: FakeTileServicesFacade by Kosmos.Fixture { (FakeTileServicesFacade(tileServiceManager)) } -val Kosmos.tileServices: TileServices by Kosmos.Fixture { tileServicesFacade.tileServices } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/domain/interactor/PanelInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/domain/interactor/PanelInteractorKosmos.kt new file mode 100644 index 000000000000..d10780b6b817 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/domain/interactor/PanelInteractorKosmos.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 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.pipeline.domain.interactor + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.shade.shadeController + +val Kosmos.panelInteractor by Kosmos.Fixture { PanelInteractorImpl(shadeController) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/CustomTileKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/CustomTileKosmos.kt index 7b9992dd5d4f..42437d5a5b81 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/CustomTileKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/CustomTileKosmos.kt @@ -23,6 +23,7 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.activityStarter import com.android.systemui.qs.external.FakeCustomTileStatePersister import com.android.systemui.qs.external.tileServices +import com.android.systemui.qs.external.tileServicesFacade import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler import com.android.systemui.qs.tiles.base.logging.QSTileLogger @@ -86,7 +87,7 @@ val Kosmos.customTileServiceInteractor: CustomTileServiceInteractor by customTileInteractor, userRepository, qsTileLogger, - tileServices, + tileServicesFacade.tileServices, testScope.backgroundScope, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakePackageManagerAdapterFacade.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakePackageManagerAdapterFacade.kt index 634d121a556c..fa8d36366415 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakePackageManagerAdapterFacade.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakePackageManagerAdapterFacade.kt @@ -17,6 +17,7 @@ package com.android.systemui.qs.tiles.impl.custom.data.repository import android.content.ComponentName +import android.content.pm.PackageInfo import android.content.pm.ServiceInfo import android.os.Bundle import com.android.systemui.qs.external.PackageManagerAdapter @@ -24,6 +25,7 @@ import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever +import org.mockito.ArgumentMatchers.anyInt /** * Facade for [PackageManagerAdapter] to provide a fake-like behaviour. You can create this class @@ -45,19 +47,33 @@ class FakePackageManagerAdapterFacade( init { whenever(packageManagerAdapter.getServiceInfo(eq(componentName), any())).thenAnswer { - ServiceInfo().apply { - metaData = - Bundle().apply { - putBoolean( - android.service.quicksettings.TileService.META_DATA_TOGGLEABLE_TILE, - isToggleable - ) - putBoolean( - android.service.quicksettings.TileService.META_DATA_ACTIVE_TILE, - isActive - ) - } - } + createServiceInfo() + } + whenever( + packageManagerAdapter.getPackageInfoAsUser( + eq(componentName.packageName), + anyInt(), + anyInt() + ) + ) + .thenAnswer { PackageInfo().apply { packageName = componentName.packageName } } + whenever(packageManagerAdapter.getServiceInfo(eq(componentName), anyInt(), anyInt())) + .thenAnswer { createServiceInfo() } + } + + private fun createServiceInfo(): ServiceInfo { + return ServiceInfo().apply { + metaData = + Bundle().apply { + putBoolean( + android.service.quicksettings.TileService.META_DATA_TOGGLEABLE_TILE, + isToggleable + ) + putBoolean( + android.service.quicksettings.TileService.META_DATA_ACTIVE_TILE, + isActive + ) + } } } |