summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/aconfig/systemui.aconfig10
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/external/CloseShadeRightAfterClickTestB339290820.kt215
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java26
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java24
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java12
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java64
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceManagerTest.java98
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServicesTest.java83
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/FakeIQSTileService.kt10
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileLifecycleManagerKosmos.kt44
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileServicesKosmos.kt50
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TilesExternalKosmos.kt5
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/domain/interactor/PanelInteractorKosmos.kt22
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/CustomTileKosmos.kt3
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakePackageManagerAdapterFacade.kt42
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
+ )
+ }
}
}