Adds ChooserSelector
ChooserSelector routes Chooser requests to either the bundled or
unbundled Chooser depending on the value of the UNBUNDLED_CHOOSER flag.
Test: atest ChooserSelectorTest
BUG: 241104067
Change-Id: I9ca3fa3103c7710cce61bc09352c27e8fbc19140
diff --git a/packages/SystemUI/src/com/android/systemui/ChooserSelector.kt b/packages/SystemUI/src/com/android/systemui/ChooserSelector.kt
new file mode 100644
index 0000000..109be40
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/ChooserSelector.kt
@@ -0,0 +1,67 @@
+package com.android.systemui
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.pm.PackageManager
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.FlagListenable
+import com.android.systemui.flags.Flags
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withContext
+
+@SysUISingleton
+class ChooserSelector @Inject constructor(
+ context: Context,
+ private val featureFlags: FeatureFlags,
+ @Application private val coroutineScope: CoroutineScope,
+ @Background private val bgDispatcher: CoroutineDispatcher
+) : CoreStartable(context) {
+
+ private val packageManager = context.packageManager
+ private val chooserComponent = ComponentName.unflattenFromString(
+ context.resources.getString(ChooserSelectorResourceHelper.CONFIG_CHOOSER_ACTIVITY))
+
+ override fun start() {
+ coroutineScope.launch {
+ val listener = FlagListenable.Listener { event ->
+ if (event.flagId == Flags.CHOOSER_UNBUNDLED.id) {
+ launch { updateUnbundledChooserEnabled() }
+ event.requestNoRestart()
+ }
+ }
+ featureFlags.addListener(Flags.CHOOSER_UNBUNDLED, listener)
+ updateUnbundledChooserEnabled()
+
+ awaitCancellationAndThen { featureFlags.removeListener(listener) }
+ }
+ }
+
+ private suspend fun updateUnbundledChooserEnabled() {
+ setUnbundledChooserEnabled(withContext(bgDispatcher) {
+ featureFlags.isEnabled(Flags.CHOOSER_UNBUNDLED)
+ })
+ }
+
+ private fun setUnbundledChooserEnabled(enabled: Boolean) {
+ val newState = if (enabled) {
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED
+ } else {
+ PackageManager.COMPONENT_ENABLED_STATE_DISABLED
+ }
+ packageManager.setComponentEnabledSetting(chooserComponent, newState, /* flags = */ 0)
+ }
+
+ suspend inline fun awaitCancellation(): Nothing = suspendCancellableCoroutine { }
+ suspend inline fun awaitCancellationAndThen(block: () -> Unit): Nothing = try {
+ awaitCancellation()
+ } finally {
+ block()
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/ChooserSelectorResourceHelper.java b/packages/SystemUI/src/com/android/systemui/ChooserSelectorResourceHelper.java
new file mode 100644
index 0000000..7a2de7b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/ChooserSelectorResourceHelper.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui;
+
+import androidx.annotation.StringRes;
+
+import com.android.internal.R;
+
+/** Helper class for referencing resources */
+class ChooserSelectorResourceHelper {
+
+ private ChooserSelectorResourceHelper() {
+ }
+
+ @StringRes
+ static final int CONFIG_CHOOSER_ACTIVITY = R.string.config_chooserActivity;
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
index 6db3e82..8bb27a7 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
@@ -17,6 +17,7 @@
package com.android.systemui.dagger
import com.android.keyguard.KeyguardBiometricLockoutLogger
+import com.android.systemui.ChooserSelector
import com.android.systemui.CoreStartable
import com.android.systemui.LatencyTester
import com.android.systemui.ScreenDecorations
@@ -60,6 +61,12 @@
@ClassKey(AuthController::class)
abstract fun bindAuthController(service: AuthController): CoreStartable
+ /** Inject into ChooserCoreStartable. */
+ @Binds
+ @IntoMap
+ @ClassKey(ChooserSelector::class)
+ abstract fun bindChooserSelector(sysui: ChooserSelector): CoreStartable
+
/** Inject into ClipboardListener. */
@Binds
@IntoMap
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
index d517aa3..ee854b6 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
@@ -253,6 +253,9 @@
// 1400 - columbus, b/242800729
public static final UnreleasedFlag QUICK_TAP_IN_PCC = new UnreleasedFlag(1400);
+ // 1500 - chooser
+ public static final UnreleasedFlag CHOOSER_UNBUNDLED = new UnreleasedFlag(1500);
+
// Pay no attention to the reflection behind the curtain.
// ========================== Curtain ==========================
// | |
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ChooserSelectorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/ChooserSelectorTest.kt
new file mode 100644
index 0000000..6b1ef38
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/ChooserSelectorTest.kt
@@ -0,0 +1,179 @@
+package com.android.systemui
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.pm.PackageManager
+import android.content.res.Resources
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flag
+import com.android.systemui.flags.FlagListenable
+import com.android.systemui.flags.Flags
+import com.android.systemui.flags.UnreleasedFlag
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.kotlinArgumentCaptor
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.test.TestCoroutineDispatcher
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyZeroInteractions
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class ChooserSelectorTest : SysuiTestCase() {
+
+ private val flagListener = kotlinArgumentCaptor<FlagListenable.Listener>()
+
+ private val testDispatcher = TestCoroutineDispatcher()
+ private val testScope = CoroutineScope(testDispatcher)
+
+ private lateinit var chooserSelector: ChooserSelector
+
+ @Mock private lateinit var mockContext: Context
+ @Mock private lateinit var mockPackageManager: PackageManager
+ @Mock private lateinit var mockResources: Resources
+ @Mock private lateinit var mockFeatureFlags: FeatureFlags
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+
+ `when`(mockContext.packageManager).thenReturn(mockPackageManager)
+ `when`(mockContext.resources).thenReturn(mockResources)
+ `when`(mockResources.getString(anyInt())).thenReturn(
+ ComponentName("TestPackage", "TestClass").flattenToString())
+
+ chooserSelector = ChooserSelector(mockContext, mockFeatureFlags, testScope, testDispatcher)
+ }
+
+ @After
+ fun tearDown() {
+ testDispatcher.cleanupTestCoroutines()
+ }
+
+ @Test
+ fun initialize_registersFlagListenerUntilScopeCancelled() {
+ // Arrange
+
+ // Act
+ chooserSelector.start()
+
+ // Assert
+ verify(mockFeatureFlags).addListener(
+ eq<Flag<*>>(Flags.CHOOSER_UNBUNDLED), flagListener.capture())
+ verify(mockFeatureFlags, never()).removeListener(any())
+
+ // Act
+ testScope.cancel()
+
+ // Assert
+ verify(mockFeatureFlags).removeListener(eq(flagListener.value))
+ }
+
+ @Test
+ fun initialize_enablesUnbundledChooser_whenFlagEnabled() {
+ // Arrange
+ `when`(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(true)
+
+ // Act
+ chooserSelector.start()
+
+ // Assert
+ verify(mockPackageManager).setComponentEnabledSetting(
+ eq(ComponentName("TestPackage", "TestClass")),
+ eq(PackageManager.COMPONENT_ENABLED_STATE_ENABLED),
+ anyInt())
+ }
+
+ @Test
+ fun initialize_disablesUnbundledChooser_whenFlagDisabled() {
+ // Arrange
+ `when`(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(false)
+
+ // Act
+ chooserSelector.start()
+
+ // Assert
+ verify(mockPackageManager).setComponentEnabledSetting(
+ eq(ComponentName("TestPackage", "TestClass")),
+ eq(PackageManager.COMPONENT_ENABLED_STATE_DISABLED),
+ anyInt())
+ }
+
+ @Test
+ fun enablesUnbundledChooser_whenFlagBecomesEnabled() {
+ // Arrange
+ `when`(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(false)
+ chooserSelector.start()
+ verify(mockFeatureFlags).addListener(
+ eq<Flag<*>>(Flags.CHOOSER_UNBUNDLED), flagListener.capture())
+ verify(mockPackageManager, never()).setComponentEnabledSetting(
+ any(), eq(PackageManager.COMPONENT_ENABLED_STATE_ENABLED), anyInt())
+
+ // Act
+ `when`(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(true)
+ flagListener.value.onFlagChanged(TestFlagEvent(Flags.CHOOSER_UNBUNDLED.id))
+
+ // Assert
+ verify(mockPackageManager).setComponentEnabledSetting(
+ eq(ComponentName("TestPackage", "TestClass")),
+ eq(PackageManager.COMPONENT_ENABLED_STATE_ENABLED),
+ anyInt())
+ }
+
+ @Test
+ fun disablesUnbundledChooser_whenFlagBecomesDisabled() {
+ // Arrange
+ `when`(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(true)
+ chooserSelector.start()
+ verify(mockFeatureFlags).addListener(
+ eq<Flag<*>>(Flags.CHOOSER_UNBUNDLED), flagListener.capture())
+ verify(mockPackageManager, never()).setComponentEnabledSetting(
+ any(), eq(PackageManager.COMPONENT_ENABLED_STATE_DISABLED), anyInt())
+
+ // Act
+ `when`(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(false)
+ flagListener.value.onFlagChanged(TestFlagEvent(Flags.CHOOSER_UNBUNDLED.id))
+
+ // Assert
+ verify(mockPackageManager).setComponentEnabledSetting(
+ eq(ComponentName("TestPackage", "TestClass")),
+ eq(PackageManager.COMPONENT_ENABLED_STATE_DISABLED),
+ anyInt())
+ }
+
+ @Test
+ fun doesNothing_whenAnotherFlagChanges() {
+ // Arrange
+ `when`(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(false)
+ chooserSelector.start()
+ verify(mockFeatureFlags).addListener(
+ eq<Flag<*>>(Flags.CHOOSER_UNBUNDLED), flagListener.capture())
+ clearInvocations(mockPackageManager)
+
+ // Act
+ `when`(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(false)
+ flagListener.value.onFlagChanged(TestFlagEvent(Flags.CHOOSER_UNBUNDLED.id + 1))
+
+ // Assert
+ verifyZeroInteractions(mockPackageManager)
+ }
+
+ private class TestFlagEvent(override val flagId: Int) : FlagListenable.FlagEvent {
+ override fun requestNoRestart() {}
+ }
+}