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
This commit is contained in:
Govinda Wasserman
2022-08-26 14:51:08 -04:00
parent a0659a2552
commit efeffbb53d
5 changed files with 287 additions and 0 deletions

View File

@@ -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()
}
}

View File

@@ -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;
}

View File

@@ -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 @@ abstract class SystemUICoreStartableModule {
@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

View File

@@ -253,6 +253,9 @@ public class Flags {
// 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 ==========================
// | |

View File

@@ -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() {}
}
}