Merge changes from topics "265180342", "265979516_cts" into tm-qpr-dev

* changes:
  Add new add flow for panels
  Flag for new flows
  Add a persistence mechanism for panels' packages
This commit is contained in:
Fabian Kozynski
2023-01-24 17:08:34 +00:00
committed by Android (Google) Code Review
19 changed files with 829 additions and 60 deletions

View File

@@ -2238,6 +2238,14 @@
<!-- Removed control in management screen [CHAR LIMIT=20] --> <!-- Removed control in management screen [CHAR LIMIT=20] -->
<string name="controls_removed">Removed</string> <string name="controls_removed">Removed</string>
<!-- Title for the dialog presented to the user to authorize this app to display a Device
controls panel (embedded activity) instead of controls rendered by SystemUI [CHAR LIMIT=30] -->
<string name="controls_panel_authorization_title">Add <xliff:g id="appName" example="My app">%s</xliff:g>?</string>
<!-- Shows in a dialog presented to the user to authorize this app to display a Device controls
panel (embedded activity) instead of controls rendered by SystemUI [CHAR LIMIT=NONE] -->
<string name="controls_panel_authorization">When you add <xliff:g id="appName" example="My app">%s</xliff:g>, it can add controls and content to this panel. In some apps, you can choose which controls show up here.</string>
<!-- a11y state description for a control that is currently favorited [CHAR LIMIT=NONE] --> <!-- 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] -->
@@ -2387,6 +2395,8 @@
<string name="controls_menu_add">Add controls</string> <string name="controls_menu_add">Add controls</string>
<!-- Controls menu, edit [CHAR_LIMIT=30] --> <!-- Controls menu, edit [CHAR_LIMIT=30] -->
<string name="controls_menu_edit">Edit controls</string> <string name="controls_menu_edit">Edit controls</string>
<!-- Controls menu, add another app [CHAR LIMIT=30] -->
<string name="controls_menu_add_another_app">Add app</string>
<!-- Title for the media output dialog with media related devices [CHAR LIMIT=50] --> <!-- 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

