From 52c10f48d0d2958c06bd0752f0e74a9f7e9f0830 Mon Sep 17 00:00:00 2001 From: Fabian Kozynski Date: Mon, 28 Nov 2022 16:03:12 -0500 Subject: [PATCH] Pass Secure.LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS Add an EXTRA for passing the value of this setting to the activities when the TaskView is started. That way they can respect it when showing content. Also, make sure that we create the pending intent for the correct user Test: atest ControlsUiControllerImplTest Test: manual with test app Fixes: 259953724 Change-Id: I44bb8191f8412eb098c3cc654dea2e8badd6fabd --- .../controls/ControlsProviderService.java | 12 + .../controls/ui/ControlsUiControllerImpl.kt | 21 +- .../ui/ControlsUiControllerImplTest.kt | 222 +++++++++++++++++- 3 files changed, 246 insertions(+), 9 deletions(-) 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 + } + } + } + ) } }