diff options
| author | 2023-01-12 16:12:00 -0500 | |
|---|---|---|
| committer | 2023-01-23 10:05:29 -0500 | |
| commit | eae741388e1c960a76f03f7289bf9c6370d5352e (patch) | |
| tree | 71fee633b50251a472aaaf890b9e49e41196c85e | |
| parent | 694d2d571106b424634fffae1431525bb70bb6bb (diff) | |
Add new add flow for panels
This adds a flow for adding panels that will query consent from the
user. Note that this needs Flags.APP_PANELS_ALL_APPS_ALLOWED for those
panels to show in the first place.
Test: manual, trigger dialog
Test: atest com.android.systemui.controls
Bug: 265180342
Change-Id: I5a0c382f98595daffbd7e4dfe1e52adb43b2f1c5
14 files changed, 566 insertions, 59 deletions
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 066b185c6d1f..8dcd3b0eb476 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2238,6 +2238,14 @@ <!-- Removed control in management screen [CHAR LIMIT=20] --> <string name="controls_removed">Removed</string> + <!-- Title for the dialog presented to the user to authorize this app to display a Device + controls panel (embedded activity) instead of controls rendered by SystemUI [CHAR LIMIT=30] --> + <string name="controls_panel_authorization_title">Add <xliff:g id="appName" example="My app">%s</xliff:g>?</string> + + <!-- Shows in a dialog presented to the user to authorize this app to display a Device controls + panel (embedded activity) instead of controls rendered by SystemUI [CHAR LIMIT=NONE] --> + <string name="controls_panel_authorization">When you add <xliff:g id="appName" example="My app">%s</xliff:g>, it can add controls and content to this panel. In some apps, you can choose which controls show up here.</string> + <!-- a11y state description for a control that is currently favorited [CHAR LIMIT=NONE] --> <string name="accessibility_control_favorite">Favorited</string> <!-- a11y state description for a control that is currently favorited with its position [CHAR LIMIT=NONE] --> @@ -2387,6 +2395,8 @@ <string name="controls_menu_add">Add controls</string> <!-- Controls menu, edit [CHAR_LIMIT=30] --> <string name="controls_menu_edit">Edit controls</string> + <!-- Controls menu, add another app [CHAR LIMIT=30] --> + <string name="controls_menu_add_another_app">Add app</string> <!-- Title for the media output dialog with media related devices [CHAR LIMIT=50] --> <string name="media_output_dialog_add_output">Add outputs</string> diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt index f29f6d0dd0cb..822190f21da1 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt @@ -188,6 +188,8 @@ interface ControlsController : UserAwareController { /** See [ControlsUiController.getPreferredSelectedItem]. */ fun getPreferredSelection(): SelectedItem + fun setPreferredSelection(selectedItem: SelectedItem) + /** * Bind to a service that provides a Device Controls panel (embedded activity). This will allow * the app to remain "warm", and reduce latency. diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt index 111fcbbe30be..1cbfe01a9a1a 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt @@ -36,6 +36,7 @@ import com.android.systemui.backup.BackupHelper import com.android.systemui.controls.ControlStatus import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.controls.management.ControlsListingController +import com.android.systemui.controls.panels.AuthorizedPanelsRepository import com.android.systemui.controls.ui.ControlsUiController import com.android.systemui.controls.ui.SelectedItem import com.android.systemui.dagger.SysUISingleton @@ -61,6 +62,7 @@ class ControlsControllerImpl @Inject constructor ( private val listingController: ControlsListingController, private val userFileManager: UserFileManager, private val userTracker: UserTracker, + private val authorizedPanelsRepository: AuthorizedPanelsRepository, optionalWrapper: Optional<ControlsFavoritePersistenceWrapper>, dumpManager: DumpManager, ) : Dumpable, ControlsController { @@ -249,6 +251,11 @@ class ControlsControllerImpl @Inject constructor ( private fun resetFavorites() { Favorites.clear() Favorites.load(persistenceWrapper.readFavorites()) + // After loading favorites, add the package names of any apps with favorites to the list + // of authorized panels. That way, if the user has previously favorited controls for an app, + // that panel will be authorized. + authorizedPanelsRepository.addAuthorizedPanels( + Favorites.getAllStructures().map { it.componentName.packageName }.toSet()) } private fun confirmAvailability(): Boolean { @@ -489,6 +496,7 @@ class ControlsControllerImpl @Inject constructor ( if (!confirmAvailability()) return executor.execute { if (Favorites.addFavorite(componentName, structureName, controlInfo)) { + authorizedPanelsRepository.addAuthorizedPanels(setOf(componentName.packageName)) persistenceWrapper.storeFavorites(Favorites.getAllStructures()) } } @@ -555,6 +563,10 @@ class ControlsControllerImpl @Inject constructor ( return uiController.getPreferredSelectedItem(getFavorites()) } + override fun setPreferredSelection(selectedItem: SelectedItem) { + uiController.updatePreferences(selectedItem) + } + override fun dump(pw: PrintWriter, args: Array<out String>) { pw.println("ControlsController state:") pw.println(" Changing users: $userChanging") diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/AppAdapter.kt b/packages/SystemUI/src/com/android/systemui/controls/management/AppAdapter.kt index 753d5addeb11..3fe0f03488d1 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/management/AppAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/management/AppAdapter.kt @@ -45,14 +45,15 @@ import java.util.concurrent.Executor * @param onAppSelected a callback to indicate that an app has been selected in the list. */ class AppAdapter( - backgroundExecutor: Executor, - uiExecutor: Executor, - lifecycle: Lifecycle, - controlsListingController: ControlsListingController, - private val layoutInflater: LayoutInflater, - private val onAppSelected: (ComponentName?) -> Unit = {}, - private val favoritesRenderer: FavoritesRenderer, - private val resources: Resources + backgroundExecutor: Executor, + uiExecutor: Executor, + lifecycle: Lifecycle, + controlsListingController: ControlsListingController, + private val layoutInflater: LayoutInflater, + private val onAppSelected: (ControlsServiceInfo) -> Unit = {}, + private val favoritesRenderer: FavoritesRenderer, + private val resources: Resources, + private val authorizedPanels: Set<String> = emptySet(), ) : RecyclerView.Adapter<AppAdapter.Holder>() { private var listOfServices = emptyList<ControlsServiceInfo>() @@ -64,8 +65,10 @@ class AppAdapter( val localeComparator = compareBy<ControlsServiceInfo, CharSequence>(collator) { it.loadLabel() ?: "" } - listOfServices = serviceInfos.filter { it.panelActivity == null } - .sortedWith(localeComparator) + // No panel or the panel is not authorized + listOfServices = serviceInfos.filter { + it.panelActivity == null || it.panelActivity?.packageName !in authorizedPanels + }.sortedWith(localeComparator) uiExecutor.execute(::notifyDataSetChanged) } } @@ -86,8 +89,8 @@ class AppAdapter( override fun onBindViewHolder(holder: Holder, index: Int) { holder.bindData(listOfServices[index]) - holder.itemView.setOnClickListener { - onAppSelected(ComponentName.unflattenFromString(listOfServices[index].key)) + holder.view.setOnClickListener { + onAppSelected(listOfServices[index]) } } @@ -95,6 +98,8 @@ class AppAdapter( * Holder for binding views in the [RecyclerView]- */ class Holder(view: View, val favRenderer: FavoritesRenderer) : RecyclerView.ViewHolder(view) { + val view: View = itemView + private val icon: ImageView = itemView.requireViewById(com.android.internal.R.id.icon) private val title: TextView = itemView.requireViewById(com.android.internal.R.id.title) private val favorites: TextView = itemView.requireViewById(R.id.favorites) @@ -106,7 +111,11 @@ class AppAdapter( fun bindData(data: ControlsServiceInfo) { icon.setImageDrawable(data.loadIcon()) title.text = data.loadLabel() - val text = favRenderer.renderFavoritesForComponent(data.componentName) + val text = if (data.panelActivity == null) { + favRenderer.renderFavoritesForComponent(data.componentName) + } else { + null + } favorites.text = text favorites.visibility = if (text == null) View.GONE else View.VISIBLE } diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt index 90bc5d0f8daa..54587b20c7e1 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt @@ -17,6 +17,7 @@ package com.android.systemui.controls.management import android.app.ActivityOptions +import android.app.Dialog import android.content.ComponentName import android.content.Context import android.content.Intent @@ -31,12 +32,15 @@ import android.widget.TextView import android.window.OnBackInvokedCallback import android.window.OnBackInvokedDispatcher import androidx.activity.ComponentActivity +import androidx.annotation.VisibleForTesting import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.android.systemui.R +import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.controls.controller.ControlsController +import com.android.systemui.controls.panels.AuthorizedPanelsRepository import com.android.systemui.controls.ui.ControlsActivity -import com.android.systemui.controls.ui.ControlsUiController +import com.android.systemui.controls.ui.SelectedItem import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.settings.UserTracker @@ -52,7 +56,8 @@ open class ControlsProviderSelectorActivity @Inject constructor( private val listingController: ControlsListingController, private val controlsController: ControlsController, private val userTracker: UserTracker, - private val uiController: ControlsUiController + private val authorizedPanelsRepository: AuthorizedPanelsRepository, + private val panelConfirmationDialogFactory: PanelConfirmationDialogFactory ) : ComponentActivity() { companion object { @@ -72,6 +77,7 @@ open class ControlsProviderSelectorActivity @Inject constructor( } } } + private var dialog: Dialog? = null private val mOnBackInvokedCallback = OnBackInvokedCallback { if (DEBUG) { @@ -138,9 +144,11 @@ open class ControlsProviderSelectorActivity @Inject constructor( lifecycle, listingController, LayoutInflater.from(this), - ::launchFavoritingActivity, + ::onAppSelected, FavoritesRenderer(resources, controlsController::countFavoritesForComponent), - resources).apply { + resources, + authorizedPanelsRepository.getAuthorizedPanels() + ).apply { registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { var hasAnimated = false override fun onChanged() { @@ -167,13 +175,35 @@ open class ControlsProviderSelectorActivity @Inject constructor( Log.d(TAG, "Unregistered onBackInvokedCallback") } onBackInvokedDispatcher.unregisterOnBackInvokedCallback(mOnBackInvokedCallback) + dialog?.cancel() + } + + fun onAppSelected(serviceInfo: ControlsServiceInfo) { + dialog?.cancel() + if (serviceInfo.panelActivity == null) { + launchFavoritingActivity(serviceInfo.componentName) + } else { + val appName = serviceInfo.loadLabel() ?: "" + dialog = panelConfirmationDialogFactory.createConfirmationDialog(this, appName) { ok -> + if (ok) { + authorizedPanelsRepository.addAuthorizedPanels( + setOf(serviceInfo.componentName.packageName) + ) + animateExitAndFinish() + val selected = SelectedItem.PanelItem(appName, componentName) + controlsController.setPreferredSelection(selected) + openControlsOrigin() + } + dialog = null + }.also { it.show() } + } } /** * Launch the [ControlsFavoritingActivity] for the specified component. * @param component a component name for a [ControlsProviderService] */ - fun launchFavoritingActivity(component: ComponentName?) { + private fun launchFavoritingActivity(component: ComponentName?) { executor.execute { component?.let { val intent = Intent(applicationContext, ControlsFavoritingActivity::class.java) @@ -194,7 +224,15 @@ open class ControlsProviderSelectorActivity @Inject constructor( super.onDestroy() } - private fun animateExitAndFinish() { + private fun openControlsOrigin() { + startActivity( + Intent(applicationContext, ControlsActivity::class.java), + ActivityOptions.makeSceneTransitionAnimation(this).toBundle() + ) + } + + @VisibleForTesting + internal open fun animateExitAndFinish() { val rootView = requireViewById<ViewGroup>(R.id.controls_management_root) ControlsAnimations.exitAnimation( rootView, diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/PanelConfirmationDialogFactory.kt b/packages/SystemUI/src/com/android/systemui/controls/management/PanelConfirmationDialogFactory.kt new file mode 100644 index 000000000000..6f87aa996f94 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/controls/management/PanelConfirmationDialogFactory.kt @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2023 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.controls.management + +import android.app.Dialog +import android.content.Context +import android.content.DialogInterface +import com.android.systemui.R +import com.android.systemui.statusbar.phone.SystemUIDialog +import java.util.function.Consumer +import javax.inject.Inject + +/** + * Factory to create dialogs for consenting to show app panels for specific apps. + * + * [internalDialogFactory] is for facilitating testing. + */ +class PanelConfirmationDialogFactory( + private val internalDialogFactory: (Context) -> SystemUIDialog +) { + @Inject constructor() : this({ SystemUIDialog(it) }) + + /** + * Creates a dialog to show to the user. [response] will be true if an only if the user responds + * affirmatively. + */ + fun createConfirmationDialog( + context: Context, + appName: CharSequence, + response: Consumer<Boolean> + ): Dialog { + val listener = + DialogInterface.OnClickListener { _, which -> + response.accept(which == DialogInterface.BUTTON_POSITIVE) + } + return internalDialogFactory(context).apply { + setTitle(this.context.getString(R.string.controls_panel_authorization_title, appName)) + setMessage(this.context.getString(R.string.controls_panel_authorization, appName)) + setCanceledOnTouchOutside(true) + setOnCancelListener { response.accept(false) } + setPositiveButton(R.string.controls_dialog_ok, listener) + setNeutralButton(R.string.cancel, listener) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepository.kt b/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepository.kt index 1a6b84a8e054..3e672f391e81 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepository.kt @@ -17,8 +17,15 @@ package com.android.systemui.controls.panels +/** + * Repository for keeping track of which packages the panel has authorized to show control panels + * (embedded activity). + */ interface AuthorizedPanelsRepository { + + /** A set of package names that the user has previously authorized to show panels. */ fun getAuthorizedPanels(): Set<String> + /** Adds [packageNames] to the set of packages that the user has authorized to show panels. */ fun addAuthorizedPanels(packageNames: Set<String>) } diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt index f5c5905779a1..c1cec9dd0f94 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt @@ -58,6 +58,8 @@ interface ControlsUiController { * This element will be the one that appears when the user first opens the controls activity. */ fun getPreferredSelectedItem(structures: List<StructureInfo>): SelectedItem + + fun updatePreferences(selectedItem: SelectedItem) } sealed class SelectedItem { 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 6289788f650a..966dbf1a9694 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt @@ -60,10 +60,13 @@ import com.android.systemui.controls.management.ControlsEditingActivity import com.android.systemui.controls.management.ControlsFavoritingActivity import com.android.systemui.controls.management.ControlsListingController import com.android.systemui.controls.management.ControlsProviderSelectorActivity +import com.android.systemui.controls.panels.AuthorizedPanelsRepository import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.globalactions.GlobalActionsPopupMenu import com.android.systemui.plugins.ActivityStarter import com.android.systemui.settings.UserFileManager @@ -99,6 +102,8 @@ class ControlsUiControllerImpl @Inject constructor ( private val userTracker: UserTracker, private val taskViewFactory: Optional<TaskViewFactory>, private val controlsSettingsRepository: ControlsSettingsRepository, + private val authorizedPanelsRepository: AuthorizedPanelsRepository, + private val featureFlags: FeatureFlags, dumpManager: DumpManager ) : ControlsUiController, Dumpable { @@ -160,6 +165,7 @@ class ControlsUiControllerImpl @Inject constructor ( ): ControlsListingController.ControlsListingCallback { return object : ControlsListingController.ControlsListingCallback { override fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>) { + val authorizedPanels = authorizedPanelsRepository.getAuthorizedPanels() val lastItems = serviceInfos.map { val uid = it.serviceInfo.applicationInfo.uid @@ -169,7 +175,11 @@ class ControlsUiControllerImpl @Inject constructor ( it.loadIcon(), it.componentName, uid, - it.panelActivity + if (it.componentName.packageName in authorizedPanels) { + it.panelActivity + } else { + null + } ) } uiExecutor.execute { @@ -417,14 +427,20 @@ class ControlsUiControllerImpl @Inject constructor ( val isPanel = selectedItem is SelectedItem.PanelItem val selectedStructure = (selectedItem as? SelectedItem.StructureItem)?.structure ?: EMPTY_STRUCTURE + val newFlows = featureFlags.isEnabled(Flags.CONTROLS_MANAGEMENT_NEW_FLOWS) + val addControlsId = if (newFlows || isPanel) { + R.string.controls_menu_add_another_app + } else { + R.string.controls_menu_add + } val items = if (isPanel) { arrayOf( - context.resources.getString(R.string.controls_menu_add), + context.resources.getString(addControlsId), ) } else { arrayOf( - context.resources.getString(R.string.controls_menu_add), + context.resources.getString(addControlsId), context.resources.getString(R.string.controls_menu_edit) ) } @@ -449,7 +465,7 @@ class ControlsUiControllerImpl @Inject constructor ( when (pos) { // 0: Add Control 0 -> { - if (isPanel) { + if (isPanel || newFlows) { startProviderSelectorActivity() } else { startFavoritingActivity(selectedStructure) @@ -610,11 +626,11 @@ class ControlsUiControllerImpl @Inject constructor ( } } - private fun updatePreferences(si: SelectedItem) { + override fun updatePreferences(selectedItem: SelectedItem) { sharedPreferences.edit() - .putString(PREF_COMPONENT, si.componentName.flattenToString()) - .putString(PREF_STRUCTURE_OR_APP_NAME, si.name.toString()) - .putBoolean(PREF_IS_PANEL, si is SelectedItem.PanelItem) + .putString(PREF_COMPONENT, selectedItem.componentName.flattenToString()) + .putString(PREF_STRUCTURE_OR_APP_NAME, selectedItem.name.toString()) + .putBoolean(PREF_IS_PANEL, selectedItem is SelectedItem.PanelItem) .commit() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt index 25f471b0d3e0..d54babfbdc0e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt @@ -33,6 +33,7 @@ import com.android.systemui.backup.BackupHelper import com.android.systemui.controls.ControlStatus import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.controls.management.ControlsListingController +import com.android.systemui.controls.panels.AuthorizedPanelsRepository import com.android.systemui.controls.ui.ControlsUiController import com.android.systemui.dump.DumpManager import com.android.systemui.settings.UserFileManager @@ -66,6 +67,7 @@ import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.verifyNoMoreInteractions import org.mockito.Mockito.`when` +import org.mockito.Mockito.clearInvocations import org.mockito.MockitoAnnotations @SmallTest @@ -88,6 +90,8 @@ class ControlsControllerImplTest : SysuiTestCase() { private lateinit var userTracker: UserTracker @Mock private lateinit var userFileManager: UserFileManager + @Mock + private lateinit var authorizedPanelsRepository: AuthorizedPanelsRepository @Captor private lateinit var structureInfoCaptor: ArgumentCaptor<StructureInfo> @@ -168,6 +172,7 @@ class ControlsControllerImplTest : SysuiTestCase() { listingController, userFileManager, userTracker, + authorizedPanelsRepository, Optional.of(persistenceWrapper), mock(DumpManager::class.java) ) @@ -224,6 +229,7 @@ class ControlsControllerImplTest : SysuiTestCase() { listingController, userFileManager, userTracker, + authorizedPanelsRepository, Optional.of(persistenceWrapper), mock(DumpManager::class.java) ) @@ -231,6 +237,26 @@ class ControlsControllerImplTest : SysuiTestCase() { } @Test + fun testAddAuthorizedPackagesFromSavedFavoritesOnStart() { + clearInvocations(authorizedPanelsRepository) + `when`(persistenceWrapper.readFavorites()).thenReturn(listOf(TEST_STRUCTURE_INFO)) + ControlsControllerImpl( + mContext, + delayableExecutor, + uiController, + bindingController, + listingController, + userFileManager, + userTracker, + authorizedPanelsRepository, + Optional.of(persistenceWrapper), + mock(DumpManager::class.java) + ) + verify(authorizedPanelsRepository) + .addAuthorizedPanels(setOf(TEST_STRUCTURE_INFO.componentName.packageName)) + } + + @Test fun testOnActionResponse() { controller.onActionResponse(TEST_COMPONENT, TEST_CONTROL_ID, ControlAction.RESPONSE_OK) diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/AppAdapterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/AppAdapterTest.kt index 765c4c0ac0f0..226ef3b85706 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/management/AppAdapterTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/AppAdapterTest.kt @@ -21,12 +21,15 @@ import android.content.res.Resources import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.LayoutInflater +import android.view.View import androidx.test.filters.SmallTest import com.android.settingslib.core.lifecycle.Lifecycle import com.android.systemui.SysuiTestCase import com.android.systemui.controls.ControlsServiceInfo 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.mock import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat @@ -36,8 +39,10 @@ import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.Mock import org.mockito.Mockito.`when` +import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations +import java.text.Collator @SmallTest @RunWith(AndroidTestingRunner::class) @@ -49,25 +54,18 @@ class AppAdapterTest : SysuiTestCase() { @Mock lateinit var lifecycle: Lifecycle @Mock lateinit var controlsListingController: ControlsListingController @Mock lateinit var layoutInflater: LayoutInflater - @Mock lateinit var onAppSelected: (ComponentName?) -> Unit + @Mock lateinit var onAppSelected: (ControlsServiceInfo) -> Unit @Mock lateinit var favoritesRenderer: FavoritesRenderer val resources: Resources = context.resources lateinit var adapter: AppAdapter @Before fun setUp() { MockitoAnnotations.initMocks(this) - adapter = AppAdapter(backgroundExecutor, - uiExecutor, - lifecycle, - controlsListingController, - layoutInflater, - onAppSelected, - favoritesRenderer, - resources) } @Test fun testOnServicesUpdated_nullLoadLabel() { + adapter = createAdapterWithAuthorizedPanels(emptySet()) val captor = ArgumentCaptor .forClass(ControlsListingController.ControlsListingCallback::class.java) val controlsServiceInfo = mock<ControlsServiceInfo>() @@ -76,14 +74,14 @@ class AppAdapterTest : SysuiTestCase() { verify(controlsListingController).observe(any(Lifecycle::class.java), captor.capture()) captor.value.onServicesUpdated(serviceInfo) - backgroundExecutor.runAllReady() - uiExecutor.runAllReady() + FakeExecutor.exhaustExecutors(backgroundExecutor, uiExecutor) assertThat(adapter.itemCount).isEqualTo(serviceInfo.size) } @Test - fun testOnServicesUpdatedDoesntHavePanels() { + fun testOnServicesUpdated_showsNotAuthorizedPanels() { + adapter = createAdapterWithAuthorizedPanels(emptySet()) val captor = ArgumentCaptor .forClass(ControlsListingController.ControlsListingCallback::class.java) val serviceInfo = listOf( @@ -93,20 +91,88 @@ class AppAdapterTest : SysuiTestCase() { verify(controlsListingController).observe(any(Lifecycle::class.java), captor.capture()) captor.value.onServicesUpdated(serviceInfo) - backgroundExecutor.runAllReady() - uiExecutor.runAllReady() + FakeExecutor.exhaustExecutors(backgroundExecutor, uiExecutor) + + assertThat(adapter.itemCount).isEqualTo(2) + } + + @Test + fun testOnServicesUpdated_doesntShowAuthorizedPanels() { + adapter = createAdapterWithAuthorizedPanels(setOf(TEST_PACKAGE)) + + val captor = ArgumentCaptor + .forClass(ControlsListingController.ControlsListingCallback::class.java) + val serviceInfo = listOf( + ControlsServiceInfo("no panel", null), + ControlsServiceInfo("panel", ComponentName(TEST_PACKAGE, "cls")) + ) + verify(controlsListingController).observe(any(Lifecycle::class.java), captor.capture()) + + captor.value.onServicesUpdated(serviceInfo) + FakeExecutor.exhaustExecutors(backgroundExecutor, uiExecutor) assertThat(adapter.itemCount).isEqualTo(1) } - fun ControlsServiceInfo( - label: CharSequence, - panelComponentName: ComponentName? = null - ): ControlsServiceInfo { - return mock { - `when`(this.loadLabel()).thenReturn(label) - `when`(this.panelActivity).thenReturn(panelComponentName) - `when`(this.loadIcon()).thenReturn(mock()) + @Test + fun testOnBindSetsClickListenerToCallOnAppSelected() { + adapter = createAdapterWithAuthorizedPanels(emptySet()) + + val captor = ArgumentCaptor + .forClass(ControlsListingController.ControlsListingCallback::class.java) + val serviceInfo = listOf( + ControlsServiceInfo("no panel", null), + ControlsServiceInfo("panel", ComponentName(TEST_PACKAGE, "cls")) + ) + verify(controlsListingController).observe(any(Lifecycle::class.java), captor.capture()) + + captor.value.onServicesUpdated(serviceInfo) + FakeExecutor.exhaustExecutors(backgroundExecutor, uiExecutor) + + val sorted = serviceInfo.sortedWith( + compareBy(Collator.getInstance(resources.configuration.locales[0])) { + it.loadLabel() ?: "" + }) + + sorted.forEachIndexed { index, info -> + val fakeView: View = mock() + val fakeHolder: AppAdapter.Holder = mock() + `when`(fakeHolder.view).thenReturn(fakeView) + + clearInvocations(onAppSelected) + adapter.onBindViewHolder(fakeHolder, index) + val listenerCaptor: ArgumentCaptor<View.OnClickListener> = argumentCaptor() + verify(fakeView).setOnClickListener(capture(listenerCaptor)) + listenerCaptor.value.onClick(fakeView) + + verify(onAppSelected).invoke(info) + } + } + + private fun createAdapterWithAuthorizedPanels(packages: Set<String>): AppAdapter { + return AppAdapter(backgroundExecutor, + uiExecutor, + lifecycle, + controlsListingController, + layoutInflater, + onAppSelected, + favoritesRenderer, + resources, + packages) + } + + companion object { + private fun ControlsServiceInfo( + label: CharSequence, + panelComponentName: ComponentName? = null + ): ControlsServiceInfo { + return mock { + `when`(loadLabel()).thenReturn(label) + `when`(panelActivity).thenReturn(panelComponentName) + `when`(loadIcon()).thenReturn(mock()) + } } + + private const val TEST_PACKAGE = "package" } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsProviderSelectorActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsProviderSelectorActivityTest.kt index 56c3efe1b8e6..8dfd22378a14 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsProviderSelectorActivityTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsProviderSelectorActivityTest.kt @@ -16,7 +16,13 @@ package com.android.systemui.controls.management +import android.app.Dialog +import android.content.ComponentName import android.content.Intent +import android.content.pm.ApplicationInfo +import android.content.pm.ServiceInfo +import android.graphics.drawable.Drawable +import android.os.Bundle import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.window.OnBackInvokedCallback @@ -25,14 +31,23 @@ import androidx.test.filters.SmallTest import androidx.test.rule.ActivityTestRule import androidx.test.runner.intercepting.SingleActivityFactory import com.android.systemui.SysuiTestCase +import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.controls.controller.ControlsController -import com.android.systemui.controls.ui.ControlsUiController +import com.android.systemui.controls.panels.AuthorizedPanelsRepository import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.settings.UserTracker +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.mockito.whenever +import com.google.common.truth.Truth.assertThat import com.google.common.util.concurrent.MoreExecutors import java.util.concurrent.CountDownLatch import java.util.concurrent.Executor +import java.util.function.Consumer import org.junit.Before import org.junit.Rule import org.junit.Test @@ -41,7 +56,11 @@ import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers import org.mockito.Captor import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.never import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyNoMoreInteractions import org.mockito.MockitoAnnotations @SmallTest @@ -58,9 +77,10 @@ class ControlsProviderSelectorActivityTest : SysuiTestCase() { @Mock lateinit var userTracker: UserTracker - @Mock lateinit var uiController: ControlsUiController + @Mock lateinit var authorizedPanelsRepository: AuthorizedPanelsRepository + + @Mock lateinit var dialogFactory: PanelConfirmationDialogFactory - private lateinit var controlsProviderSelectorActivity: ControlsProviderSelectorActivity_Factory private var latch: CountDownLatch = CountDownLatch(1) @Mock private lateinit var mockDispatcher: OnBackInvokedDispatcher @@ -81,7 +101,8 @@ class ControlsProviderSelectorActivityTest : SysuiTestCase() { listingController, controlsController, userTracker, - uiController, + authorizedPanelsRepository, + dialogFactory, mockDispatcher, latch ) @@ -113,13 +134,99 @@ class ControlsProviderSelectorActivityTest : SysuiTestCase() { verify(mockDispatcher).unregisterOnBackInvokedCallback(captureCallback.value) } - public class TestableControlsProviderSelectorActivity( + @Test + fun testOnAppSelectedForNonPanelStartsFavoritingActivity() { + val info = ControlsServiceInfo(ComponentName("test_pkg", "service"), "", null) + activityRule.activity.onAppSelected(info) + + verifyNoMoreInteractions(dialogFactory) + + assertThat(activityRule.activity.lastStartedActivity?.component?.className) + .isEqualTo(ControlsFavoritingActivity::class.java.name) + + assertThat(activityRule.activity.triedToFinish).isTrue() + } + + @Test + fun testOnAppSelectedForPanelTriggersDialog() { + val label = "label" + val info = + ControlsServiceInfo( + ComponentName("test_pkg", "service"), + label, + ComponentName("test_pkg", "activity") + ) + + val dialog: Dialog = mock() + whenever(dialogFactory.createConfirmationDialog(any(), any(), any())).thenReturn(dialog) + + activityRule.activity.onAppSelected(info) + verify(dialogFactory).createConfirmationDialog(any(), eq(label), any()) + verify(dialog).show() + + assertThat(activityRule.activity.triedToFinish).isFalse() + } + + @Test + fun dialogAcceptAddsPackage() { + val label = "label" + val info = + ControlsServiceInfo( + ComponentName("test_pkg", "service"), + label, + ComponentName("test_pkg", "activity") + ) + + val dialog: Dialog = mock() + whenever(dialogFactory.createConfirmationDialog(any(), any(), any())).thenReturn(dialog) + + activityRule.activity.onAppSelected(info) + + val captor: ArgumentCaptor<Consumer<Boolean>> = argumentCaptor() + verify(dialogFactory).createConfirmationDialog(any(), any(), capture(captor)) + + captor.value.accept(true) + + val setCaptor: ArgumentCaptor<Set<String>> = argumentCaptor() + verify(authorizedPanelsRepository).addAuthorizedPanels(capture(setCaptor)) + assertThat(setCaptor.value).containsExactly(info.componentName.packageName) + + assertThat(activityRule.activity.triedToFinish).isTrue() + } + + @Test + fun dialogCancelDoesntAddPackage() { + val label = "label" + val info = + ControlsServiceInfo( + ComponentName("test_pkg", "service"), + label, + ComponentName("test_pkg", "activity") + ) + + val dialog: Dialog = mock() + whenever(dialogFactory.createConfirmationDialog(any(), any(), any())).thenReturn(dialog) + + activityRule.activity.onAppSelected(info) + + val captor: ArgumentCaptor<Consumer<Boolean>> = argumentCaptor() + verify(dialogFactory).createConfirmationDialog(any(), any(), capture(captor)) + + captor.value.accept(false) + + verify(authorizedPanelsRepository, never()).addAuthorizedPanels(any()) + + assertThat(activityRule.activity.triedToFinish).isFalse() + } + + class TestableControlsProviderSelectorActivity( executor: Executor, backExecutor: Executor, listingController: ControlsListingController, controlsController: ControlsController, userTracker: UserTracker, - uiController: ControlsUiController, + authorizedPanelsRepository: AuthorizedPanelsRepository, + dialogFactory: PanelConfirmationDialogFactory, private val mockDispatcher: OnBackInvokedDispatcher, private val latch: CountDownLatch ) : @@ -129,16 +236,50 @@ class ControlsProviderSelectorActivityTest : SysuiTestCase() { listingController, controlsController, userTracker, - uiController + authorizedPanelsRepository, + dialogFactory ) { + + var lastStartedActivity: Intent? = null + var triedToFinish = false + override fun getOnBackInvokedDispatcher(): OnBackInvokedDispatcher { return mockDispatcher } + override fun startActivity(intent: Intent?, options: Bundle?) { + lastStartedActivity = intent + } + override fun onStop() { super.onStop() // ensures that test runner thread does not proceed until ui thread is done latch.countDown() } + + override fun animateExitAndFinish() { + // Activity should only be finished from the rule. + triedToFinish = true + } + } + + companion object { + private fun ControlsServiceInfo( + componentName: ComponentName, + label: CharSequence, + panelComponentName: ComponentName? = null + ): ControlsServiceInfo { + val serviceInfo = + ServiceInfo().apply { + applicationInfo = ApplicationInfo() + packageName = componentName.packageName + name = componentName.className + } + return Mockito.spy(ControlsServiceInfo(mock(), serviceInfo)).apply { + doReturn(label).`when`(this).loadLabel() + doReturn(mock<Drawable>()).`when`(this).loadIcon() + doReturn(panelComponentName).`when`(this).panelActivity + } + } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/PanelConfirmationDialogFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/PanelConfirmationDialogFactoryTest.kt new file mode 100644 index 000000000000..756f267671e1 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/PanelConfirmationDialogFactoryTest.kt @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2023 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.controls.management + +import android.content.DialogInterface +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.phone.SystemUIDialog +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.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class PanelConfirmationDialogFactoryTest : SysuiTestCase() { + + @Test + fun testDialogHasCorrectInfo() { + val mockDialog: SystemUIDialog = mock() { `when`(context).thenReturn(mContext) } + val factory = PanelConfirmationDialogFactory { mockDialog } + val appName = "appName" + + factory.createConfirmationDialog(context, appName) {} + + verify(mockDialog).setCanceledOnTouchOutside(true) + verify(mockDialog) + .setTitle(context.getString(R.string.controls_panel_authorization_title, appName)) + verify(mockDialog) + .setMessage(context.getString(R.string.controls_panel_authorization, appName)) + } + + @Test + fun testDialogPositiveButton() { + val mockDialog: SystemUIDialog = mock() { `when`(context).thenReturn(mContext) } + val factory = PanelConfirmationDialogFactory { mockDialog } + + var response: Boolean? = null + + factory.createConfirmationDialog(context, "") { response = it } + + val captor: ArgumentCaptor<DialogInterface.OnClickListener> = argumentCaptor() + verify(mockDialog).setPositiveButton(eq(R.string.controls_dialog_ok), capture(captor)) + + captor.value.onClick(mockDialog, DialogInterface.BUTTON_POSITIVE) + + assertThat(response).isTrue() + } + + @Test + fun testDialogNeutralButton() { + val mockDialog: SystemUIDialog = mock() { `when`(context).thenReturn(mContext) } + val factory = PanelConfirmationDialogFactory { mockDialog } + + var response: Boolean? = null + + factory.createConfirmationDialog(context, "") { response = it } + + val captor: ArgumentCaptor<DialogInterface.OnClickListener> = argumentCaptor() + verify(mockDialog).setNeutralButton(eq(R.string.cancel), capture(captor)) + + captor.value.onClick(mockDialog, DialogInterface.BUTTON_NEUTRAL) + + assertThat(response).isFalse() + } + + @Test + fun testDialogCancel() { + val mockDialog: SystemUIDialog = mock() { `when`(context).thenReturn(mContext) } + val factory = PanelConfirmationDialogFactory { mockDialog } + + var response: Boolean? = null + + factory.createConfirmationDialog(context, "") { response = it } + + val captor: ArgumentCaptor<DialogInterface.OnCancelListener> = argumentCaptor() + verify(mockDialog).setOnCancelListener(capture(captor)) + + captor.value.onCancel(mockDialog) + + assertThat(response).isFalse() + } +} 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 edc6882e71c0..ed40c90b2c69 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 @@ -39,8 +39,10 @@ import com.android.systemui.controls.controller.ControlsController import com.android.systemui.controls.controller.StructureInfo import com.android.systemui.controls.management.ControlsListingController import com.android.systemui.controls.management.ControlsProviderSelectorActivity +import com.android.systemui.controls.panels.AuthorizedPanelsRepository import com.android.systemui.controls.settings.FakeControlsSettingsRepository import com.android.systemui.dump.DumpManager +import com.android.systemui.flags.FeatureFlags import com.android.systemui.plugins.ActivityStarter import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker @@ -91,6 +93,8 @@ class ControlsUiControllerImplTest : SysuiTestCase() { @Mock lateinit var userTracker: UserTracker @Mock lateinit var taskViewFactory: TaskViewFactory @Mock lateinit var dumpManager: DumpManager + @Mock lateinit var authorizedPanelsRepository: AuthorizedPanelsRepository + @Mock lateinit var featureFlags: FeatureFlags val sharedPreferences = FakeSharedPreferences() lateinit var controlsSettingsRepository: FakeControlsSettingsRepository @@ -132,6 +136,8 @@ class ControlsUiControllerImplTest : SysuiTestCase() { userTracker, Optional.of(taskViewFactory), controlsSettingsRepository, + authorizedPanelsRepository, + featureFlags, dumpManager ) `when`( @@ -240,7 +246,9 @@ class ControlsUiControllerImplTest : SysuiTestCase() { @Test fun testPanelCallsTaskViewFactoryCreate() { mockLayoutInflater() - val panel = SelectedItem.PanelItem("App name", ComponentName("pkg", "cls")) + val packageName = "pkg" + `when`(authorizedPanelsRepository.getAuthorizedPanels()).thenReturn(setOf(packageName)) + val panel = SelectedItem.PanelItem("App name", ComponentName(packageName, "cls")) val serviceInfo = setUpPanel(panel) underTest.show(parent, {}, context) @@ -258,9 +266,11 @@ class ControlsUiControllerImplTest : SysuiTestCase() { @Test fun testPanelControllerStartActivityWithCorrectArguments() { mockLayoutInflater() + val packageName = "pkg" + `when`(authorizedPanelsRepository.getAuthorizedPanels()).thenReturn(setOf(packageName)) controlsSettingsRepository.setAllowActionOnTrivialControlsInLockscreen(true) - val panel = SelectedItem.PanelItem("App name", ComponentName("pkg", "cls")) + val panel = SelectedItem.PanelItem("App name", ComponentName(packageName, "cls")) val serviceInfo = setUpPanel(panel) underTest.show(parent, {}, context) @@ -290,9 +300,11 @@ class ControlsUiControllerImplTest : SysuiTestCase() { @Test fun testPendingIntentExtrasAreModified() { mockLayoutInflater() + val packageName = "pkg" + `when`(authorizedPanelsRepository.getAuthorizedPanels()).thenReturn(setOf(packageName)) controlsSettingsRepository.setAllowActionOnTrivialControlsInLockscreen(true) - val panel = SelectedItem.PanelItem("App name", ComponentName("pkg", "cls")) + val panel = SelectedItem.PanelItem("App name", ComponentName(packageName, "cls")) val serviceInfo = setUpPanel(panel) underTest.show(parent, {}, context) |