@@ -188,6 +188,8 @@ interface ControlsController : UserAwareController {
/** See [ControlsUiController.getPreferredSelectedItem]. */ /** See [ControlsUiController.getPreferredSelectedItem]. */
fun getPreferredSelection(): SelectedItem fun getPreferredSelection(): SelectedItem
fun setPreferredSelection(selectedItem: SelectedItem)
/** /**
* Bind to a service that provides a Device Controls panel (embedded activity). This will allow * Bind to a service that provides a Device Controls panel (embedded activity). This will allow
* the app to remain "warm", and reduce latency. * the app to remain "warm", and reduce latency.

View File

@@ -36,6 +36,7 @@ import com.android.systemui.backup.BackupHelper
import com.android.systemui.controls.ControlStatus import com.android.systemui.controls.ControlStatus
import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.controls.ControlsServiceInfo
import com.android.systemui.controls.management.ControlsListingController 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.ControlsUiController
import com.android.systemui.controls.ui.SelectedItem import com.android.systemui.controls.ui.SelectedItem
import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.SysUISingleton
@@ -61,6 +62,7 @@ class ControlsControllerImpl @Inject constructor (
private val listingController: ControlsListingController, private val listingController: ControlsListingController,
private val userFileManager: UserFileManager, private val userFileManager: UserFileManager,
private val userTracker: UserTracker, private val userTracker: UserTracker,
private val authorizedPanelsRepository: AuthorizedPanelsRepository,
optionalWrapper: Optional<ControlsFavoritePersistenceWrapper>, optionalWrapper: Optional<ControlsFavoritePersistenceWrapper>,
dumpManager: DumpManager, dumpManager: DumpManager,
) : Dumpable, ControlsController { ) : Dumpable, ControlsController {
@@ -249,6 +251,11 @@ class ControlsControllerImpl @Inject constructor (
private fun resetFavorites() { private fun resetFavorites() {
Favorites.clear() Favorites.clear()
Favorites.load(persistenceWrapper.readFavorites()) 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 { private fun confirmAvailability(): Boolean {
@@ -489,6 +496,7 @@ class ControlsControllerImpl @Inject constructor (
if (!confirmAvailability()) return if (!confirmAvailability()) return
executor.execute { executor.execute {
if (Favorites.addFavorite(componentName, structureName, controlInfo)) { if (Favorites.addFavorite(componentName, structureName, controlInfo)) {
authorizedPanelsRepository.addAuthorizedPanels(setOf(componentName.packageName))
persistenceWrapper.storeFavorites(Favorites.getAllStructures()) persistenceWrapper.storeFavorites(Favorites.getAllStructures())
} }
} }
@@ -555,6 +563,10 @@ class ControlsControllerImpl @Inject constructor (
return uiController.getPreferredSelectedItem(getFavorites()) return uiController.getPreferredSelectedItem(getFavorites())
} }
override fun setPreferredSelection(selectedItem: SelectedItem) {
uiController.updatePreferences(selectedItem)
}
override fun dump(pw: PrintWriter, args: Array<out String>) { override fun dump(pw: PrintWriter, args: Array<out String>) {
pw.println("ControlsController state:") pw.println("ControlsController state:")
pw.println(" Changing users: $userChanging") pw.println(" Changing users: $userChanging")

View File

@@ -34,6 +34,8 @@ import com.android.systemui.controls.management.ControlsListingController
import com.android.systemui.controls.management.ControlsListingControllerImpl import com.android.systemui.controls.management.ControlsListingControllerImpl
import com.android.systemui.controls.management.ControlsProviderSelectorActivity import com.android.systemui.controls.management.ControlsProviderSelectorActivity
import com.android.systemui.controls.management.ControlsRequestDialog 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.ControlsSettingsDialogManager
import com.android.systemui.controls.settings.ControlsSettingsDialogManagerImpl import com.android.systemui.controls.settings.ControlsSettingsDialogManagerImpl
import com.android.systemui.controls.ui.ControlActionCoordinator import com.android.systemui.controls.ui.ControlActionCoordinator
@@ -104,6 +106,11 @@ abstract class ControlsModule {
coordinator: ControlActionCoordinatorImpl coordinator: ControlActionCoordinatorImpl
): ControlActionCoordinator ): ControlActionCoordinator
@Binds
abstract fun provideAuthorizedPanelsRepository(
repository: AuthorizedPanelsRepositoryImpl
): AuthorizedPanelsRepository
@BindsOptionalOf @BindsOptionalOf
abstract fun optionalPersistenceWrapper(): ControlsFavoritePersistenceWrapper abstract fun optionalPersistenceWrapper(): ControlsFavoritePersistenceWrapper

View File

@@ -45,14 +45,15 @@ import java.util.concurrent.Executor
* @param onAppSelected a callback to indicate that an app has been selected in the list. * @param onAppSelected a callback to indicate that an app has been selected in the list.
*/ */
class AppAdapter( class AppAdapter(
backgroundExecutor: Executor, backgroundExecutor: Executor,
uiExecutor: Executor, uiExecutor: Executor,
lifecycle: Lifecycle, lifecycle: Lifecycle,
controlsListingController: ControlsListingController, controlsListingController: ControlsListingController,
private val layoutInflater: LayoutInflater, private val layoutInflater: LayoutInflater,
private val onAppSelected: (ComponentName?) -> Unit = {}, private val onAppSelected: (ControlsServiceInfo) -> Unit = {},
private val favoritesRenderer: FavoritesRenderer, private val favoritesRenderer: FavoritesRenderer,
private val resources: Resources private val resources: Resources,
private val authorizedPanels: Set<String> = emptySet(),
) : RecyclerView.Adapter<AppAdapter.Holder>() { ) : RecyclerView.Adapter<AppAdapter.Holder>() {
private var listOfServices = emptyList<ControlsServiceInfo>() private var listOfServices = emptyList<ControlsServiceInfo>()
@@ -64,8 +65,10 @@ class AppAdapter(
val localeComparator = compareBy<ControlsServiceInfo, CharSequence>(collator) { val localeComparator = compareBy<ControlsServiceInfo, CharSequence>(collator) {
it.loadLabel() ?: "" it.loadLabel() ?: ""
} }
listOfServices = serviceInfos.filter { it.panelActivity == null } // No panel or the panel is not authorized
.sortedWith(localeComparator) listOfServices = serviceInfos.filter {
it.panelActivity == null || it.panelActivity?.packageName !in authorizedPanels
}.sortedWith(localeComparator)
uiExecutor.execute(::notifyDataSetChanged) uiExecutor.execute(::notifyDataSetChanged)
} }
} }
@@ -86,8 +89,8 @@ class AppAdapter(
override fun onBindViewHolder(holder: Holder, index: Int) { override fun onBindViewHolder(holder: Holder, index: Int) {
holder.bindData(listOfServices[index]) holder.bindData(listOfServices[index])
holder.itemView.setOnClickListener { holder.view.setOnClickListener {
onAppSelected(ComponentName.unflattenFromString(listOfServices[index].key)) onAppSelected(listOfServices[index])
} }
} }
@@ -95,6 +98,8 @@ class AppAdapter(
* Holder for binding views in the [RecyclerView]- * Holder for binding views in the [RecyclerView]-
*/ */
class Holder(view: View, val favRenderer: FavoritesRenderer) : RecyclerView.ViewHolder(view) { 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 icon: ImageView = itemView.requireViewById(com.android.internal.R.id.icon)
private val title: TextView = itemView.requireViewById(com.android.internal.R.id.title) private val title: TextView = itemView.requireViewById(com.android.internal.R.id.title)
private val favorites: TextView = itemView.requireViewById(R.id.favorites) private val favorites: TextView = itemView.requireViewById(R.id.favorites)
@@ -106,7 +111,11 @@ class AppAdapter(
fun bindData(data: ControlsServiceInfo) { fun bindData(data: ControlsServiceInfo) {
icon.setImageDrawable(data.loadIcon()) icon.setImageDrawable(data.loadIcon())
title.text = data.loadLabel() 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.text = text
favorites.visibility = if (text == null) View.GONE else View.VISIBLE favorites.visibility = if (text == null) View.GONE else View.VISIBLE
} }

View File

@@ -17,6 +17,7 @@
package com.android.systemui.controls.management package com.android.systemui.controls.management
import android.app.ActivityOptions import android.app.ActivityOptions
import android.app.Dialog
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@@ -31,12 +32,15 @@ import android.widget.TextView
import android.window.OnBackInvokedCallback import android.window.OnBackInvokedCallback
import android.window.OnBackInvokedDispatcher import android.window.OnBackInvokedDispatcher
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.annotation.VisibleForTesting
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.android.systemui.R import com.android.systemui.R
import com.android.systemui.controls.ControlsServiceInfo
import com.android.systemui.controls.controller.ControlsController 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.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.Background
import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.settings.UserTracker import com.android.systemui.settings.UserTracker
@@ -52,7 +56,8 @@ open class ControlsProviderSelectorActivity @Inject constructor(
private val listingController: ControlsListingController, private val listingController: ControlsListingController,
private val controlsController: ControlsController, private val controlsController: ControlsController,
private val userTracker: UserTracker, private val userTracker: UserTracker,
private val uiController: ControlsUiController private val authorizedPanelsRepository: AuthorizedPanelsRepository,
private val panelConfirmationDialogFactory: PanelConfirmationDialogFactory
) : ComponentActivity() { ) : ComponentActivity() {
companion object { companion object {
@@ -72,6 +77,7 @@ open class ControlsProviderSelectorActivity @Inject constructor(
} }
} }
} }
private var dialog: Dialog? = null
private val mOnBackInvokedCallback = OnBackInvokedCallback { private val mOnBackInvokedCallback = OnBackInvokedCallback {
if (DEBUG) { if (DEBUG) {
@@ -138,9 +144,11 @@ open class ControlsProviderSelectorActivity @Inject constructor(
lifecycle, lifecycle,
listingController, listingController,
LayoutInflater.from(this), LayoutInflater.from(this),
::launchFavoritingActivity, ::onAppSelected,
FavoritesRenderer(resources, controlsController::countFavoritesForComponent), FavoritesRenderer(resources, controlsController::countFavoritesForComponent),
resources).apply { resources,
authorizedPanelsRepository.getAuthorizedPanels()
).apply {
registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
var hasAnimated = false var hasAnimated = false
override fun onChanged() { override fun onChanged() {
@@ -167,13 +175,35 @@ open class ControlsProviderSelectorActivity @Inject constructor(
Log.d(TAG, "Unregistered onBackInvokedCallback") Log.d(TAG, "Unregistered onBackInvokedCallback")
} }
onBackInvokedDispatcher.unregisterOnBackInvokedCallback(mOnBackInvokedCallback) 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. * Launch the [ControlsFavoritingActivity] for the specified component.
* @param component a component name for a [ControlsProviderService] * @param component a component name for a [ControlsProviderService]
*/ */
fun launchFavoritingActivity(component: ComponentName?) { private fun launchFavoritingActivity(component: ComponentName?) {
executor.execute { executor.execute {
component?.let { component?.let {
val intent = Intent(applicationContext, ControlsFavoritingActivity::class.java) val intent = Intent(applicationContext, ControlsFavoritingActivity::class.java)
@@ -194,7 +224,15 @@ open class ControlsProviderSelectorActivity @Inject constructor(
super.onDestroy() super.onDestroy()
} }
private fun animateExitAndFinish() { private fun openControlsOrigin() {
startActivity(
Intent(applicationContext, ControlsActivity::class.java),
ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
)
}
@VisibleForTesting
internal open fun animateExitAndFinish() {
val rootView = requireViewById<ViewGroup>(R.id.controls_management_root) val rootView = requireViewById<ViewGroup>(R.id.controls_management_root)
ControlsAnimations.exitAnimation( ControlsAnimations.exitAnimation(
rootView, rootView,

View File

@@ -0,0 +1,60 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package com.android.systemui.controls.management
import android.app.Dialog
import android.content.Context
import android.content.DialogInterface
import com.android.systemui.R
import com.android.systemui.statusbar.phone.SystemUIDialog
import java.util.function.Consumer
import javax.inject.Inject
/**
* Factory to create dialogs for consenting to show app panels for specific apps.
*
* [internalDialogFactory] is for facilitating testing.
*/
class PanelConfirmationDialogFactory(
private val internalDialogFactory: (Context) -> SystemUIDialog
) {
@Inject constructor() : this({ SystemUIDialog(it) })
/**
* Creates a dialog to show to the user. [response] will be true if an only if the user responds
* affirmatively.
*/
fun createConfirmationDialog(
context: Context,
appName: CharSequence,
response: Consumer<Boolean>
): Dialog {
val listener =
DialogInterface.OnClickListener { _, which ->
response.accept(which == DialogInterface.BUTTON_POSITIVE)
}
return internalDialogFactory(context).apply {
setTitle(this.context.getString(R.string.controls_panel_authorization_title, appName))
setMessage(this.context.getString(R.string.controls_panel_authorization, appName))
setCanceledOnTouchOutside(true)
setOnCancelListener { response.accept(false) }
setPositiveButton(R.string.controls_dialog_ok, listener)
setNeutralButton(R.string.cancel, listener)
}
}
}

View File

@@ -0,0 +1,31 @@
/*
* 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
/**
* Repository for keeping track of which packages the panel has authorized to show control panels
* (embedded activity).
*/
interface AuthorizedPanelsRepository {
/** A set of package names that the user has previously authorized to show panels. */
fun getAuthorizedPanels(): Set<String>
/** Adds [packageNames] to the set of packages that the user has authorized to show panels. */
fun addAuthorizedPanels(packageNames: Set<String>)
}

View File

@@ -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<String> {
return getAuthorizedPanelsInternal(instantiateSharedPrefs())
}
override fun addAuthorizedPanels(packageNames: Set<String>) {
addAuthorizedPanelsInternal(instantiateSharedPrefs(), packageNames)
}
private fun getAuthorizedPanelsInternal(sharedPreferences: SharedPreferences): Set<String> {
return sharedPreferences.getStringSet(KEY, emptySet())!!
}
private fun addAuthorizedPanelsInternal(
sharedPreferences: SharedPreferences,
packageNames: Set<String>
) {
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"
}
}

View File

@@ -58,6 +58,8 @@ interface ControlsUiController {
* This element will be the one that appears when the user first opens the controls activity. * This element will be the one that appears when the user first opens the controls activity.
*/ */
fun getPreferredSelectedItem(structures: List<StructureInfo>): SelectedItem fun getPreferredSelectedItem(structures: List<StructureInfo>): SelectedItem
fun updatePreferences(selectedItem: SelectedItem)
} }
sealed class SelectedItem { sealed class SelectedItem {

View File

@@ -60,10 +60,13 @@ import com.android.systemui.controls.management.ControlsEditingActivity
import com.android.systemui.controls.management.ControlsFavoritingActivity 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.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
import com.android.systemui.dump.DumpManager 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.globalactions.GlobalActionsPopupMenu
import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserFileManager
@@ -99,6 +102,8 @@ class ControlsUiControllerImpl @Inject constructor (
private val userTracker: UserTracker, private val userTracker: UserTracker,
private val taskViewFactory: Optional<TaskViewFactory>, private val taskViewFactory: Optional<TaskViewFactory>,
private val controlsSettingsRepository: ControlsSettingsRepository, private val controlsSettingsRepository: ControlsSettingsRepository,
private val authorizedPanelsRepository: AuthorizedPanelsRepository,
private val featureFlags: FeatureFlags,
dumpManager: DumpManager dumpManager: DumpManager
) : ControlsUiController, Dumpable { ) : ControlsUiController, Dumpable {
@@ -160,6 +165,7 @@ class ControlsUiControllerImpl @Inject constructor (
): ControlsListingController.ControlsListingCallback { ): ControlsListingController.ControlsListingCallback {
return object : ControlsListingController.ControlsListingCallback { return object : ControlsListingController.ControlsListingCallback {
override fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>) { override fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>) {
val authorizedPanels = authorizedPanelsRepository.getAuthorizedPanels()
val lastItems = serviceInfos.map { val lastItems = serviceInfos.map {
val uid = it.serviceInfo.applicationInfo.uid val uid = it.serviceInfo.applicationInfo.uid
@@ -169,7 +175,11 @@ class ControlsUiControllerImpl @Inject constructor (
it.loadIcon(), it.loadIcon(),
it.componentName, it.componentName,
uid, uid,
it.panelActivity if (it.componentName.packageName in authorizedPanels) {
it.panelActivity
} else {
null
}
) )
} }
uiExecutor.execute { uiExecutor.execute {
@@ -417,14 +427,20 @@ class ControlsUiControllerImpl @Inject constructor (
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
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) { val items = if (isPanel) {
arrayOf( arrayOf(
context.resources.getString(R.string.controls_menu_add), context.resources.getString(addControlsId),
) )
} else { } else {
arrayOf( arrayOf(
context.resources.getString(R.string.controls_menu_add), context.resources.getString(addControlsId),
context.resources.getString(R.string.controls_menu_edit) context.resources.getString(R.string.controls_menu_edit)
) )
} }
@@ -449,7 +465,7 @@ class ControlsUiControllerImpl @Inject constructor (
when (pos) { when (pos) {
// 0: Add Control // 0: Add Control
0 -> { 0 -> {
if (isPanel) { if (isPanel || newFlows) {
startProviderSelectorActivity() startProviderSelectorActivity()
} else { } else {
startFavoritingActivity(selectedStructure) startFavoritingActivity(selectedStructure)
@@ -610,11 +626,11 @@ class ControlsUiControllerImpl @Inject constructor (
} }
} }
private fun updatePreferences(si: SelectedItem) { override fun updatePreferences(selectedItem: SelectedItem) {
sharedPreferences.edit() sharedPreferences.edit()
.putString(PREF_COMPONENT, si.componentName.flattenToString()) .putString(PREF_COMPONENT, selectedItem.componentName.flattenToString())
.putString(PREF_STRUCTURE_OR_APP_NAME, si.name.toString()) .putString(PREF_STRUCTURE_OR_APP_NAME, selectedItem.name.toString())
.putBoolean(PREF_IS_PANEL, si is SelectedItem.PanelItem) .putBoolean(PREF_IS_PANEL, selectedItem is SelectedItem.PanelItem)
.commit() .commit()
} }

View File

@@ -530,6 +530,10 @@ object Flags {
val APP_PANELS_ALL_APPS_ALLOWED = val APP_PANELS_ALL_APPS_ALLOWED =
unreleasedFlag(2001, "app_panels_all_apps_allowed", teamfood = true) 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 // 2100 - Falsing Manager
@JvmField val FALSING_FOR_LONG_TAPS = releasedFlag(2100, "falsing_for_long_taps") @JvmField val FALSING_FOR_LONG_TAPS = releasedFlag(2100, "falsing_for_long_taps")

View File

@@ -33,6 +33,7 @@ import com.android.systemui.backup.BackupHelper
import com.android.systemui.controls.ControlStatus import com.android.systemui.controls.ControlStatus
import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.controls.ControlsServiceInfo
import com.android.systemui.controls.management.ControlsListingController 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.ControlsUiController
import com.android.systemui.dump.DumpManager import com.android.systemui.dump.DumpManager
import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserFileManager
@@ -66,6 +67,7 @@ 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.`when`
import org.mockito.Mockito.clearInvocations
import org.mockito.MockitoAnnotations import org.mockito.MockitoAnnotations
@SmallTest @SmallTest
@@ -88,6 +90,8 @@ class ControlsControllerImplTest : SysuiTestCase() {
private lateinit var userTracker: UserTracker private lateinit var userTracker: UserTracker
@Mock @Mock
private lateinit var userFileManager: UserFileManager private lateinit var userFileManager: UserFileManager
@Mock
private lateinit var authorizedPanelsRepository: AuthorizedPanelsRepository
@Captor @Captor
private lateinit var structureInfoCaptor: ArgumentCaptor<StructureInfo> private lateinit var structureInfoCaptor: ArgumentCaptor<StructureInfo>
@@ -168,6 +172,7 @@ class ControlsControllerImplTest : SysuiTestCase() {
listingController, listingController,
userFileManager, userFileManager,
userTracker, userTracker,
authorizedPanelsRepository,
Optional.of(persistenceWrapper), Optional.of(persistenceWrapper),
mock(DumpManager::class.java) mock(DumpManager::class.java)
) )
@@ -224,12 +229,33 @@ class ControlsControllerImplTest : SysuiTestCase() {
listingController, listingController,
userFileManager, userFileManager,
userTracker, userTracker,
authorizedPanelsRepository,
Optional.of(persistenceWrapper), Optional.of(persistenceWrapper),
mock(DumpManager::class.java) mock(DumpManager::class.java)
) )
assertEquals(listOf(TEST_STRUCTURE_INFO), controller_other.getFavorites()) 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 @Test
fun testOnActionResponse() { fun testOnActionResponse() {
controller.onActionResponse(TEST_COMPONENT, TEST_CONTROL_ID, ControlAction.RESPONSE_OK) controller.onActionResponse(TEST_COMPONENT, TEST_CONTROL_ID, ControlAction.RESPONSE_OK)

View File

@@ -21,12 +21,15 @@ import android.content.res.Resources
import android.testing.AndroidTestingRunner import android.testing.AndroidTestingRunner
import android.testing.TestableLooper import android.testing.TestableLooper
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest
import com.android.settingslib.core.lifecycle.Lifecycle import com.android.settingslib.core.lifecycle.Lifecycle
import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestCase
import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.controls.ControlsServiceInfo
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.capture
import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.mock
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
@@ -36,8 +39,10 @@ import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor import org.mockito.ArgumentCaptor
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito.`when` import org.mockito.Mockito.`when`
import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.verify import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations import org.mockito.MockitoAnnotations
import java.text.Collator
@SmallTest @SmallTest
@RunWith(AndroidTestingRunner::class) @RunWith(AndroidTestingRunner::class)
@@ -49,25 +54,18 @@ class AppAdapterTest : SysuiTestCase() {
@Mock lateinit var lifecycle: Lifecycle @Mock lateinit var lifecycle: Lifecycle
@Mock lateinit var controlsListingController: ControlsListingController @Mock lateinit var controlsListingController: ControlsListingController
@Mock lateinit var layoutInflater: LayoutInflater @Mock lateinit var layoutInflater: LayoutInflater
@Mock lateinit var onAppSelected: (ComponentName?) -> Unit @Mock lateinit var onAppSelected: (ControlsServiceInfo) -> Unit
@Mock lateinit var favoritesRenderer: FavoritesRenderer @Mock lateinit var favoritesRenderer: FavoritesRenderer
val resources: Resources = context.resources val resources: Resources = context.resources
lateinit var adapter: AppAdapter lateinit var adapter: AppAdapter
@Before @Before
fun setUp() { fun setUp() {
MockitoAnnotations.initMocks(this) MockitoAnnotations.initMocks(this)
adapter = AppAdapter(backgroundExecutor,
uiExecutor,
lifecycle,
controlsListingController,
layoutInflater,
onAppSelected,
favoritesRenderer,
resources)
} }
@Test @Test
fun testOnServicesUpdated_nullLoadLabel() { fun testOnServicesUpdated_nullLoadLabel() {
adapter = createAdapterWithAuthorizedPanels(emptySet())
val captor = ArgumentCaptor val captor = ArgumentCaptor
.forClass(ControlsListingController.ControlsListingCallback::class.java) .forClass(ControlsListingController.ControlsListingCallback::class.java)
val controlsServiceInfo = mock<ControlsServiceInfo>() val controlsServiceInfo = mock<ControlsServiceInfo>()
@@ -76,14 +74,14 @@ class AppAdapterTest : SysuiTestCase() {
verify(controlsListingController).observe(any(Lifecycle::class.java), captor.capture()) verify(controlsListingController).observe(any(Lifecycle::class.java), captor.capture())
captor.value.onServicesUpdated(serviceInfo) captor.value.onServicesUpdated(serviceInfo)
backgroundExecutor.runAllReady() FakeExecutor.exhaustExecutors(backgroundExecutor, uiExecutor)
uiExecutor.runAllReady()
assertThat(adapter.itemCount).isEqualTo(serviceInfo.size) assertThat(adapter.itemCount).isEqualTo(serviceInfo.size)
} }
@Test @Test
fun testOnServicesUpdatedDoesntHavePanels() { fun testOnServicesUpdated_showsNotAuthorizedPanels() {
adapter = createAdapterWithAuthorizedPanels(emptySet())
val captor = ArgumentCaptor val captor = ArgumentCaptor
.forClass(ControlsListingController.ControlsListingCallback::class.java) .forClass(ControlsListingController.ControlsListingCallback::class.java)
val serviceInfo = listOf( val serviceInfo = listOf(
@@ -93,20 +91,88 @@ class AppAdapterTest : SysuiTestCase() {
verify(controlsListingController).observe(any(Lifecycle::class.java), captor.capture()) verify(controlsListingController).observe(any(Lifecycle::class.java), captor.capture())
captor.value.onServicesUpdated(serviceInfo) captor.value.onServicesUpdated(serviceInfo)
backgroundExecutor.runAllReady() FakeExecutor.exhaustExecutors(backgroundExecutor, uiExecutor)
uiExecutor.runAllReady()
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) assertThat(adapter.itemCount).isEqualTo(1)
} }
fun ControlsServiceInfo( @Test
label: CharSequence, fun testOnBindSetsClickListenerToCallOnAppSelected() {
panelComponentName: ComponentName? = null adapter = createAdapterWithAuthorizedPanels(emptySet())
): ControlsServiceInfo {
return mock { val captor = ArgumentCaptor
`when`(this.loadLabel()).thenReturn(label) .forClass(ControlsListingController.ControlsListingCallback::class.java)
`when`(this.panelActivity).thenReturn(panelComponentName) val serviceInfo = listOf(
`when`(this.loadIcon()).thenReturn(mock()) ControlsServiceInfo("no panel", null),
ControlsServiceInfo("panel", ComponentName(TEST_PACKAGE, "cls"))
)
verify(controlsListingController).observe(any(Lifecycle::class.java), captor.capture())
captor.value.onServicesUpdated(serviceInfo)
FakeExecutor.exhaustExecutors(backgroundExecutor, uiExecutor)
val sorted = serviceInfo.sortedWith(
compareBy(Collator.getInstance(resources.configuration.locales[0])) {
it.loadLabel() ?: ""
})
sorted.forEachIndexed { index, info ->
val fakeView: View = mock()
val fakeHolder: AppAdapter.Holder = mock()
`when`(fakeHolder.view).thenReturn(fakeView)
clearInvocations(onAppSelected)
adapter.onBindViewHolder(fakeHolder, index)
val listenerCaptor: ArgumentCaptor<View.OnClickListener> = argumentCaptor()
verify(fakeView).setOnClickListener(capture(listenerCaptor))
listenerCaptor.value.onClick(fakeView)
verify(onAppSelected).invoke(info)
} }
} }
private fun createAdapterWithAuthorizedPanels(packages: Set<String>): AppAdapter {
return AppAdapter(backgroundExecutor,
uiExecutor,
lifecycle,
controlsListingController,
layoutInflater,
onAppSelected,
favoritesRenderer,
resources,
packages)
}
companion object {
private fun ControlsServiceInfo(
label: CharSequence,
panelComponentName: ComponentName? = null
): ControlsServiceInfo {
return mock {
`when`(loadLabel()).thenReturn(label)
`when`(panelActivity).thenReturn(panelComponentName)
`when`(loadIcon()).thenReturn(mock())
}
}
private const val TEST_PACKAGE = "package"
}
} }

View File

@@ -16,7 +16,13 @@
package com.android.systemui.controls.management package com.android.systemui.controls.management
import android.app.Dialog
import android.content.ComponentName
import android.content.Intent 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.AndroidTestingRunner
import android.testing.TestableLooper import android.testing.TestableLooper
import android.window.OnBackInvokedCallback import android.window.OnBackInvokedCallback
@@ -25,14 +31,23 @@ import androidx.test.filters.SmallTest
import androidx.test.rule.ActivityTestRule import androidx.test.rule.ActivityTestRule
import androidx.test.runner.intercepting.SingleActivityFactory import androidx.test.runner.intercepting.SingleActivityFactory
import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestCase
import com.android.systemui.controls.ControlsServiceInfo
import com.android.systemui.controls.controller.ControlsController 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.Background
import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.settings.UserTracker 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 com.google.common.util.concurrent.MoreExecutors
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
import java.util.concurrent.Executor import java.util.concurrent.Executor
import java.util.function.Consumer
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@@ -41,7 +56,11 @@ import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers import org.mockito.ArgumentMatchers
import org.mockito.Captor import org.mockito.Captor
import org.mockito.Mock 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.verify
import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.MockitoAnnotations import org.mockito.MockitoAnnotations
@SmallTest @SmallTest
@@ -58,9 +77,10 @@ class ControlsProviderSelectorActivityTest : SysuiTestCase() {
@Mock lateinit var userTracker: UserTracker @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) private var latch: CountDownLatch = CountDownLatch(1)
@Mock private lateinit var mockDispatcher: OnBackInvokedDispatcher @Mock private lateinit var mockDispatcher: OnBackInvokedDispatcher
@@ -81,7 +101,8 @@ class ControlsProviderSelectorActivityTest : SysuiTestCase() {
listingController, listingController,
controlsController, controlsController,
userTracker, userTracker,
uiController, authorizedPanelsRepository,
dialogFactory,
mockDispatcher, mockDispatcher,
latch latch
) )
@@ -113,13 +134,99 @@ class ControlsProviderSelectorActivityTest : SysuiTestCase() {
verify(mockDispatcher).unregisterOnBackInvokedCallback(captureCallback.value) verify(mockDispatcher).unregisterOnBackInvokedCallback(captureCallback.value)
} }
public class TestableControlsProviderSelectorActivity( @Test
fun testOnAppSelectedForNonPanelStartsFavoritingActivity() {
val info = ControlsServiceInfo(ComponentName("test_pkg", "service"), "", null)
activityRule.activity.onAppSelected(info)
verifyNoMoreInteractions(dialogFactory)
assertThat(activityRule.activity.lastStartedActivity?.component?.className)
.isEqualTo(ControlsFavoritingActivity::class.java.name)
assertThat(activityRule.activity.triedToFinish).isTrue()
}
@Test
fun testOnAppSelectedForPanelTriggersDialog() {
val label = "label"
val info =
ControlsServiceInfo(
ComponentName("test_pkg", "service"),
label,
ComponentName("test_pkg", "activity")
)
val dialog: Dialog = mock()
whenever(dialogFactory.createConfirmationDialog(any(), any(), any())).thenReturn(dialog)
activityRule.activity.onAppSelected(info)
verify(dialogFactory).createConfirmationDialog(any(), eq(label), any())
verify(dialog).show()
assertThat(activityRule.activity.triedToFinish).isFalse()
}
@Test
fun dialogAcceptAddsPackage() {
val label = "label"
val info =
ControlsServiceInfo(
ComponentName("test_pkg", "service"),
label,
ComponentName("test_pkg", "activity")
)
val dialog: Dialog = mock()
whenever(dialogFactory.createConfirmationDialog(any(), any(), any())).thenReturn(dialog)
activityRule.activity.onAppSelected(info)
val captor: ArgumentCaptor<Consumer<Boolean>> = argumentCaptor()
verify(dialogFactory).createConfirmationDialog(any(), any(), capture(captor))
captor.value.accept(true)
val setCaptor: ArgumentCaptor<Set<String>> = argumentCaptor()
verify(authorizedPanelsRepository).addAuthorizedPanels(capture(setCaptor))
assertThat(setCaptor.value).containsExactly(info.componentName.packageName)
assertThat(activityRule.activity.triedToFinish).isTrue()
}
@Test
fun dialogCancelDoesntAddPackage() {
val label = "label"
val info =
ControlsServiceInfo(
ComponentName("test_pkg", "service"),
label,
ComponentName("test_pkg", "activity")
)
val dialog: Dialog = mock()
whenever(dialogFactory.createConfirmationDialog(any(), any(), any())).thenReturn(dialog)
activityRule.activity.onAppSelected(info)
val captor: ArgumentCaptor<Consumer<Boolean>> = argumentCaptor()
verify(dialogFactory).createConfirmationDialog(any(), any(), capture(captor))
captor.value.accept(false)
verify(authorizedPanelsRepository, never()).addAuthorizedPanels(any())
assertThat(activityRule.activity.triedToFinish).isFalse()
}
class TestableControlsProviderSelectorActivity(
executor: Executor, executor: Executor,
backExecutor: Executor, backExecutor: Executor,
listingController: ControlsListingController, listingController: ControlsListingController,
controlsController: ControlsController, controlsController: ControlsController,
userTracker: UserTracker, userTracker: UserTracker,
uiController: ControlsUiController, authorizedPanelsRepository: AuthorizedPanelsRepository,
dialogFactory: PanelConfirmationDialogFactory,
private val mockDispatcher: OnBackInvokedDispatcher, private val mockDispatcher: OnBackInvokedDispatcher,
private val latch: CountDownLatch private val latch: CountDownLatch
) : ) :
@@ -129,16 +236,50 @@ class ControlsProviderSelectorActivityTest : SysuiTestCase() {
listingController, listingController,
controlsController, controlsController,
userTracker, userTracker,
uiController authorizedPanelsRepository,
dialogFactory
) { ) {
var lastStartedActivity: Intent? = null
var triedToFinish = false
override fun getOnBackInvokedDispatcher(): OnBackInvokedDispatcher { override fun getOnBackInvokedDispatcher(): OnBackInvokedDispatcher {
return mockDispatcher return mockDispatcher
} }
override fun startActivity(intent: Intent?, options: Bundle?) {
lastStartedActivity = intent
}
override fun onStop() { override fun onStop() {
super.onStop() super.onStop()
// ensures that test runner thread does not proceed until ui thread is done // ensures that test runner thread does not proceed until ui thread is done
latch.countDown() latch.countDown()
} }
override fun animateExitAndFinish() {
// Activity should only be finished from the rule.
triedToFinish = true
}
}
companion object {
private fun ControlsServiceInfo(
componentName: ComponentName,
label: CharSequence,
panelComponentName: ComponentName? = null
): ControlsServiceInfo {
val serviceInfo =
ServiceInfo().apply {
applicationInfo = ApplicationInfo()
packageName = componentName.packageName
name = componentName.className
}
return Mockito.spy(ControlsServiceInfo(mock(), serviceInfo)).apply {
doReturn(label).`when`(this).loadLabel()
doReturn(mock<Drawable>()).`when`(this).loadIcon()
doReturn(panelComponentName).`when`(this).panelActivity
}
}
} }
} }

View File

@@ -0,0 +1,106 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package com.android.systemui.controls.management
import android.content.DialogInterface
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.statusbar.phone.SystemUIDialog
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.capture
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
@SmallTest
@RunWith(AndroidTestingRunner::class)
class PanelConfirmationDialogFactoryTest : SysuiTestCase() {
@Test
fun testDialogHasCorrectInfo() {
val mockDialog: SystemUIDialog = mock() { `when`(context).thenReturn(mContext) }
val factory = PanelConfirmationDialogFactory { mockDialog }
val appName = "appName"
factory.createConfirmationDialog(context, appName) {}
verify(mockDialog).setCanceledOnTouchOutside(true)
verify(mockDialog)
.setTitle(context.getString(R.string.controls_panel_authorization_title, appName))
verify(mockDialog)
.setMessage(context.getString(R.string.controls_panel_authorization, appName))
}
@Test
fun testDialogPositiveButton() {
val mockDialog: SystemUIDialog = mock() { `when`(context).thenReturn(mContext) }
val factory = PanelConfirmationDialogFactory { mockDialog }
var response: Boolean? = null
factory.createConfirmationDialog(context, "") { response = it }
val captor: ArgumentCaptor<DialogInterface.OnClickListener> = argumentCaptor()
verify(mockDialog).setPositiveButton(eq(R.string.controls_dialog_ok), capture(captor))
captor.value.onClick(mockDialog, DialogInterface.BUTTON_POSITIVE)
assertThat(response).isTrue()
}
@Test
fun testDialogNeutralButton() {
val mockDialog: SystemUIDialog = mock() { `when`(context).thenReturn(mContext) }
val factory = PanelConfirmationDialogFactory { mockDialog }
var response: Boolean? = null
factory.createConfirmationDialog(context, "") { response = it }
val captor: ArgumentCaptor<DialogInterface.OnClickListener> = argumentCaptor()
verify(mockDialog).setNeutralButton(eq(R.string.cancel), capture(captor))
captor.value.onClick(mockDialog, DialogInterface.BUTTON_NEUTRAL)
assertThat(response).isFalse()
}
@Test
fun testDialogCancel() {
val mockDialog: SystemUIDialog = mock() { `when`(context).thenReturn(mContext) }
val factory = PanelConfirmationDialogFactory { mockDialog }
var response: Boolean? = null
factory.createConfirmationDialog(context, "") { response = it }
val captor: ArgumentCaptor<DialogInterface.OnCancelListener> = argumentCaptor()
verify(mockDialog).setOnCancelListener(capture(captor))
captor.value.onCancel(mockDialog)
assertThat(response).isFalse()
}
}

View File

@@ -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<String>()
)
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<Int, SharedPreferences>) :
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"
}
}

View File

@@ -39,8 +39,10 @@ import com.android.systemui.controls.controller.ControlsController
import com.android.systemui.controls.controller.StructureInfo import com.android.systemui.controls.controller.StructureInfo
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.settings.FakeControlsSettingsRepository import com.android.systemui.controls.settings.FakeControlsSettingsRepository
import com.android.systemui.dump.DumpManager import com.android.systemui.dump.DumpManager
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserFileManager
import com.android.systemui.settings.UserTracker import com.android.systemui.settings.UserTracker
@@ -91,6 +93,8 @@ class ControlsUiControllerImplTest : SysuiTestCase() {
@Mock lateinit var userTracker: UserTracker @Mock lateinit var userTracker: UserTracker
@Mock lateinit var taskViewFactory: TaskViewFactory @Mock lateinit var taskViewFactory: TaskViewFactory
@Mock lateinit var dumpManager: DumpManager @Mock lateinit var dumpManager: DumpManager
@Mock lateinit var authorizedPanelsRepository: AuthorizedPanelsRepository
@Mock lateinit var featureFlags: FeatureFlags
val sharedPreferences = FakeSharedPreferences() val sharedPreferences = FakeSharedPreferences()
lateinit var controlsSettingsRepository: FakeControlsSettingsRepository lateinit var controlsSettingsRepository: FakeControlsSettingsRepository
@@ -132,6 +136,8 @@ class ControlsUiControllerImplTest : SysuiTestCase() {
userTracker, userTracker,
Optional.of(taskViewFactory), Optional.of(taskViewFactory),
controlsSettingsRepository, controlsSettingsRepository,
authorizedPanelsRepository,
featureFlags,
dumpManager dumpManager
) )
`when`( `when`(
@@ -240,7 +246,9 @@ class ControlsUiControllerImplTest : SysuiTestCase() {
@Test @Test
fun testPanelCallsTaskViewFactoryCreate() { fun testPanelCallsTaskViewFactoryCreate() {
mockLayoutInflater() 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) val serviceInfo = setUpPanel(panel)
underTest.show(parent, {}, context) underTest.show(parent, {}, context)
@@ -258,9 +266,11 @@ class ControlsUiControllerImplTest : SysuiTestCase() {
@Test @Test
fun testPanelControllerStartActivityWithCorrectArguments() { fun testPanelControllerStartActivityWithCorrectArguments() {
mockLayoutInflater() mockLayoutInflater()
val packageName = "pkg"
`when`(authorizedPanelsRepository.getAuthorizedPanels()).thenReturn(setOf(packageName))
controlsSettingsRepository.setAllowActionOnTrivialControlsInLockscreen(true) 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) val serviceInfo = setUpPanel(panel)
underTest.show(parent, {}, context) underTest.show(parent, {}, context)
@@ -290,9 +300,11 @@ class ControlsUiControllerImplTest : SysuiTestCase() {
@Test @Test
fun testPendingIntentExtrasAreModified() { fun testPendingIntentExtrasAreModified() {
mockLayoutInflater() mockLayoutInflater()
val packageName = "pkg"
`when`(authorizedPanelsRepository.getAuthorizedPanels()).thenReturn(setOf(packageName))
controlsSettingsRepository.setAllowActionOnTrivialControlsInLockscreen(true) 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) val serviceInfo = setUpPanel(panel)
underTest.show(parent, {}, context) underTest.show(parent, {}, context)

View File

@@ -41,7 +41,7 @@ class FakeSharedPreferences : SharedPreferences {
} }
override fun getStringSet(key: String, defValues: MutableSet<String>?): MutableSet<String>? { override fun getStringSet(key: String, defValues: MutableSet<String>?): MutableSet<String>? {
return data.getOrDefault(key, defValues) as? MutableSet<String>? return (data.getOrDefault(key, defValues) as? Set<String>?)?.toMutableSet()
} }
override fun getInt(key: String, defValue: Int): Int { override fun getInt(key: String, defValue: Int): Int {