Merge "Add remove app flow to the home controls panel" into tm-qpr-dev

This commit is contained in:
Anton Potapov
2023-02-27 11:36:03 +00:00
committed by Android (Google) Code Review
12 changed files with 424 additions and 34 deletions

View File

@@ -2262,6 +2262,10 @@
panel (embedded activity) instead of controls rendered by SystemUI [CHAR LIMIT=NONE] --> 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> <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] --> <!-- a11y state description for a control that is currently favorited [CHAR LIMIT=NONE] -->
<string name="accessibility_control_favorite">Favorited</string> <string name="accessibility_control_favorite">Favorited</string>
<!-- a11y state description for a control that is currently favorited with its position [CHAR LIMIT=NONE] --> <!-- 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> <string name="controls_dialog_title">Add to device controls</string>
<!-- Controls dialog add to favorites [CHAR LIMIT=40] --> <!-- Controls dialog add to favorites [CHAR LIMIT=40] -->
<string name="controls_dialog_ok">Add</string> <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] --> <!-- 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> <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] --> <!-- 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> <string name="controls_menu_edit">Edit controls</string>
<!-- Controls menu, add another app [CHAR LIMIT=30] --> <!-- Controls menu, add another app [CHAR LIMIT=30] -->
<string name="controls_menu_add_another_app">Add app</string> <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] --> <!-- Title for the media output dialog with media related devices [CHAR LIMIT=50] -->
<string name="media_output_dialog_add_output">Add outputs</string> <string name="media_output_dialog_add_output">Add outputs</string>

View File

@@ -165,6 +165,19 @@ interface ControlsController : UserAwareController {
controlInfo: ControlInfo controlInfo: ControlInfo
) )
/**
* 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. * Replaces the favorites for the given structure.
* *

View File

@@ -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) { override fun replaceFavoritesForStructure(structureInfo: StructureInfo) {
if (!confirmAvailability()) return if (!confirmAvailability()) return
executor.execute { executor.execute {
@@ -655,10 +670,11 @@ private object Favorites {
return true return true
} }
fun removeStructures(componentName: ComponentName) { fun removeStructures(componentName: ComponentName): Boolean {
val newFavMap = favMap.toMutableMap() val newFavMap = favMap.toMutableMap()
newFavMap.remove(componentName) val removed = newFavMap.remove(componentName) != null
favMap = newFavMap favMap = newFavMap
return removed
} }
fun addFavorite( fun addFavorite(

View File

@@ -26,6 +26,14 @@ interface AuthorizedPanelsRepository {
/** A set of package names that the user has previously authorized to show panels. */ /** A set of package names that the user has previously authorized to show panels. */
fun getAuthorizedPanels(): Set<String> 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. */ /** Adds [packageNames] to the set of packages that the user has authorized to show panels. */
fun addAuthorizedPanels(packageNames: Set<String>) 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>)
} }

View File

@@ -37,10 +37,20 @@ constructor(
return getAuthorizedPanelsInternal(instantiateSharedPrefs()) return getAuthorizedPanelsInternal(instantiateSharedPrefs())
} }
override fun getPreferredPackages(): Set<String> =
context.resources.getStringArray(R.array.config_controlsPreferredPackages).toSet()
override fun addAuthorizedPanels(packageNames: Set<String>) { override fun addAuthorizedPanels(packageNames: Set<String>) {
addAuthorizedPanelsInternal(instantiateSharedPrefs(), packageNames) 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> { private fun getAuthorizedPanelsInternal(sharedPreferences: SharedPreferences): Set<String> {
return sharedPreferences.getStringSet(KEY, emptySet())!! 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 we've never run this (i.e., the key doesn't exist), add the default packages
if (sharedPref.getStringSet(KEY, null) == null) { if (sharedPref.getStringSet(KEY, null) == null) {
sharedPref sharedPref.edit().putStringSet(KEY, getPreferredPackages()).apply()
.edit()
.putStringSet(
KEY,
context.resources
.getStringArray(R.array.config_controlsPreferredPackages)
.toSet()
)
.apply()
} }
return sharedPref return sharedPref
} }

View File

@@ -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)
}
}
}

View File

