diff options
12 files changed, 424 insertions, 34 deletions
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 464ce0333fd1..48f257a7f4c9 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2262,6 +2262,10 @@ 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> + <!-- Shows in a dialog presented to the user to authorize this app removal from a Device + controls panel [CHAR LIMIT=NONE] --> + <string name="controls_panel_remove_app_authorization">Remove controls for <xliff:g example="My app" id="appName">%s</xliff:g>?</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] --> @@ -2302,6 +2306,8 @@ <string name="controls_dialog_title">Add to device controls</string> <!-- Controls dialog add to favorites [CHAR LIMIT=40] --> <string name="controls_dialog_ok">Add</string> + <!-- Controls dialog remove app from a panel [CHAR LIMIT=40] --> + <string name="controls_dialog_remove">Remove</string> <!-- Controls dialog message. Indicates app that suggested this control [CHAR LIMIT=NONE] --> <string name="controls_dialog_message">Suggested by <xliff:g id="app" example="System UI">%s</xliff:g></string> <!-- Controls tile secondary label when device is locked and user does not want access to controls from lockscreen [CHAR LIMIT=20] --> @@ -2419,6 +2425,8 @@ <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> + <!-- Controls menu, remove app [CHAR_LIMIT=30] --> + <string name="controls_menu_remove">Remove 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 822190f21da1..3555d0a7e7fb 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt @@ -166,6 +166,19 @@ interface ControlsController : UserAwareController { ) /** + * Removes favorites for a given component + * @param componentName the name of the service that provides the [Control] + * @return true when favorites is scheduled for deletion + */ + fun removeFavorites(componentName: ComponentName): Boolean + + /** + * Checks if the favorites can be removed. You can't remove components from the preferred list. + * @param componentName the name of the service that provides the [Control] + */ + fun canRemoveFavorites(componentName: ComponentName): Boolean + + /** * Replaces the favorites for the given structure. * * Calling this method will eliminate the previous selection of favorites and replace it with a 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 278ee7079720..854790360f6a 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt @@ -497,6 +497,21 @@ class ControlsControllerImpl @Inject constructor ( } } + override fun canRemoveFavorites(componentName: ComponentName): Boolean = + !authorizedPanelsRepository.getPreferredPackages().contains(componentName.packageName) + + override fun removeFavorites(componentName: ComponentName): Boolean { + if (!confirmAvailability()) return false + if (!canRemoveFavorites(componentName)) return false + + executor.execute { + Favorites.removeStructures(componentName) + authorizedPanelsRepository.removeAuthorizedPanels(setOf(componentName.packageName)) + persistenceWrapper.storeFavorites(Favorites.getAllStructures()) + } + return true + } + override fun replaceFavoritesForStructure(structureInfo: StructureInfo) { if (!confirmAvailability()) return executor.execute { @@ -655,10 +670,11 @@ private object Favorites { return true } - fun removeStructures(componentName: ComponentName) { + fun removeStructures(componentName: ComponentName): Boolean { val newFavMap = favMap.toMutableMap() - newFavMap.remove(componentName) + val removed = newFavMap.remove(componentName) != null favMap = newFavMap + return removed } fun addFavorite( 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 3e672f391e81..ae9c37aa0e7b 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepository.kt @@ -26,6 +26,14 @@ interface AuthorizedPanelsRepository { /** A set of package names that the user has previously authorized to show panels. */ fun getAuthorizedPanels(): Set<String> + /** Preferred applications to query controls suggestions from */ + fun getPreferredPackages(): Set<String> + /** Adds [packageNames] to the set of packages that the user has authorized to show panels. */ fun addAuthorizedPanels(packageNames: Set<String>) + + /** + * Removes [packageNames] from the set of packages that the user has authorized to show panels. + */ + fun removeAuthorizedPanels(packageNames: Set<String>) } diff --git a/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImpl.kt index f7e43a77b573..e51e8326c0a5 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImpl.kt @@ -37,10 +37,20 @@ constructor( return getAuthorizedPanelsInternal(instantiateSharedPrefs()) } + override fun getPreferredPackages(): Set<String> = + context.resources.getStringArray(R.array.config_controlsPreferredPackages).toSet() + override fun addAuthorizedPanels(packageNames: Set<String>) { addAuthorizedPanelsInternal(instantiateSharedPrefs(), packageNames) } + override fun removeAuthorizedPanels(packageNames: Set<String>) { + with(instantiateSharedPrefs()) { + val currentSet = getAuthorizedPanelsInternal(this) + edit().putStringSet(KEY, currentSet - packageNames).apply() + } + } + private fun getAuthorizedPanelsInternal(sharedPreferences: SharedPreferences): Set<String> { return sharedPreferences.getStringSet(KEY, emptySet())!! } @@ -63,15 +73,7 @@ constructor( // 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() + sharedPref.edit().putStringSet(KEY, getPreferredPackages()).apply() } return sharedPref } diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsDialogsFactory.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsDialogsFactory.kt new file mode 100644 index 000000000000..d6cfb79101d7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsDialogsFactory.kt @@ -0,0 +1,49 @@ +/* + * 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.ui + +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 + +class ControlsDialogsFactory(private val internalDialogFactory: (Context) -> SystemUIDialog) { + + @Inject constructor() : this({ SystemUIDialog(it) }) + + fun createRemoveAppDialog( + context: Context, + appName: CharSequence, + response: Consumer<Boolean> + ): Dialog { + val listener = + DialogInterface.OnClickListener { _, which -> + response.accept(which == DialogInterface.BUTTON_POSITIVE) + } + return internalDialogFactory(context).apply { + setTitle(context.getString(R.string.controls_panel_remove_app_authorization, appName)) + setCanceledOnTouchOutside(true) + setOnCancelListener { response.accept(false) } + setPositiveButton(R.string.controls_dialog_remove, listener) + setNeutralButton(R.string.cancel, listener) + } + } +} 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 860601bfa290..c61dad6fc075 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt @@ -21,6 +21,7 @@ import android.animation.AnimatorListenerAdapter import android.animation.ObjectAnimator import android.app.Activity import android.app.ActivityOptions +import android.app.Dialog import android.app.PendingIntent import android.content.ComponentName import android.content.Context @@ -52,7 +53,6 @@ import com.android.systemui.Dumpable import com.android.systemui.R import com.android.systemui.controls.ControlsMetricsLogger import com.android.systemui.controls.ControlsServiceInfo -import com.android.systemui.controls.settings.ControlsSettingsRepository import com.android.systemui.controls.CustomIconCache import com.android.systemui.controls.controller.ControlsController import com.android.systemui.controls.controller.StructureInfo @@ -64,6 +64,7 @@ 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.controls.settings.ControlsSettingsRepository import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main @@ -83,7 +84,7 @@ import com.android.wm.shell.TaskViewFactory import dagger.Lazy import java.io.PrintWriter import java.text.Collator -import java.util.Optional +import java.util.* import java.util.function.Consumer import javax.inject.Inject @@ -108,6 +109,7 @@ class ControlsUiControllerImpl @Inject constructor ( private val controlsSettingsRepository: ControlsSettingsRepository, private val authorizedPanelsRepository: AuthorizedPanelsRepository, private val featureFlags: FeatureFlags, + private val dialogsFactory: ControlsDialogsFactory, dumpManager: DumpManager ) : ControlsUiController, Dumpable { @@ -122,6 +124,7 @@ class ControlsUiControllerImpl @Inject constructor ( private const val ADD_CONTROLS_ID = 1L private const val ADD_APP_ID = 2L private const val EDIT_CONTROLS_ID = 3L + private const val REMOVE_APP_ID = 4L } private var selectedItem: SelectedItem = SelectedItem.EMPTY_SELECTION @@ -151,6 +154,7 @@ class ControlsUiControllerImpl @Inject constructor ( private var openAppIntent: Intent? = null private var overflowMenuAdapter: BaseAdapter? = null + private var removeAppDialog: Dialog? = null private val onSeedingComplete = Consumer<Boolean> { accepted -> @@ -330,6 +334,31 @@ class ControlsUiControllerImpl @Inject constructor ( } } + @VisibleForTesting + internal fun startRemovingApp(componentName: ComponentName, appName: CharSequence) { + removeAppDialog?.cancel() + removeAppDialog = dialogsFactory.createRemoveAppDialog(context, appName) { + if (!controlsController.get().removeFavorites(componentName)) { + return@createRemoveAppDialog + } + if ( + sharedPreferences.getString(PREF_COMPONENT, "") == + componentName.flattenToString() + ) { + sharedPreferences + .edit() + .remove(PREF_COMPONENT) + .remove(PREF_STRUCTURE_OR_APP_NAME) + .remove(PREF_IS_PANEL) + .commit() + } + + allStructures = controlsController.get().getFavorites() + selectedItem = getPreferredSelectedItem(allStructures) + reload(parent) + }.apply { show() } + } + private fun startTargetedActivity(si: StructureInfo, klazz: Class<*>) { val i = Intent(activityContext, klazz) putIntentExtras(i, si) @@ -433,7 +462,10 @@ class ControlsUiControllerImpl @Inject constructor ( val currentApps = panelsAndStructures.map { it.componentName }.toSet() val allApps = controlsListingController.get() .getCurrentServices().map { it.componentName }.toSet() - createMenu(extraApps = (allApps - currentApps).isNotEmpty()) + createMenu( + selectionItem = selectionItem, + extraApps = (allApps - currentApps).isNotEmpty(), + ) } private fun createPanelView(componentName: ComponentName) { @@ -472,7 +504,7 @@ class ControlsUiControllerImpl @Inject constructor ( } } - private fun createMenu(extraApps: Boolean) { + private fun createMenu(selectionItem: SelectionItem, extraApps: Boolean) { val isPanel = selectedItem is SelectedItem.PanelItem val selectedStructure = (selectedItem as? SelectedItem.StructureItem)?.structure ?: EMPTY_STRUCTURE @@ -490,6 +522,13 @@ class ControlsUiControllerImpl @Inject constructor ( ADD_APP_ID )) } + if (featureFlags.isEnabled(Flags.APP_PANELS_REMOVE_APPS_ALLOWED) && + controlsController.get().canRemoveFavorites(selectedItem.componentName)) { + add(OverflowMenuAdapter.MenuItem( + context.getText(R.string.controls_menu_remove), + REMOVE_APP_ID, + )) + } } else { add(OverflowMenuAdapter.MenuItem( context.getText(R.string.controls_menu_add), @@ -529,6 +568,9 @@ class ControlsUiControllerImpl @Inject constructor ( ADD_APP_ID -> startProviderSelectorActivity() ADD_CONTROLS_ID -> startFavoritingActivity(selectedStructure) EDIT_CONTROLS_ID -> startEditingActivity(selectedStructure) + REMOVE_APP_ID -> startRemovingApp( + selectedStructure.componentName, selectionItem.appName + ) } dismiss() } @@ -732,6 +774,7 @@ class ControlsUiControllerImpl @Inject constructor ( it.value.dismiss() } controlActionCoordinator.closeDialogs() + removeAppDialog?.cancel() } override fun hide(parent: ViewGroup) { 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 e35b2a384bd0..28e80057a672 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 @@ -39,11 +39,9 @@ import com.android.systemui.dump.DumpManager import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat -import java.io.File -import java.util.Optional -import java.util.function.Consumer import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse @@ -58,7 +56,9 @@ import org.mockito.ArgumentMatchers.anyString import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito +import org.mockito.Mockito.`when` import org.mockito.Mockito.anyInt +import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.inOrder import org.mockito.Mockito.mock import org.mockito.Mockito.never @@ -66,9 +66,10 @@ import org.mockito.Mockito.reset 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 +import java.io.File +import java.util.* +import java.util.function.Consumer @SmallTest @RunWith(AndroidTestingRunner::class) @@ -146,6 +147,7 @@ class ControlsControllerImplTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) + whenever(authorizedPanelsRepository.getAuthorizedPanels()).thenReturn(setOf()) `when`(userTracker.userHandle).thenReturn(UserHandle.of(user)) delayableExecutor = FakeExecutor(FakeSystemClock()) @@ -945,6 +947,28 @@ class ControlsControllerImplTest : SysuiTestCase() { controller.bindComponentForPanel(TEST_COMPONENT) verify(bindingController).bindServiceForPanel(TEST_COMPONENT) } + + @Test + fun testRemoveFavoriteRemovesFavorite() { + val componentName = ComponentName(context, "test.Cls") + controller.addFavorite( + componentName, + "test structure", + ControlInfo( + controlId = "testId", + controlTitle = "Test Control", + controlSubtitle = "test control subtitle", + deviceType = DeviceTypes.TYPE_LIGHT, + ), + ) + + controller.removeFavorites(componentName) + delayableExecutor.runAllReady() + + verify(authorizedPanelsRepository) + .removeAuthorizedPanels(eq(setOf(componentName.packageName))) + assertThat(controller.getFavorites()).isEmpty() + } } private class DidRunRunnable() : Runnable { 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 index b91a3fd4b28c..7ac1953ee495 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImplTest.kt @@ -115,6 +115,18 @@ class AuthorizedPanelsRepositoryImplTest : SysuiTestCase() { assertThat(sharedPrefs.getStringSet(KEY, null)).containsExactly(TEST_PACKAGE) } + @Test + fun testRemoveAuthorizedPackageRemovesIt() { + val sharedPrefs = FakeSharedPreferences() + val fileManager = FakeUserFileManager(mapOf(0 to sharedPrefs)) + val repository = createRepository(fileManager) + repository.addAuthorizedPanels(setOf(TEST_PACKAGE)) + + repository.removeAuthorizedPanels(setOf(TEST_PACKAGE)) + + assertThat(sharedPrefs.getStringSet(KEY, null)).isEmpty() + } + private fun createRepository(userFileManager: UserFileManager): AuthorizedPanelsRepositoryImpl { return AuthorizedPanelsRepositoryImpl(mContext, userFileManager, userTracker) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsDialogsFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsDialogsFactoryTest.kt new file mode 100644 index 000000000000..1e8cd4117688 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsDialogsFactoryTest.kt @@ -0,0 +1,89 @@ +/* + * 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.ui + +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.util.FakeSystemUIDialogController +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.eq +import org.mockito.Mockito.verify + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class ControlsDialogsFactoryTest : SysuiTestCase() { + + private companion object { + const val APP_NAME = "Test App" + } + + private val fakeDialogController = FakeSystemUIDialogController() + + private lateinit var underTest: ControlsDialogsFactory + + @Before + fun setup() { + underTest = ControlsDialogsFactory { fakeDialogController.dialog } + } + + @Test + fun testCreatesRemoveAppDialog() { + val dialog = underTest.createRemoveAppDialog(context, APP_NAME) {} + + verify(dialog) + .setTitle( + eq(context.getString(R.string.controls_panel_remove_app_authorization, APP_NAME)) + ) + verify(dialog).setCanceledOnTouchOutside(eq(true)) + } + + @Test + fun testPositiveClickRemoveAppDialogWorks() { + var dialogResult: Boolean? = null + underTest.createRemoveAppDialog(context, APP_NAME) { dialogResult = it } + + fakeDialogController.clickPositive() + + assertThat(dialogResult).isTrue() + } + + @Test + fun testNeutralClickRemoveAppDialogWorks() { + var dialogResult: Boolean? = null + underTest.createRemoveAppDialog(context, APP_NAME) { dialogResult = it } + + fakeDialogController.clickNeutral() + + assertThat(dialogResult).isFalse() + } + + @Test + fun testCancelRemoveAppDialogWorks() { + var dialogResult: Boolean? = null + underTest.createRemoveAppDialog(context, APP_NAME) { dialogResult = it } + + fakeDialogController.cancel() + + assertThat(dialogResult).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 aa90e2a45f10..23faa99c0b9d 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 @@ -52,6 +52,7 @@ import com.android.systemui.shade.ShadeController import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.FakeSharedPreferences +import com.android.systemui.util.FakeSystemUIDialogController import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor @@ -63,21 +64,20 @@ import com.android.systemui.util.time.FakeSystemClock import com.android.wm.shell.TaskView import com.android.wm.shell.TaskViewFactory import com.google.common.truth.Truth.assertThat -import dagger.Lazy -import java.util.Optional -import java.util.function.Consumer import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock +import org.mockito.Mockito.`when` import org.mockito.Mockito.anyInt import org.mockito.Mockito.anyString import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.never import org.mockito.Mockito.spy import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations +import java.util.Optional +import java.util.function.Consumer @SmallTest @RunWith(AndroidTestingRunner::class) @@ -98,13 +98,15 @@ class ControlsUiControllerImplTest : SysuiTestCase() { @Mock lateinit var authorizedPanelsRepository: AuthorizedPanelsRepository @Mock lateinit var featureFlags: FeatureFlags @Mock lateinit var packageManager: PackageManager - val sharedPreferences = FakeSharedPreferences() - lateinit var controlsSettingsRepository: FakeControlsSettingsRepository - var uiExecutor = FakeExecutor(FakeSystemClock()) - var bgExecutor = FakeExecutor(FakeSystemClock()) - lateinit var underTest: ControlsUiControllerImpl - lateinit var parent: FrameLayout + private val sharedPreferences = FakeSharedPreferences() + private val fakeDialogController = FakeSystemUIDialogController() + private val uiExecutor = FakeExecutor(FakeSystemClock()) + private val bgExecutor = FakeExecutor(FakeSystemClock()) + + private lateinit var controlsSettingsRepository: FakeControlsSettingsRepository + private lateinit var parent: FrameLayout + private lateinit var underTest: ControlsUiControllerImpl @Before fun setup() { @@ -125,12 +127,12 @@ class ControlsUiControllerImplTest : SysuiTestCase() { underTest = ControlsUiControllerImpl( - Lazy { controlsController }, + { controlsController }, context, packageManager, uiExecutor, bgExecutor, - Lazy { controlsListingController }, + { controlsListingController }, controlActionCoordinator, activityStarter, iconCache, @@ -142,7 +144,8 @@ class ControlsUiControllerImplTest : SysuiTestCase() { controlsSettingsRepository, authorizedPanelsRepository, featureFlags, - dumpManager + ControlsDialogsFactory { fakeDialogController.dialog }, + dumpManager, ) `when`( userFileManager.getSharedPreferences( @@ -410,8 +413,45 @@ class ControlsUiControllerImplTest : SysuiTestCase() { verify(controlsListingController, never()).removeCallback(any()) } + @Test + fun testRemovingAppsRemovesFavorite() { + val componentName = ComponentName(context, "cls") + whenever(controlsController.removeFavorites(eq(componentName))).thenReturn(true) + val panel = SelectedItem.PanelItem("App name", componentName) + sharedPreferences + .edit() + .putString("controls_component", panel.componentName.flattenToString()) + .putString("controls_structure", panel.appName.toString()) + .putBoolean("controls_is_panel", true) + .commit() + underTest.show(parent, {}, context) + underTest.startRemovingApp(componentName, "Test App") + + fakeDialogController.clickPositive() + + verify(controlsController).removeFavorites(eq(componentName)) + assertThat(underTest.getPreferredSelectedItem(emptyList())) + .isEqualTo(SelectedItem.EMPTY_SELECTION) + with(sharedPreferences) { + assertThat(contains("controls_component")).isFalse() + assertThat(contains("controls_structure")).isFalse() + assertThat(contains("controls_is_panel")).isFalse() + } + } + + @Test + fun testHideCancelsTheRemoveAppDialog() { + val componentName = ComponentName(context, "cls") + underTest.show(parent, {}, context) + underTest.startRemovingApp(componentName, "Test App") + + underTest.hide(parent) + + verify(fakeDialogController.dialog).cancel() + } + private fun setUpPanel(panel: SelectedItem.PanelItem): ControlsServiceInfo { - val activity = ComponentName("pkg", "activity") + val activity = ComponentName(context, "activity") sharedPreferences .edit() .putString("controls_component", panel.componentName.flattenToString()) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeSystemUIDialogController.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeSystemUIDialogController.kt new file mode 100644 index 000000000000..0c9ce0f145f1 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeSystemUIDialogController.kt @@ -0,0 +1,86 @@ +/* + * 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.util + +import android.content.DialogInterface +import com.android.systemui.statusbar.phone.SystemUIDialog +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import org.mockito.ArgumentCaptor +import org.mockito.Mockito.doAnswer +import org.mockito.Mockito.verify +import org.mockito.stubbing.Stubber + +class FakeSystemUIDialogController { + + val dialog: SystemUIDialog = mock() + + private val clickListeners: MutableMap<Int, DialogInterface.OnClickListener> = mutableMapOf() + + init { + saveListener(DialogInterface.BUTTON_POSITIVE) + .whenever(dialog) + .setPositiveButton(any(), any()) + saveListener(DialogInterface.BUTTON_POSITIVE) + .whenever(dialog) + .setPositiveButton(any(), any(), any()) + + saveListener(DialogInterface.BUTTON_NEGATIVE) + .whenever(dialog) + .setNegativeButton(any(), any()) + saveListener(DialogInterface.BUTTON_NEGATIVE) + .whenever(dialog) + .setNegativeButton(any(), any(), any()) + + saveListener(DialogInterface.BUTTON_NEUTRAL).whenever(dialog).setNeutralButton(any(), any()) + saveListener(DialogInterface.BUTTON_NEUTRAL) + .whenever(dialog) + .setNeutralButton(any(), any(), any()) + } + + fun clickNegative() { + performClick(DialogInterface.BUTTON_NEGATIVE, "This dialog has no negative button") + } + + fun clickPositive() { + performClick(DialogInterface.BUTTON_POSITIVE, "This dialog has no positive button") + } + + fun clickNeutral() { + performClick(DialogInterface.BUTTON_NEUTRAL, "This dialog has no neutral button") + } + + fun cancel() { + val captor = ArgumentCaptor.forClass(DialogInterface.OnCancelListener::class.java) + verify(dialog).setOnCancelListener(captor.capture()) + captor.value.onCancel(dialog) + } + + private fun performClick(which: Int, errorMessage: String) { + clickListeners + .getOrElse(which) { throw IllegalAccessException(errorMessage) } + .onClick(dialog, which) + } + + private fun saveListener(which: Int): Stubber = doAnswer { + val listener = it.getArgument<DialogInterface.OnClickListener>(1) + clickListeners[which] = listener + Unit + } +} |