diff --git a/core/java/android/service/controls/ControlsProviderService.java b/core/java/android/service/controls/ControlsProviderService.java index d2a4ae2820611..9396a888ec130 100644 --- a/core/java/android/service/controls/ControlsProviderService.java +++ b/core/java/android/service/controls/ControlsProviderService.java @@ -68,6 +68,18 @@ public abstract class ControlsProviderService extends Service { public static final String META_DATA_PANEL_ACTIVITY = "android.service.controls.META_DATA_PANEL_ACTIVITY"; + /** + * Boolean extra containing the value of + * {@link android.provider.Settings.Secure#LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS}. + * + * This is passed with the intent when the panel specified by {@link #META_DATA_PANEL_ACTIVITY} + * is launched. + * + * @hide + */ + public static final String EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS = + "android.service.controls.EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS"; + /** * @hide */ diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt index 4c8e1ac968f9a..a07c716bc8f96 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt @@ -28,6 +28,7 @@ import android.content.Intent import android.graphics.drawable.Drawable import android.graphics.drawable.LayerDrawable import android.service.controls.Control +import android.service.controls.ControlsProviderService import android.util.Log import android.view.ContextThemeWrapper import android.view.LayoutInflater @@ -48,6 +49,7 @@ import com.android.systemui.Dumpable import com.android.systemui.R import com.android.systemui.controls.ControlsMetricsLogger import com.android.systemui.controls.ControlsServiceInfo +import com.android.systemui.controls.ControlsSettingsRepository import com.android.systemui.controls.CustomIconCache import com.android.systemui.controls.controller.ControlsController import com.android.systemui.controls.controller.StructureInfo @@ -96,6 +98,7 @@ class ControlsUiControllerImpl @Inject constructor ( private val userFileManager: UserFileManager, private val userTracker: UserTracker, private val taskViewFactory: Optional, + private val controlsSettingsRepository: ControlsSettingsRepository, dumpManager: DumpManager ) : ControlsUiController, Dumpable { @@ -354,7 +357,6 @@ class ControlsUiControllerImpl @Inject constructor ( } else { items[0] } - maybeUpdateSelectedItem(selectionItem) createControlsSpaceFrame() @@ -374,11 +376,20 @@ class ControlsUiControllerImpl @Inject constructor ( } private fun createPanelView(componentName: ComponentName) { - val pendingIntent = PendingIntent.getActivity( + val setting = controlsSettingsRepository + .allowActionOnTrivialControlsInLockscreen.value + val pendingIntent = PendingIntent.getActivityAsUser( context, 0, - Intent().setComponent(componentName), - PendingIntent.FLAG_IMMUTABLE + Intent() + .setComponent(componentName) + .putExtra( + ControlsProviderService.EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS, + setting + ), + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, + null, + userTracker.userHandle ) parent.requireViewById(R.id.controls_scroll_view).visibility = View.GONE @@ -698,6 +709,8 @@ class ControlsUiControllerImpl @Inject constructor ( println("hidden: $hidden") println("selectedItem: $selectedItem") println("lastSelections: $lastSelections") + println("setting: ${controlsSettingsRepository + .allowActionOnTrivialControlsInLockscreen.value}") } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt index e679b1391c770..d965e337f47a6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt @@ -16,15 +16,26 @@ package com.android.systemui.controls.ui +import android.app.PendingIntent import android.content.ComponentName import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.pm.ServiceInfo +import android.os.UserHandle +import android.service.controls.ControlsProviderService import android.testing.AndroidTestingRunner import android.testing.TestableLooper +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View import android.widget.FrameLayout import androidx.test.filters.SmallTest +import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.controls.ControlsMetricsLogger +import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.controls.CustomIconCache +import com.android.systemui.controls.FakeControlsSettingsRepository import com.android.systemui.controls.controller.ControlsController import com.android.systemui.controls.controller.StructureInfo import com.android.systemui.controls.management.ControlsListingController @@ -38,19 +49,26 @@ import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.FakeSharedPreferences import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.argumentCaptor +import com.android.systemui.util.mockito.capture +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.mock import com.android.systemui.util.time.FakeSystemClock +import com.android.wm.shell.TaskView import com.android.wm.shell.TaskViewFactory import com.google.common.truth.Truth.assertThat import dagger.Lazy import java.util.Optional +import java.util.function.Consumer 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.anyString -import org.mockito.Mockito.mock +import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.never +import org.mockito.Mockito.spy import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations @@ -70,9 +88,9 @@ class ControlsUiControllerImplTest : SysuiTestCase() { @Mock lateinit var userFileManager: UserFileManager @Mock lateinit var userTracker: UserTracker @Mock lateinit var taskViewFactory: TaskViewFactory - @Mock lateinit var activityContext: Context @Mock lateinit var dumpManager: DumpManager val sharedPreferences = FakeSharedPreferences() + lateinit var controlsSettingsRepository: FakeControlsSettingsRepository var uiExecutor = FakeExecutor(FakeSystemClock()) var bgExecutor = FakeExecutor(FakeSystemClock()) @@ -83,6 +101,17 @@ class ControlsUiControllerImplTest : SysuiTestCase() { fun setup() { MockitoAnnotations.initMocks(this) + controlsSettingsRepository = FakeControlsSettingsRepository() + + // This way, it won't be cloned every time `LayoutInflater.fromContext` is called, but we + // need to clone it once so we don't modify the original one. + mContext.addMockSystemService( + Context.LAYOUT_INFLATER_SERVICE, + mContext.baseContext + .getSystemService(LayoutInflater::class.java)!! + .cloneInContext(mContext) + ) + parent = FrameLayout(mContext) underTest = @@ -100,6 +129,7 @@ class ControlsUiControllerImplTest : SysuiTestCase() { userFileManager, userTracker, Optional.of(taskViewFactory), + controlsSettingsRepository, dumpManager ) `when`( @@ -113,11 +143,12 @@ class ControlsUiControllerImplTest : SysuiTestCase() { `when`(userFileManager.getSharedPreferences(anyString(), anyInt(), anyInt())) .thenReturn(sharedPreferences) `when`(userTracker.userId).thenReturn(0) + `when`(userTracker.userHandle).thenReturn(UserHandle.of(0)) } @Test fun testGetPreferredStructure() { - val structureInfo = mock(StructureInfo::class.java) + val structureInfo = mock() underTest.getPreferredSelectedItem(listOf(structureInfo)) verify(userFileManager) .getSharedPreferences( @@ -189,14 +220,195 @@ class ControlsUiControllerImplTest : SysuiTestCase() { @Test fun testPanelDoesNotRefreshControls() { val panel = SelectedItem.PanelItem("App name", ComponentName("pkg", "cls")) + setUpPanel(panel) + + underTest.show(parent, {}, context) + verify(controlsController, never()).refreshStatus(any(), any()) + } + + @Test + fun testPanelCallsTaskViewFactoryCreate() { + mockLayoutInflater() + val panel = SelectedItem.PanelItem("App name", ComponentName("pkg", "cls")) + val serviceInfo = setUpPanel(panel) + + underTest.show(parent, {}, context) + + val captor = argumentCaptor() + + verify(controlsListingController).addCallback(capture(captor)) + + captor.value.onServicesUpdated(listOf(serviceInfo)) + FakeExecutor.exhaustExecutors(uiExecutor, bgExecutor) + + verify(taskViewFactory).create(eq(context), eq(uiExecutor), any()) + } + + @Test + fun testPanelControllerStartActivityWithCorrectArguments() { + mockLayoutInflater() + controlsSettingsRepository.setAllowActionOnTrivialControlsInLockscreen(true) + + val panel = SelectedItem.PanelItem("App name", ComponentName("pkg", "cls")) + val serviceInfo = setUpPanel(panel) + + underTest.show(parent, {}, context) + + val captor = argumentCaptor() + + verify(controlsListingController).addCallback(capture(captor)) + + captor.value.onServicesUpdated(listOf(serviceInfo)) + FakeExecutor.exhaustExecutors(uiExecutor, bgExecutor) + + val pendingIntent = verifyPanelCreatedAndStartTaskView() + + with(pendingIntent) { + assertThat(isActivity).isTrue() + assertThat(intent.component).isEqualTo(serviceInfo.panelActivity) + assertThat( + intent.getBooleanExtra( + ControlsProviderService.EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS, + false + ) + ) + .isTrue() + } + } + + @Test + fun testPendingIntentExtrasAreModified() { + mockLayoutInflater() + controlsSettingsRepository.setAllowActionOnTrivialControlsInLockscreen(true) + + val panel = SelectedItem.PanelItem("App name", ComponentName("pkg", "cls")) + val serviceInfo = setUpPanel(panel) + + underTest.show(parent, {}, context) + + val captor = argumentCaptor() + + verify(controlsListingController).addCallback(capture(captor)) + + captor.value.onServicesUpdated(listOf(serviceInfo)) + FakeExecutor.exhaustExecutors(uiExecutor, bgExecutor) + + val pendingIntent = verifyPanelCreatedAndStartTaskView() + assertThat( + pendingIntent.intent.getBooleanExtra( + ControlsProviderService.EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS, + false + ) + ) + .isTrue() + + underTest.hide() + + clearInvocations(controlsListingController, taskViewFactory) + controlsSettingsRepository.setAllowActionOnTrivialControlsInLockscreen(false) + underTest.show(parent, {}, context) + + verify(controlsListingController).addCallback(capture(captor)) + captor.value.onServicesUpdated(listOf(serviceInfo)) + FakeExecutor.exhaustExecutors(uiExecutor, bgExecutor) + + val newPendingIntent = verifyPanelCreatedAndStartTaskView() + assertThat( + newPendingIntent.intent.getBooleanExtra( + ControlsProviderService.EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS, + false + ) + ) + .isFalse() + } + + private fun setUpPanel(panel: SelectedItem.PanelItem): ControlsServiceInfo { + val activity = ComponentName("pkg", "activity") sharedPreferences .edit() .putString("controls_component", panel.componentName.flattenToString()) .putString("controls_structure", panel.appName.toString()) .putBoolean("controls_is_panel", true) .commit() + return ControlsServiceInfo(panel.componentName, panel.appName, activity) + } - underTest.show(parent, {}, activityContext) - verify(controlsController, never()).refreshStatus(any(), any()) + private fun verifyPanelCreatedAndStartTaskView(): PendingIntent { + val taskViewConsumerCaptor = argumentCaptor>() + verify(taskViewFactory).create(eq(context), eq(uiExecutor), capture(taskViewConsumerCaptor)) + + val taskView: TaskView = mock { + `when`(this.post(any())).thenAnswer { + uiExecutor.execute(it.arguments[0] as Runnable) + true + } + } + // calls PanelTaskViewController#launchTaskView + taskViewConsumerCaptor.value.accept(taskView) + val listenerCaptor = argumentCaptor() + verify(taskView).setListener(any(), capture(listenerCaptor)) + listenerCaptor.value.onInitialized() + FakeExecutor.exhaustExecutors(uiExecutor, bgExecutor) + + val pendingIntentCaptor = argumentCaptor() + verify(taskView).startActivity(capture(pendingIntentCaptor), any(), any(), any()) + return pendingIntentCaptor.value + } + + private fun ControlsServiceInfo( + componentName: ComponentName, + label: CharSequence, + panelComponentName: ComponentName? = null + ): ControlsServiceInfo { + val serviceInfo = + ServiceInfo().apply { + applicationInfo = ApplicationInfo() + packageName = componentName.packageName + name = componentName.className + } + return spy(ControlsServiceInfo(mContext, serviceInfo)).apply { + `when`(loadLabel()).thenReturn(label) + `when`(loadIcon()).thenReturn(mock()) + `when`(panelActivity).thenReturn(panelComponentName) + } + } + + private fun mockLayoutInflater() { + LayoutInflater.from(context) + .setPrivateFactory( + object : LayoutInflater.Factory2 { + override fun onCreateView( + view: View?, + name: String, + context: Context, + attrs: AttributeSet + ): View? { + return onCreateView(name, context, attrs) + } + + override fun onCreateView( + name: String, + context: Context, + attrs: AttributeSet + ): View? { + if (FrameLayout::class.java.simpleName.equals(name)) { + val mock: FrameLayout = mock { + `when`(this.context).thenReturn(context) + `when`(this.id).thenReturn(R.id.controls_panel) + `when`(this.requireViewById(any())).thenCallRealMethod() + `when`(this.findViewById(R.id.controls_panel)) + .thenReturn(this) + `when`(this.post(any())).thenAnswer { + uiExecutor.execute(it.arguments[0] as Runnable) + true + } + } + return mock + } else { + return null + } + } + } + ) } }