From e192a439af9a5025f7be515e39078b584c73461c Mon Sep 17 00:00:00 2001 From: Fabian Kozynski Date: Wed, 11 Jan 2023 14:19:28 -0500 Subject: [PATCH 1/3] Add a persistence mechanism for panels' packages This will allow to persist the packages of apps that provide device controls panels after they are authorized (opted in) by the user. Packages that are in the seeding list (config_controlsPreferredPackages) will be auto-added, but only when creating the set of authorized package for the first time (per user). Test: atest AuthorizedPanelsRepositoryImplTest Fixes: 264414289 Change-Id: Ic23e6172158bba3cf960e81919978ec62da79150 --- .../controls/dagger/ControlsModule.kt | 7 + .../panels/AuthorizedPanelsRepository.kt | 24 +++ .../panels/AuthorizedPanelsRepositoryImpl.kt | 82 ++++++++++ .../AuthorizedPanelsRepositoryImplTest.kt | 145 ++++++++++++++++++ .../systemui/util/FakeSharedPreferences.kt | 2 +- 5 files changed, 259 insertions(+), 1 deletion(-) create mode 100644 packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepository.kt create mode 100644 packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImpl.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImplTest.kt diff --git a/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt index 6d6410de1a91d..6af8e73c8d254 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt @@ -34,6 +34,8 @@ import com.android.systemui.controls.management.ControlsListingController import com.android.systemui.controls.management.ControlsListingControllerImpl import com.android.systemui.controls.management.ControlsProviderSelectorActivity import com.android.systemui.controls.management.ControlsRequestDialog +import com.android.systemui.controls.panels.AuthorizedPanelsRepository +import com.android.systemui.controls.panels.AuthorizedPanelsRepositoryImpl import com.android.systemui.controls.settings.ControlsSettingsDialogManager import com.android.systemui.controls.settings.ControlsSettingsDialogManagerImpl import com.android.systemui.controls.ui.ControlActionCoordinator @@ -104,6 +106,11 @@ abstract class ControlsModule { coordinator: ControlActionCoordinatorImpl ): ControlActionCoordinator + @Binds + abstract fun provideAuthorizedPanelsRepository( + repository: AuthorizedPanelsRepositoryImpl + ): AuthorizedPanelsRepository + @BindsOptionalOf abstract fun optionalPersistenceWrapper(): ControlsFavoritePersistenceWrapper diff --git a/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepository.kt b/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepository.kt new file mode 100644 index 0000000000000..1a6b84a8e0541 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepository.kt @@ -0,0 +1,24 @@ +/* + * 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.panels + +interface AuthorizedPanelsRepository { + fun getAuthorizedPanels(): Set + + fun addAuthorizedPanels(packageNames: Set) +} diff --git a/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImpl.kt new file mode 100644 index 0000000000000..f7e43a77b5739 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImpl.kt @@ -0,0 +1,82 @@ +/* + * 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.panels + +import android.content.Context +import android.content.SharedPreferences +import com.android.systemui.R +import com.android.systemui.settings.UserFileManager +import com.android.systemui.settings.UserTracker +import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl +import javax.inject.Inject + +class AuthorizedPanelsRepositoryImpl +@Inject +constructor( + private val context: Context, + private val userFileManager: UserFileManager, + private val userTracker: UserTracker +) : AuthorizedPanelsRepository { + + override fun getAuthorizedPanels(): Set { + return getAuthorizedPanelsInternal(instantiateSharedPrefs()) + } + + override fun addAuthorizedPanels(packageNames: Set) { + addAuthorizedPanelsInternal(instantiateSharedPrefs(), packageNames) + } + + private fun getAuthorizedPanelsInternal(sharedPreferences: SharedPreferences): Set { + return sharedPreferences.getStringSet(KEY, emptySet())!! + } + + private fun addAuthorizedPanelsInternal( + sharedPreferences: SharedPreferences, + packageNames: Set + ) { + val currentSet = getAuthorizedPanelsInternal(sharedPreferences) + sharedPreferences.edit().putStringSet(KEY, currentSet + packageNames).apply() + } + + private fun instantiateSharedPrefs(): SharedPreferences { + val sharedPref = + userFileManager.getSharedPreferences( + DeviceControlsControllerImpl.PREFS_CONTROLS_FILE, + Context.MODE_PRIVATE, + userTracker.userId, + ) + + // If we've never run this (i.e., the key doesn't exist), add the default packages + if (sharedPref.getStringSet(KEY, null) == null) { + sharedPref + .edit() + .putStringSet( + KEY, + context.resources + .getStringArray(R.array.config_controlsPreferredPackages) + .toSet() + ) + .apply() + } + return sharedPref + } + + companion object { + private const val KEY = "authorized_panels" + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImplTest.kt new file mode 100644 index 0000000000000..b91a3fd4b28cb --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImplTest.kt @@ -0,0 +1,145 @@ +/* + * 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.panels + +import android.content.SharedPreferences +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.settings.UserFileManager +import com.android.systemui.settings.UserTracker +import com.android.systemui.util.FakeSharedPreferences +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import java.io.File +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@RunWith(AndroidTestingRunner::class) +@SmallTest +class AuthorizedPanelsRepositoryImplTest : SysuiTestCase() { + + @Mock private lateinit var userTracker: UserTracker + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + mContext.orCreateTestableResources.addOverride( + R.array.config_controlsPreferredPackages, + arrayOf() + ) + whenever(userTracker.userId).thenReturn(0) + } + + @Test + fun testPreApprovedPackagesAreSeededIfNoSavedPreferences() { + mContext.orCreateTestableResources.addOverride( + R.array.config_controlsPreferredPackages, + arrayOf(TEST_PACKAGE) + ) + val sharedPrefs = FakeSharedPreferences() + val fileManager = FakeUserFileManager(mapOf(0 to sharedPrefs)) + val repository = createRepository(fileManager) + + assertThat(repository.getAuthorizedPanels()).containsExactly(TEST_PACKAGE) + assertThat(sharedPrefs.getStringSet(KEY, null)).containsExactly(TEST_PACKAGE) + } + + @Test + fun testPreApprovedPackagesNotSeededIfEmptySavedPreferences() { + mContext.orCreateTestableResources.addOverride( + R.array.config_controlsPreferredPackages, + arrayOf(TEST_PACKAGE) + ) + val sharedPrefs = FakeSharedPreferences() + sharedPrefs.edit().putStringSet(KEY, emptySet()).apply() + val fileManager = FakeUserFileManager(mapOf(0 to sharedPrefs)) + createRepository(fileManager) + + assertThat(sharedPrefs.getStringSet(KEY, null)).isEmpty() + } + + @Test + fun testPreApprovedPackagesOnlySetForUserThatDoesntHaveThem() { + mContext.orCreateTestableResources.addOverride( + R.array.config_controlsPreferredPackages, + arrayOf(TEST_PACKAGE) + ) + val sharedPrefs_0 = FakeSharedPreferences() + val sharedPrefs_1 = FakeSharedPreferences() + sharedPrefs_1.edit().putStringSet(KEY, emptySet()).apply() + val fileManager = FakeUserFileManager(mapOf(0 to sharedPrefs_0, 1 to sharedPrefs_1)) + val repository = createRepository(fileManager) + + assertThat(repository.getAuthorizedPanels()).containsExactly(TEST_PACKAGE) + whenever(userTracker.userId).thenReturn(1) + assertThat(repository.getAuthorizedPanels()).isEmpty() + } + + @Test + fun testGetAuthorizedPackages() { + val sharedPrefs = FakeSharedPreferences() + sharedPrefs.edit().putStringSet(KEY, mutableSetOf(TEST_PACKAGE)).apply() + val fileManager = FakeUserFileManager(mapOf(0 to sharedPrefs)) + + val repository = createRepository(fileManager) + assertThat(repository.getAuthorizedPanels()).containsExactly(TEST_PACKAGE) + } + + @Test + fun testSetAuthorizedPackage() { + val sharedPrefs = FakeSharedPreferences() + val fileManager = FakeUserFileManager(mapOf(0 to sharedPrefs)) + + val repository = createRepository(fileManager) + repository.addAuthorizedPanels(setOf(TEST_PACKAGE)) + assertThat(sharedPrefs.getStringSet(KEY, null)).containsExactly(TEST_PACKAGE) + } + + private fun createRepository(userFileManager: UserFileManager): AuthorizedPanelsRepositoryImpl { + return AuthorizedPanelsRepositoryImpl(mContext, userFileManager, userTracker) + } + + private class FakeUserFileManager(private val sharedPrefs: Map) : + UserFileManager { + override fun getFile(fileName: String, userId: Int): File { + throw UnsupportedOperationException() + } + + override fun getSharedPreferences( + fileName: String, + mode: Int, + userId: Int + ): SharedPreferences { + if (fileName != FILE_NAME) { + throw IllegalArgumentException("Preference files must be $FILE_NAME") + } + return sharedPrefs.getValue(userId) + } + } + + companion object { + private const val FILE_NAME = "controls_prefs" + private const val KEY = "authorized_panels" + private const val TEST_PACKAGE = "package" + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeSharedPreferences.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeSharedPreferences.kt index 4a881a7ce8987..fd1b8e9ebb7a5 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeSharedPreferences.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeSharedPreferences.kt @@ -41,7 +41,7 @@ class FakeSharedPreferences : SharedPreferences { } override fun getStringSet(key: String, defValues: MutableSet?): MutableSet? { - return data.getOrDefault(key, defValues) as? MutableSet? + return (data.getOrDefault(key, defValues) as? Set?)?.toMutableSet() } override fun getInt(key: String, defValue: Int): Int { From 694d2d571106b424634fffae1431525bb70bb6bb Mon Sep 17 00:00:00 2001 From: Fabian Kozynski Date: Wed, 11 Jan 2023 15:20:26 -0500 Subject: [PATCH 2/3] Flag for new flows Test: no test Bug: 265180342 Change-Id: Ie0175a05a031273f7a9c7de391de7f8cecd5b4ec --- packages/SystemUI/src/com/android/systemui/flags/Flags.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 012615b0c6530..ce98dbf943e0f 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -497,6 +497,10 @@ object Flags { val APP_PANELS_ALL_APPS_ALLOWED = unreleasedFlag(2001, "app_panels_all_apps_allowed", teamfood = true) + @JvmField + val CONTROLS_MANAGEMENT_NEW_FLOWS = + unreleasedFlag(2002, "controls_management_new_flows", teamfood = true) + // 2100 - Falsing Manager @JvmField val FALSING_FOR_LONG_TAPS = releasedFlag(2100, "falsing_for_long_taps") @@ -539,6 +543,5 @@ object Flags { // 2600 - keyboard shortcut // TODO(b/259352579): Tracking Bug - @JvmField - val SHORTCUT_LIST_SEARCH_LAYOUT = unreleasedFlag(2600, "shortcut_list_search_layout") + @JvmField val SHORTCUT_LIST_SEARCH_LAYOUT = unreleasedFlag(2600, "shortcut_list_search_layout") } From eae741388e1c960a76f03f7289bf9c6370d5352e Mon Sep 17 00:00:00 2001 From: Fabian Kozynski Date: Thu, 12 Jan 2023 16:12:00 -0500 Subject: [PATCH 3/3] 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 --- packages/SystemUI/res/values/strings.xml | 10 ++ .../controls/controller/ControlsController.kt | 2 + .../controller/ControlsControllerImpl.kt | 12 ++ .../controls/management/AppAdapter.kt | 35 ++-- .../ControlsProviderSelectorActivity.kt | 50 +++++- .../PanelConfirmationDialogFactory.kt | 60 +++++++ .../panels/AuthorizedPanelsRepository.kt | 7 + .../controls/ui/ControlsUiController.kt | 2 + .../controls/ui/ControlsUiControllerImpl.kt | 32 +++- .../controller/ControlsControllerImplTest.kt | 26 +++ .../controls/management/AppAdapterTest.kt | 110 ++++++++++--- .../ControlsProviderSelectorActivityTest.kt | 155 +++++++++++++++++- .../PanelConfirmationDialogFactoryTest.kt | 106 ++++++++++++ .../ui/ControlsUiControllerImplTest.kt | 18 +- 14 files changed, 566 insertions(+), 59 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/controls/management/PanelConfirmationDialogFactory.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/controls/management/PanelConfirmationDialogFactoryTest.kt diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 066b185c6d1f1..8dcd3b0eb4763 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2238,6 +2238,14 @@ Removed + + Add %s? + + + When you add %s, it can add controls and content to this panel. In some apps, you can choose which controls show up here. + Favorited @@ -2387,6 +2395,8 @@ Add controls Edit controls + + Add app Add outputs 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 f29f6d0dd0cb0..822190f21da1c 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 111fcbbe30be8..1cbfe01a9a1a8 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, 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) { 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 753d5addeb114..3fe0f03488d1a 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 = emptySet(), ) : RecyclerView.Adapter() { private var listOfServices = emptyList() @@ -64,8 +65,10 @@ class AppAdapter( val localeComparator = compareBy(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 90bc5d0f8daad..54587b20c7e13 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(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 0000000000000..6f87aa996f94d --- /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 + ): 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 1a6b84a8e0541..3e672f391e81e 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 + /** Adds [packageNames] to the set of packages that the user has authorized to show panels. */ fun addAuthorizedPanels(packageNames: Set) } 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 f5c5905779a14..c1cec9dd0f943 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): 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 6289788f650a7..966dbf1a96946 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, 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) { + 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 25f471b0d3e01..d54babfbdc0e9 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 @@ -168,6 +172,7 @@ class ControlsControllerImplTest : SysuiTestCase() { listingController, userFileManager, userTracker, + authorizedPanelsRepository, Optional.of(persistenceWrapper), mock(DumpManager::class.java) ) @@ -224,12 +229,33 @@ class ControlsControllerImplTest : SysuiTestCase() { listingController, userFileManager, userTracker, + authorizedPanelsRepository, Optional.of(persistenceWrapper), mock(DumpManager::class.java) ) assertEquals(listOf(TEST_STRUCTURE_INFO), controller_other.getFavorites()) } + @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 765c4c0ac0f07..226ef3b85706c 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() @@ -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 = argumentCaptor() + verify(fakeView).setOnClickListener(capture(listenerCaptor)) + listenerCaptor.value.onClick(fakeView) + + verify(onAppSelected).invoke(info) } } + + private fun createAdapterWithAuthorizedPanels(packages: Set): 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 56c3efe1b8e69..8dfd22378a14d 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> = argumentCaptor() + verify(dialogFactory).createConfirmationDialog(any(), any(), capture(captor)) + + captor.value.accept(true) + + val setCaptor: ArgumentCaptor> = 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> = 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()).`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 0000000000000..756f267671e15 --- /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 = 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 = 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 = 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 edc6882e71c00..ed40c90b2c69f 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)