Fix click on TileService bug when closing shade
If an active (unbound) tile is clicked and then the shade is closed
immediately, the click would never be sent to the tile. This was because
a tile that has `onStopListening` called will immediately stopListening
(even if not bound) and reject the click when bound finally happens.
Instead, queue the stopListening to happen right after the click is
dispatched once the tile is bound.
Also, fix when we unbind from active tiles (as together with this it was
causing multiple calls to `onStopListening`). Now:
* If an active tile requests listening, it will be unbound right after
they send a status update.
* If an active tile is bound because of a click, it will stop listening
and be unbound as if it was not active.
Test: atest com.android.systemui.qs
Test: atest CtsTileServiceTestCases CtsSystemUiHostTestCases
Flag: ACONFIG com.android.systemui.qs_custom_tile_click_guaranteed_bug_fix DISABLED
Fixes: 339290820
Change-Id: I0d0a87304e252ad68c48145819098115b00399a1
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index e69ac0a..626e219 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -882,3 +882,13 @@
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 0000000..8d1aa73
--- /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 2a726c2..24b7a01 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.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 @@
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 @@
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 @@
@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 f8bf0a6..6bc5095 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 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 @@
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 @@
}
}
+ 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 8278c79..d457e88 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 @@
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 f57f040..68307b1 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.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.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 @@
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 @@
}
@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 @@
}
@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 0ff29db..1c86638 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.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.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 @@
@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 @@
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 @@
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 b62d59d..bcff88a 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.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.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 @@
@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 @@
}
@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 @@
}
@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 @@
}
@Test
+ @DisableFlags(FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX)
public void tileFreedForCorrectUser() throws RemoteException {
verify(mCommandQueue).addCallback(mCallbacksArgumentCaptor.capture());
@@ -297,6 +340,42 @@
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 cff5980..744942c 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 @@
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 0000000..a0fc76b
--- /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 0000000..3f129da
--- /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 36c2c2b..9a6730e 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 @@
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.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 0000000..d10780b
--- /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 7b9992d..42437d5a 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.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 @@
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 634d121..fa8d363 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.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 @@
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
+ )
+ }
}
}