@@ -21,6 +21,7 @@ import android.animation.AnimatorListenerAdapter
import android.animation.ObjectAnimator import android.animation.ObjectAnimator
import android.app.Activity import android.app.Activity
import android.app.ActivityOptions import android.app.ActivityOptions
import android.app.Dialog
import android.app.PendingIntent import android.app.PendingIntent
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
@@ -52,7 +53,6 @@ import com.android.systemui.Dumpable
import com.android.systemui.R import com.android.systemui.R
import com.android.systemui.controls.ControlsMetricsLogger import com.android.systemui.controls.ControlsMetricsLogger
import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.controls.ControlsServiceInfo
import com.android.systemui.controls.settings.ControlsSettingsRepository
import com.android.systemui.controls.CustomIconCache import com.android.systemui.controls.CustomIconCache
import com.android.systemui.controls.controller.ControlsController import com.android.systemui.controls.controller.ControlsController
import com.android.systemui.controls.controller.StructureInfo 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.ControlsListingController
import com.android.systemui.controls.management.ControlsProviderSelectorActivity import com.android.systemui.controls.management.ControlsProviderSelectorActivity
import com.android.systemui.controls.panels.AuthorizedPanelsRepository 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.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dagger.qualifiers.Main
@@ -83,7 +84,7 @@ import com.android.wm.shell.TaskViewFactory
import dagger.Lazy import dagger.Lazy
import java.io.PrintWriter import java.io.PrintWriter
import java.text.Collator import java.text.Collator
import java.util.Optional import java.util.*
import java.util.function.Consumer import java.util.function.Consumer
import javax.inject.Inject import javax.inject.Inject
@@ -108,6 +109,7 @@ class ControlsUiControllerImpl @Inject constructor (
private val controlsSettingsRepository: ControlsSettingsRepository, private val controlsSettingsRepository: ControlsSettingsRepository,
private val authorizedPanelsRepository: AuthorizedPanelsRepository, private val authorizedPanelsRepository: AuthorizedPanelsRepository,
private val featureFlags: FeatureFlags, private val featureFlags: FeatureFlags,
private val dialogsFactory: ControlsDialogsFactory,
dumpManager: DumpManager dumpManager: DumpManager
) : ControlsUiController, Dumpable { ) : ControlsUiController, Dumpable {
@@ -122,6 +124,7 @@ class ControlsUiControllerImpl @Inject constructor (
private const val ADD_CONTROLS_ID = 1L private const val ADD_CONTROLS_ID = 1L
private const val ADD_APP_ID = 2L private const val ADD_APP_ID = 2L
private const val EDIT_CONTROLS_ID = 3L private const val EDIT_CONTROLS_ID = 3L
private const val REMOVE_APP_ID = 4L
} }
private var selectedItem: SelectedItem = SelectedItem.EMPTY_SELECTION private var selectedItem: SelectedItem = SelectedItem.EMPTY_SELECTION
@@ -151,6 +154,7 @@ class ControlsUiControllerImpl @Inject constructor (
private var openAppIntent: Intent? = null private var openAppIntent: Intent? = null
private var overflowMenuAdapter: BaseAdapter? = null private var overflowMenuAdapter: BaseAdapter? = null
private var removeAppDialog: Dialog? = null
private val onSeedingComplete = Consumer<Boolean> { private val onSeedingComplete = Consumer<Boolean> {
accepted -> 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<*>) { private fun startTargetedActivity(si: StructureInfo, klazz: Class<*>) {
val i = Intent(activityContext, klazz) val i = Intent(activityContext, klazz)
putIntentExtras(i, si) putIntentExtras(i, si)
@@ -433,7 +462,10 @@ class ControlsUiControllerImpl @Inject constructor (
val currentApps = panelsAndStructures.map { it.componentName }.toSet() val currentApps = panelsAndStructures.map { it.componentName }.toSet()
val allApps = controlsListingController.get() val allApps = controlsListingController.get()
.getCurrentServices().map { it.componentName }.toSet() .getCurrentServices().map { it.componentName }.toSet()
createMenu(extraApps = (allApps - currentApps).isNotEmpty()) createMenu(
selectionItem = selectionItem,
extraApps = (allApps - currentApps).isNotEmpty(),
)
} }
private fun createPanelView(componentName: ComponentName) { 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 isPanel = selectedItem is SelectedItem.PanelItem
val selectedStructure = (selectedItem as? SelectedItem.StructureItem)?.structure val selectedStructure = (selectedItem as? SelectedItem.StructureItem)?.structure
?: EMPTY_STRUCTURE ?: EMPTY_STRUCTURE
@@ -490,6 +522,13 @@ class ControlsUiControllerImpl @Inject constructor (
ADD_APP_ID 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 { } else {
add(OverflowMenuAdapter.MenuItem( add(OverflowMenuAdapter.MenuItem(
context.getText(R.string.controls_menu_add), context.getText(R.string.controls_menu_add),
@@ -529,6 +568,9 @@ class ControlsUiControllerImpl @Inject constructor (
ADD_APP_ID -> startProviderSelectorActivity() ADD_APP_ID -> startProviderSelectorActivity()
ADD_CONTROLS_ID -> startFavoritingActivity(selectedStructure) ADD_CONTROLS_ID -> startFavoritingActivity(selectedStructure)
EDIT_CONTROLS_ID -> startEditingActivity(selectedStructure) EDIT_CONTROLS_ID -> startEditingActivity(selectedStructure)
REMOVE_APP_ID -> startRemovingApp(
selectedStructure.componentName, selectionItem.appName
)
} }
dismiss() dismiss()
} }
@@ -732,6 +774,7 @@ class ControlsUiControllerImpl @Inject constructor (
it.value.dismiss() it.value.dismiss()
} }
controlActionCoordinator.closeDialogs() controlActionCoordinator.closeDialogs()
removeAppDialog?.cancel()
} }
override fun hide(parent: ViewGroup) { override fun hide(parent: ViewGroup) {

View File

@@ -39,11 +39,9 @@ import com.android.systemui.dump.DumpManager
import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserFileManager
import com.android.systemui.settings.UserTracker import com.android.systemui.settings.UserTracker
import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat 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.After
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse import org.junit.Assert.assertFalse
@@ -58,7 +56,9 @@ import org.mockito.ArgumentMatchers.anyString
import org.mockito.Captor import org.mockito.Captor
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito import org.mockito.Mockito
import org.mockito.Mockito.`when`
import org.mockito.Mockito.anyInt import org.mockito.Mockito.anyInt
import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.inOrder import org.mockito.Mockito.inOrder
import org.mockito.Mockito.mock import org.mockito.Mockito.mock
import org.mockito.Mockito.never import org.mockito.Mockito.never
@@ -66,9 +66,10 @@ import org.mockito.Mockito.reset
import org.mockito.Mockito.times import org.mockito.Mockito.times
import org.mockito.Mockito.verify import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.Mockito.`when`
import org.mockito.Mockito.clearInvocations
import org.mockito.MockitoAnnotations import org.mockito.MockitoAnnotations
import java.io.File
import java.util.*
import java.util.function.Consumer
@SmallTest @SmallTest
@RunWith(AndroidTestingRunner::class) @RunWith(AndroidTestingRunner::class)
@@ -146,6 +147,7 @@ class ControlsControllerImplTest : SysuiTestCase() {
fun setUp() { fun setUp() {
MockitoAnnotations.initMocks(this) MockitoAnnotations.initMocks(this)
whenever(authorizedPanelsRepository.getAuthorizedPanels()).thenReturn(setOf())
`when`(userTracker.userHandle).thenReturn(UserHandle.of(user)) `when`(userTracker.userHandle).thenReturn(UserHandle.of(user))
delayableExecutor = FakeExecutor(FakeSystemClock()) delayableExecutor = FakeExecutor(FakeSystemClock())
@@ -945,6 +947,28 @@ class ControlsControllerImplTest : SysuiTestCase() {
controller.bindComponentForPanel(TEST_COMPONENT) controller.bindComponentForPanel(TEST_COMPONENT)
verify(bindingController).bindServiceForPanel(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 { private class DidRunRunnable() : Runnable {

View File

@@ -115,6 +115,18 @@ class AuthorizedPanelsRepositoryImplTest : SysuiTestCase() {
assertThat(sharedPrefs.getStringSet(KEY, null)).containsExactly(TEST_PACKAGE) 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 { private fun createRepository(userFileManager: UserFileManager): AuthorizedPanelsRepositoryImpl {
return AuthorizedPanelsRepositoryImpl(mContext, userFileManager, userTracker) return AuthorizedPanelsRepositoryImpl(mContext, userFileManager, userTracker)
} }

View File

@@ -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()
}
}

View File

@@ -52,6 +52,7 @@ import com.android.systemui.shade.ShadeController
import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl
import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.FakeSharedPreferences import com.android.systemui.util.FakeSharedPreferences
import com.android.systemui.util.FakeSystemUIDialogController
import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor 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.TaskView
import com.android.wm.shell.TaskViewFactory import com.android.wm.shell.TaskViewFactory
import com.google.common.truth.Truth.assertThat 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.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito.`when`
import org.mockito.Mockito.anyInt import org.mockito.Mockito.anyInt
import org.mockito.Mockito.anyString import org.mockito.Mockito.anyString
import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.never import org.mockito.Mockito.never
import org.mockito.Mockito.spy import org.mockito.Mockito.spy
import org.mockito.Mockito.verify import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations import org.mockito.MockitoAnnotations
import java.util.Optional
import java.util.function.Consumer
@SmallTest @SmallTest
@RunWith(AndroidTestingRunner::class) @RunWith(AndroidTestingRunner::class)
@@ -98,13 +98,15 @@ class ControlsUiControllerImplTest : SysuiTestCase() {
@Mock lateinit var authorizedPanelsRepository: AuthorizedPanelsRepository @Mock lateinit var authorizedPanelsRepository: AuthorizedPanelsRepository
@Mock lateinit var featureFlags: FeatureFlags @Mock lateinit var featureFlags: FeatureFlags
@Mock lateinit var packageManager: PackageManager @Mock lateinit var packageManager: PackageManager
val sharedPreferences = FakeSharedPreferences()
lateinit var controlsSettingsRepository: FakeControlsSettingsRepository
var uiExecutor = FakeExecutor(FakeSystemClock()) private val sharedPreferences = FakeSharedPreferences()
var bgExecutor = FakeExecutor(FakeSystemClock()) private val fakeDialogController = FakeSystemUIDialogController()
lateinit var underTest: ControlsUiControllerImpl private val uiExecutor = FakeExecutor(FakeSystemClock())
lateinit var parent: FrameLayout private val bgExecutor = FakeExecutor(FakeSystemClock())
private lateinit var controlsSettingsRepository: FakeControlsSettingsRepository
private lateinit var parent: FrameLayout
private lateinit var underTest: ControlsUiControllerImpl
@Before @Before
fun setup() { fun setup() {
@@ -125,12 +127,12 @@ class ControlsUiControllerImplTest : SysuiTestCase() {
underTest = underTest =
ControlsUiControllerImpl( ControlsUiControllerImpl(
Lazy { controlsController }, { controlsController },
context, context,
packageManager, packageManager,
uiExecutor, uiExecutor,
bgExecutor, bgExecutor,
Lazy { controlsListingController }, { controlsListingController },
controlActionCoordinator, controlActionCoordinator,
activityStarter, activityStarter,
iconCache, iconCache,
@@ -142,7 +144,8 @@ class ControlsUiControllerImplTest : SysuiTestCase() {
controlsSettingsRepository, controlsSettingsRepository,
authorizedPanelsRepository, authorizedPanelsRepository,
featureFlags, featureFlags,
dumpManager ControlsDialogsFactory { fakeDialogController.dialog },
dumpManager,
) )
`when`( `when`(
userFileManager.getSharedPreferences( userFileManager.getSharedPreferences(
@@ -410,8 +413,45 @@ class ControlsUiControllerImplTest : SysuiTestCase() {
verify(controlsListingController, never()).removeCallback(any()) 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 { private fun setUpPanel(panel: SelectedItem.PanelItem): ControlsServiceInfo {
val activity = ComponentName("pkg", "activity") val activity = ComponentName(context, "activity")
sharedPreferences sharedPreferences
.edit() .edit()
.putString("controls_component", panel.componentName.flattenToString()) .putString("controls_component", panel.componentName.flattenToString())

View File

@@ -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
}
}