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:
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 ==========================
|
||||
// | |
|
||||
|
||||
@@ -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() {}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user