diff --git a/packages/SystemUI/res/layout-sw600dp/global_actions_controls_list_view.xml b/packages/SystemUI/res/layout-sw600dp/global_actions_controls_list_view.xml deleted file mode 100644 index ef49b9c3b6adb..0000000000000 --- a/packages/SystemUI/res/layout-sw600dp/global_actions_controls_list_view.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - \ No newline at end of file diff --git a/packages/SystemUI/res/layout/controls_fullscreen.xml b/packages/SystemUI/res/layout/controls_fullscreen.xml index 11a566588738a..e08e63b39e59a 100644 --- a/packages/SystemUI/res/layout/controls_fullscreen.xml +++ b/packages/SystemUI/res/layout/controls_fullscreen.xml @@ -15,28 +15,19 @@ limitations under the License. --> - - - - + diff --git a/packages/SystemUI/res/layout/controls_with_favorites.xml b/packages/SystemUI/res/layout/controls_with_favorites.xml index 9d011482d0114..9efad22694634 100644 --- a/packages/SystemUI/res/layout/controls_with_favorites.xml +++ b/packages/SystemUI/res/layout/controls_with_favorites.xml @@ -18,7 +18,7 @@ @@ -71,5 +71,27 @@ android:background="?android:attr/selectableItemBackgroundBorderless" /> - + + + + + + diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt index 31fadb13db08e..2f49c3fe863e1 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt @@ -24,6 +24,7 @@ import com.android.systemui.controls.ControlStatus import com.android.systemui.util.UserAwareController import com.android.systemui.controls.management.ControlsFavoritingActivity import com.android.systemui.controls.ui.ControlsUiController +import com.android.systemui.controls.ui.SelectedItem import java.util.function.Consumer /** @@ -184,8 +185,8 @@ interface ControlsController : UserAwareController { */ fun countFavoritesForComponent(componentName: ComponentName): Int - /** See [ControlsUiController.getPreferredStructure]. */ - fun getPreferredStructure(): StructureInfo + /** See [ControlsUiController.getPreferredSelectedItem]. */ + fun getPreferredSelection(): SelectedItem /** * Interface for structure to pass data to [ControlsFavoritingActivity]. diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt index 50ce9d4ec0f4d..bdfe1fbeb4b55 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt @@ -38,6 +38,7 @@ import com.android.systemui.controls.ControlStatus import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.controls.management.ControlsListingController import com.android.systemui.controls.ui.ControlsUiController +import com.android.systemui.controls.ui.SelectedItem import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dump.DumpManager @@ -556,8 +557,8 @@ class ControlsControllerImpl @Inject constructor ( ) } - override fun getPreferredStructure(): StructureInfo { - return uiController.getPreferredStructure(getFavorites()) + override fun getPreferredSelection(): SelectedItem { + return uiController.getPreferredSelectedItem(getFavorites()) } override fun dump(pw: PrintWriter, args: Array) { diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/StructureInfo.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/StructureInfo.kt index 34bfa135f0c85..c8090bfb60e89 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/StructureInfo.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/StructureInfo.kt @@ -31,4 +31,9 @@ data class StructureInfo( val componentName: ComponentName, val structure: CharSequence, val controls: List -) +) { + companion object { + val EMPTY_COMPONENT = ComponentName("", "") + val EMPTY_STRUCTURE = StructureInfo(EMPTY_COMPONENT, "", mutableListOf()) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/AppAdapter.kt b/packages/SystemUI/src/com/android/systemui/controls/management/AppAdapter.kt index 2389ad134c55e..753d5addeb114 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/management/AppAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/management/AppAdapter.kt @@ -64,7 +64,8 @@ class AppAdapter( val localeComparator = compareBy(collator) { it.loadLabel() ?: "" } - listOfServices = serviceInfos.sortedWith(localeComparator) + listOfServices = serviceInfos.filter { it.panelActivity == null } + .sortedWith(localeComparator) uiExecutor.execute(::notifyDataSetChanged) } } diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt index d3b5d0edd2221..bd704c1ff0861 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt @@ -27,10 +27,13 @@ import android.view.View import android.view.ViewGroup import android.view.WindowInsets import android.view.WindowInsets.Type +import android.view.WindowManager import androidx.activity.ComponentActivity import com.android.systemui.R import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.controls.management.ControlsAnimations +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import javax.inject.Inject /** @@ -44,6 +47,7 @@ class ControlsActivity @Inject constructor( private val uiController: ControlsUiController, private val broadcastDispatcher: BroadcastDispatcher, private val dreamManager: IDreamManager, + private val featureFlags: FeatureFlags ) : ComponentActivity() { private lateinit var parent: ViewGroup @@ -52,6 +56,9 @@ class ControlsActivity @Inject constructor( override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + if (featureFlags.isEnabled(Flags.USE_APP_PANELS)) { + window.addPrivateFlags(WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY) + } setContentView(R.layout.controls_fullscreen) diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt index c1cfbcb0c2115..f5c5905779a14 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt @@ -35,7 +35,7 @@ interface ControlsUiController { /** * Returns the preferred activity to start, depending on if the user has favorited any - * controls. + * controls or whether there are any app providing panels. */ fun resolveActivity(): Class<*> @@ -53,9 +53,43 @@ interface ControlsUiController { ) /** - * Returns the structure that is currently preferred by the user. + * Returns the element that is currently preferred by the user. * - * This structure 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 getPreferredStructure(structures: List): StructureInfo + fun getPreferredSelectedItem(structures: List): SelectedItem } + +sealed class SelectedItem { + + abstract val name: CharSequence + abstract val hasControls: Boolean + abstract val componentName: ComponentName + + /** + * Represents the currently selected item for a structure. + */ + data class StructureItem(val structure: StructureInfo) : SelectedItem() { + override val name: CharSequence = structure.structure + override val hasControls: Boolean = structure.controls.isNotEmpty() + override val componentName: ComponentName = structure.componentName + } + + /** + * Represents the currently selected item for a service that provides a panel activity. + * + * The [componentName] is that of the service, as that is the expected identifier that should + * not change (to always provide proper migration). + */ + data class PanelItem( + val appName: CharSequence, + override val componentName: + ComponentName + ) : SelectedItem() { + override val name: CharSequence = appName + override val hasControls: Boolean = true + } + companion object { + val EMPTY_SELECTION: SelectedItem = StructureItem(StructureInfo.EMPTY_STRUCTURE) + } +} \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt index 6cb0e8b048693..4c8e1ac968f9a 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt @@ -21,6 +21,7 @@ import android.animation.AnimatorListenerAdapter import android.animation.ObjectAnimator import android.app.Activity import android.app.ActivityOptions +import android.app.PendingIntent import android.content.ComponentName import android.content.Context import android.content.Intent @@ -36,18 +37,22 @@ import android.view.animation.AccelerateInterpolator import android.view.animation.DecelerateInterpolator import android.widget.AdapterView import android.widget.ArrayAdapter +import android.widget.FrameLayout import android.widget.ImageView import android.widget.LinearLayout import android.widget.ListPopupWindow import android.widget.Space import android.widget.TextView +import androidx.annotation.VisibleForTesting +import com.android.systemui.Dumpable import com.android.systemui.R import com.android.systemui.controls.ControlsMetricsLogger import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.controls.CustomIconCache -import com.android.systemui.controls.controller.ControlInfo import com.android.systemui.controls.controller.ControlsController import com.android.systemui.controls.controller.StructureInfo +import com.android.systemui.controls.controller.StructureInfo.Companion.EMPTY_COMPONENT +import com.android.systemui.controls.controller.StructureInfo.Companion.EMPTY_STRUCTURE import com.android.systemui.controls.management.ControlAdapter import com.android.systemui.controls.management.ControlsEditingActivity import com.android.systemui.controls.management.ControlsFavoritingActivity @@ -56,16 +61,21 @@ import com.android.systemui.controls.management.ControlsProviderSelectorActivity import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.dump.DumpManager import com.android.systemui.globalactions.GlobalActionsPopupMenu import com.android.systemui.plugins.ActivityStarter import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker -import com.android.systemui.shade.ShadeController import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.util.asIndenting import com.android.systemui.util.concurrency.DelayableExecutor +import com.android.systemui.util.indentIfPossible +import com.android.wm.shell.TaskViewFactory import dagger.Lazy +import java.io.PrintWriter import java.text.Collator +import java.util.Optional import java.util.function.Consumer import javax.inject.Inject @@ -80,39 +90,34 @@ class ControlsUiControllerImpl @Inject constructor ( val controlsListingController: Lazy, val controlActionCoordinator: ControlActionCoordinator, private val activityStarter: ActivityStarter, - private val shadeController: ShadeController, private val iconCache: CustomIconCache, private val controlsMetricsLogger: ControlsMetricsLogger, private val keyguardStateController: KeyguardStateController, private val userFileManager: UserFileManager, private val userTracker: UserTracker, -) : ControlsUiController { + private val taskViewFactory: Optional, + dumpManager: DumpManager +) : ControlsUiController, Dumpable { companion object { private const val PREF_COMPONENT = "controls_component" - private const val PREF_STRUCTURE = "controls_structure" + private const val PREF_STRUCTURE_OR_APP_NAME = "controls_structure" + private const val PREF_IS_PANEL = "controls_is_panel" private const val FADE_IN_MILLIS = 200L - - private val EMPTY_COMPONENT = ComponentName("", "") - private val EMPTY_STRUCTURE = StructureInfo( - EMPTY_COMPONENT, - "", - mutableListOf() - ) } - private var selectedStructure: StructureInfo = EMPTY_STRUCTURE + private var selectedItem: SelectedItem = SelectedItem.EMPTY_SELECTION private lateinit var allStructures: List private val controlsById = mutableMapOf() private val controlViewsById = mutableMapOf() private lateinit var parent: ViewGroup - private lateinit var lastItems: List private var popup: ListPopupWindow? = null private var hidden = true private lateinit var onDismiss: Runnable private val popupThemedContext = ContextThemeWrapper(context, R.style.Control_ListPopupWindow) private var retainCache = false + private var lastSelections = emptyList() private val sharedPreferences get() = userFileManager.getSharedPreferences( fileName = DeviceControlsControllerImpl.PREFS_CONTROLS_FILE, @@ -120,6 +125,8 @@ class ControlsUiControllerImpl @Inject constructor ( userId = userTracker.userId ) + private var taskViewController: PanelTaskViewController? = null + private val collator = Collator.getInstance(context.resources.configuration.locales[0]) private val localeComparator = compareBy(collator) { it.getTitle() @@ -128,10 +135,12 @@ class ControlsUiControllerImpl @Inject constructor ( private val onSeedingComplete = Consumer { accepted -> if (accepted) { - selectedStructure = controlsController.get().getFavorites().maxByOrNull { + selectedItem = controlsController.get().getFavorites().maxByOrNull { it.controls.size - } ?: EMPTY_STRUCTURE - updatePreferences(selectedStructure) + }?.let { + SelectedItem.StructureItem(it) + } ?: SelectedItem.EMPTY_SELECTION + updatePreferences(selectedItem) } reload(parent) } @@ -139,6 +148,10 @@ class ControlsUiControllerImpl @Inject constructor ( private lateinit var activityContext: Context private lateinit var listingCallback: ControlsListingController.ControlsListingCallback + init { + dumpManager.registerDumpable(javaClass.name, this) + } + private fun createCallback( onResult: (List) -> Unit ): ControlsListingController.ControlsListingCallback { @@ -146,7 +159,15 @@ class ControlsUiControllerImpl @Inject constructor ( override fun onServicesUpdated(serviceInfos: List) { val lastItems = serviceInfos.map { val uid = it.serviceInfo.applicationInfo.uid - SelectionItem(it.loadLabel(), "", it.loadIcon(), it.componentName, uid) + + SelectionItem( + it.loadLabel(), + "", + it.loadIcon(), + it.componentName, + uid, + it.panelActivity + ) } uiExecutor.execute { parent.removeAllViews() @@ -160,11 +181,13 @@ class ControlsUiControllerImpl @Inject constructor ( override fun resolveActivity(): Class<*> { val allStructures = controlsController.get().getFavorites() - val selectedStructure = getPreferredStructure(allStructures) + val selected = getPreferredSelectedItem(allStructures) + val anyPanels = controlsListingController.get().getCurrentServices() + .none { it.panelActivity != null } return if (controlsController.get().addSeedingFavoritesCallback(onSeedingComplete)) { ControlsActivity::class.java - } else if (selectedStructure.controls.isEmpty() && allStructures.size <= 1) { + } else if (!selected.hasControls && allStructures.size <= 1 && !anyPanels) { ControlsProviderSelectorActivity::class.java } else { ControlsActivity::class.java @@ -186,31 +209,49 @@ class ControlsUiControllerImpl @Inject constructor ( controlActionCoordinator.activityContext = activityContext allStructures = controlsController.get().getFavorites() - selectedStructure = getPreferredStructure(allStructures) + selectedItem = getPreferredSelectedItem(allStructures) if (controlsController.get().addSeedingFavoritesCallback(onSeedingComplete)) { listingCallback = createCallback(::showSeedingView) - } else if (selectedStructure.controls.isEmpty() && allStructures.size <= 1) { + } else if ( + selectedItem !is SelectedItem.PanelItem && + !selectedItem.hasControls && + allStructures.size <= 1 + ) { // only show initial view if there are really no favorites across any structure - listingCallback = createCallback(::showInitialSetupView) + listingCallback = createCallback(::initialView) } else { - selectedStructure.controls.map { - ControlWithState(selectedStructure.componentName, it, null) - }.associateByTo(controlsById) { - ControlKey(selectedStructure.componentName, it.ci.controlId) + val selected = selectedItem + if (selected is SelectedItem.StructureItem) { + selected.structure.controls.map { + ControlWithState(selected.structure.componentName, it, null) + }.associateByTo(controlsById) { + ControlKey(selected.structure.componentName, it.ci.controlId) + } + controlsController.get().subscribeToFavorites(selected.structure) } listingCallback = createCallback(::showControlsView) - controlsController.get().subscribeToFavorites(selectedStructure) } controlsListingController.get().addCallback(listingCallback) } + private fun initialView(items: List) { + if (items.any { it.isPanel }) { + // We have at least a panel, so we'll end up showing that. + showControlsView(items) + } else { + showInitialSetupView(items) + } + } + private fun reload(parent: ViewGroup) { if (hidden) return controlsListingController.get().removeCallback(listingCallback) controlsController.get().unsubscribe() + taskViewController?.dismiss() + taskViewController = null val fadeAnim = ObjectAnimator.ofFloat(parent, "alpha", 1.0f, 0.0f) fadeAnim.setInterpolator(AccelerateInterpolator(1.0f)) @@ -290,27 +331,90 @@ class ControlsUiControllerImpl @Inject constructor ( private fun showControlsView(items: List) { controlViewsById.clear() - val itemsByComponent = items.associateBy { it.componentName } - val itemsWithStructure = mutableListOf() - allStructures.mapNotNullTo(itemsWithStructure) { + val (panels, structures) = items.partition { it.isPanel } + val panelComponents = panels.map { it.componentName }.toSet() + + val itemsByComponent = structures.associateBy { it.componentName } + .filterNot { it.key in panelComponents } + val panelsAndStructures = mutableListOf() + allStructures.mapNotNullTo(panelsAndStructures) { itemsByComponent.get(it.componentName)?.copy(structure = it.structure) } - itemsWithStructure.sortWith(localeComparator) + panelsAndStructures.addAll(panels) - val selectionItem = findSelectionItem(selectedStructure, itemsWithStructure) ?: items[0] + panelsAndStructures.sortWith(localeComparator) - controlsMetricsLogger.refreshBegin(selectionItem.uid, !keyguardStateController.isUnlocked()) + lastSelections = panelsAndStructures - createListView(selectionItem) - createDropDown(itemsWithStructure, selectionItem) + val selectionItem = findSelectionItem(selectedItem, panelsAndStructures) + ?: if (panels.isNotEmpty()) { + // If we couldn't find a good selected item, but there's at least one panel, + // show a panel. + panels[0] + } else { + items[0] + } + + maybeUpdateSelectedItem(selectionItem) + + createControlsSpaceFrame() + + if (taskViewFactory.isPresent && selectionItem.isPanel) { + createPanelView(selectionItem.panelComponentName!!) + } else if (!selectionItem.isPanel) { + controlsMetricsLogger + .refreshBegin(selectionItem.uid, !keyguardStateController.isUnlocked()) + createListView(selectionItem) + } else { + Log.w(ControlsUiController.TAG, "Not TaskViewFactory to display panel $selectionItem") + } + + createDropDown(panelsAndStructures, selectionItem) createMenu() } - private fun createMenu() { - val items = arrayOf( - context.resources.getString(R.string.controls_menu_add), - context.resources.getString(R.string.controls_menu_edit) + private fun createPanelView(componentName: ComponentName) { + val pendingIntent = PendingIntent.getActivity( + context, + 0, + Intent().setComponent(componentName), + PendingIntent.FLAG_IMMUTABLE ) + + parent.requireViewById(R.id.controls_scroll_view).visibility = View.GONE + val container = parent.requireViewById(R.id.controls_panel) + container.visibility = View.VISIBLE + container.post { + taskViewFactory.get().create(activityContext, uiExecutor) { taskView -> + taskViewController = PanelTaskViewController( + activityContext, + uiExecutor, + pendingIntent, + taskView, + onDismiss::run + ).also { + container.addView(taskView) + it.launchTaskView() + } + } + } + } + + private fun createMenu() { + val isPanel = selectedItem is SelectedItem.PanelItem + val selectedStructure = (selectedItem as? SelectedItem.StructureItem)?.structure + ?: EMPTY_STRUCTURE + + val items = if (isPanel) { + arrayOf( + context.resources.getString(R.string.controls_menu_add), + ) + } else { + arrayOf( + context.resources.getString(R.string.controls_menu_add), + context.resources.getString(R.string.controls_menu_edit) + ) + } var adapter = ArrayAdapter(context, R.layout.controls_more_item, items) val anchor = parent.requireViewById(R.id.controls_more) @@ -331,7 +435,13 @@ class ControlsUiControllerImpl @Inject constructor ( ) { when (pos) { // 0: Add Control - 0 -> startFavoritingActivity(selectedStructure) + 0 -> { + if (isPanel) { + startProviderSelectorActivity() + } else { + startFavoritingActivity(selectedStructure) + } + } // 1: Edit controls 1 -> startEditingActivity(selectedStructure) } @@ -353,6 +463,9 @@ class ControlsUiControllerImpl @Inject constructor ( addAll(items) } + val iconSize = context.resources + .getDimensionPixelSize(R.dimen.controls_header_app_icon_size) + /* * Default spinner widget does not work with the window type required * for this dialog. Use a textView with the ListPopupWindow to achieve @@ -363,14 +476,21 @@ class ControlsUiControllerImpl @Inject constructor ( // override the default color on the dropdown drawable (getBackground() as LayerDrawable).getDrawable(0) .setTint(context.resources.getColor(R.color.control_spinner_dropdown, null)) - } - - if (items.size == 1) { - spinner.setBackground(null) - return + selected.icon.setBounds(0, 0, iconSize, iconSize) + compoundDrawablePadding = (iconSize / 2.4f).toInt() + setCompoundDrawablesRelative(selected.icon, null, null, null) } val anchor = parent.requireViewById(R.id.controls_header) + if (items.size == 1) { + spinner.setBackground(null) + anchor.setOnClickListener(null) + return + } else { + spinner.background = parent.context.resources + .getDrawable(R.drawable.control_spinner_background) + } + anchor.setOnClickListener(object : View.OnClickListener { override fun onClick(v: View) { popup = GlobalActionsPopupMenu( @@ -398,14 +518,20 @@ class ControlsUiControllerImpl @Inject constructor ( }) } - private fun createListView(selected: SelectionItem) { - val inflater = LayoutInflater.from(context) + private fun createControlsSpaceFrame() { + val inflater = LayoutInflater.from(activityContext) inflater.inflate(R.layout.controls_with_favorites, parent, true) parent.requireViewById(R.id.controls_close).apply { setOnClickListener { _: View -> onDismiss.run() } visibility = View.VISIBLE } + } + + private fun createListView(selected: SelectionItem) { + if (selectedItem !is SelectedItem.StructureItem) return + val selectedStructure = (selectedItem as SelectedItem.StructureItem).structure + val inflater = LayoutInflater.from(activityContext) val maxColumns = ControlAdapter.findMaxColumns(activityContext.resources) @@ -453,35 +579,51 @@ class ControlsUiControllerImpl @Inject constructor ( } } - override fun getPreferredStructure(structures: List): StructureInfo { - if (structures.isEmpty()) return EMPTY_STRUCTURE + override fun getPreferredSelectedItem(structures: List): SelectedItem { + val sp = sharedPreferences - val component = sharedPreferences.getString(PREF_COMPONENT, null)?.let { + val component = sp.getString(PREF_COMPONENT, null)?.let { ComponentName.unflattenFromString(it) } ?: EMPTY_COMPONENT - val structure = sharedPreferences.getString(PREF_STRUCTURE, "") - - return structures.firstOrNull { - component == it.componentName && structure == it.structure - } ?: structures.get(0) + val name = sp.getString(PREF_STRUCTURE_OR_APP_NAME, "")!! + val isPanel = sp.getBoolean(PREF_IS_PANEL, false) + return if (isPanel) { + SelectedItem.PanelItem(name, component) + } else { + if (structures.isEmpty()) return SelectedItem.EMPTY_SELECTION + SelectedItem.StructureItem(structures.firstOrNull { + component == it.componentName && name == it.structure + } ?: structures.get(0)) + } } - private fun updatePreferences(si: StructureInfo) { - if (si == EMPTY_STRUCTURE) return + private fun updatePreferences(si: SelectedItem) { sharedPreferences.edit() - .putString(PREF_COMPONENT, si.componentName.flattenToString()) - .putString(PREF_STRUCTURE, si.structure.toString()) - .commit() + .putString(PREF_COMPONENT, si.componentName.flattenToString()) + .putString(PREF_STRUCTURE_OR_APP_NAME, si.name.toString()) + .putBoolean(PREF_IS_PANEL, si is SelectedItem.PanelItem) + .commit() + } + + private fun maybeUpdateSelectedItem(item: SelectionItem): Boolean { + val newSelection = if (item.isPanel) { + SelectedItem.PanelItem(item.appName, item.componentName) + } else { + SelectedItem.StructureItem(allStructures.firstOrNull { + it.structure == item.structure && it.componentName == item.componentName + } ?: EMPTY_STRUCTURE) + } + return if (newSelection != selectedItem ) { + selectedItem = newSelection + updatePreferences(selectedItem) + true + } else { + false + } } private fun switchAppOrStructure(item: SelectionItem) { - val newSelection = allStructures.first { - it.structure == item.structure && it.componentName == item.componentName - } - - if (newSelection != selectedStructure) { - selectedStructure = newSelection - updatePreferences(selectedStructure) + if (maybeUpdateSelectedItem(item)) { reload(parent) } } @@ -505,6 +647,8 @@ class ControlsUiControllerImpl @Inject constructor ( closeDialogs(true) controlsController.get().unsubscribe() + taskViewController?.dismiss() + taskViewController = null parent.removeAllViews() controlsById.clear() @@ -545,20 +689,46 @@ class ControlsUiControllerImpl @Inject constructor ( return row } - private fun findSelectionItem(si: StructureInfo, items: List): SelectionItem? = - items.firstOrNull { - it.componentName == si.componentName && it.structure == si.structure + private fun findSelectionItem(si: SelectedItem, items: List): SelectionItem? = + items.firstOrNull { it.matches(si) } + + override fun dump(pw: PrintWriter, args: Array) { + pw.println("ControlsUiControllerImpl:") + pw.asIndenting().indentIfPossible { + println("hidden: $hidden") + println("selectedItem: $selectedItem") + println("lastSelections: $lastSelections") } + } } -private data class SelectionItem( +@VisibleForTesting +internal data class SelectionItem( val appName: CharSequence, val structure: CharSequence, val icon: Drawable, val componentName: ComponentName, - val uid: Int + val uid: Int, + val panelComponentName: ComponentName? ) { fun getTitle() = if (structure.isEmpty()) { appName } else { structure } + + val isPanel: Boolean = panelComponentName != null + + fun matches(selectedItem: SelectedItem): Boolean { + if (componentName != selectedItem.componentName) { + // Not the same component so they are not the same. + return false + } + if (isPanel || selectedItem is SelectedItem.PanelItem) { + // As they have the same component, if [this.isPanel] then we may be migrating from + // device controls API into panel. Want this to match, even if the selectedItem is not + // a panel. We don't want to match on app name because that can change with locale. + return true + } + // Return true if we find a structure with the correct name + return structure == (selectedItem as SelectedItem.StructureItem).structure.structure + } } private class ItemAdapter( diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/PanelTaskViewController.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/PanelTaskViewController.kt new file mode 100644 index 0000000000000..7143be298a9db --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/PanelTaskViewController.kt @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2022 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.ActivityOptions +import android.app.ActivityTaskManager +import android.app.ActivityTaskManager.INVALID_TASK_ID +import android.app.PendingIntent +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import com.android.systemui.util.boundsOnScreen +import com.android.wm.shell.TaskView +import java.util.concurrent.Executor + +class PanelTaskViewController( + private val activityContext: Context, + private val uiExecutor: Executor, + private val pendingIntent: PendingIntent, + private val taskView: TaskView, + private val hide: () -> Unit = {} +) { + + private var detailTaskId = INVALID_TASK_ID + + private val fillInIntent = + Intent().apply { + // Apply flags to make behaviour match documentLaunchMode=always. + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK) + } + + private fun removeDetailTask() { + if (detailTaskId == INVALID_TASK_ID) return + ActivityTaskManager.getInstance().removeTask(detailTaskId) + detailTaskId = INVALID_TASK_ID + } + + private val stateCallback = + object : TaskView.Listener { + override fun onInitialized() { + + val options = + ActivityOptions.makeCustomAnimation( + activityContext, + 0 /* enterResId */, + 0 /* exitResId */ + ) + options.taskAlwaysOnTop = true + + taskView.post { + taskView.startActivity( + pendingIntent, + fillInIntent, + options, + taskView.boundsOnScreen + ) + } + } + + override fun onTaskRemovalStarted(taskId: Int) { + detailTaskId = INVALID_TASK_ID + dismiss() + } + + override fun onTaskCreated(taskId: Int, name: ComponentName?) { + detailTaskId = taskId + } + + override fun onReleased() { + removeDetailTask() + } + + override fun onBackPressedOnTaskRoot(taskId: Int) { + dismiss() + hide() + } + } + + fun dismiss() { + taskView.release() + } + + fun launchTaskView() { + taskView.setListener(uiExecutor, stateCallback) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/MinHeightScrollView.java b/packages/SystemUI/src/com/android/systemui/globalactions/MinHeightScrollView.java deleted file mode 100644 index 622fa658f1b0e..0000000000000 --- a/packages/SystemUI/src/com/android/systemui/globalactions/MinHeightScrollView.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (C) 2020 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.globalactions; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.View; -import android.widget.ScrollView; - -/** - * When measured, this view sets the minimum height of its first child to be equal to its own - * target height. - * - * This ensures fall-through click handlers can be placed on this view's child component. - */ -public class MinHeightScrollView extends ScrollView { - public MinHeightScrollView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @Override - public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - View firstChild = getChildAt(0); - if (firstChild != null) { - firstChild.setMinimumHeight(MeasureSpec.getSize(heightMeasureSpec)); - } - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DeviceControlsTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/DeviceControlsTile.kt index c65bd9b4348e1..41d854969e20f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/DeviceControlsTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DeviceControlsTile.kt @@ -32,6 +32,7 @@ import com.android.systemui.controls.dagger.ControlsComponent import com.android.systemui.controls.dagger.ControlsComponent.Visibility.AVAILABLE import com.android.systemui.controls.management.ControlsListingController import com.android.systemui.controls.ui.ControlsUiController +import com.android.systemui.controls.ui.SelectedItem import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.plugins.ActivityStarter @@ -125,14 +126,15 @@ class DeviceControlsTile @Inject constructor( state.icon = icon if (controlsComponent.isEnabled() && hasControlsApps.get()) { if (controlsComponent.getVisibility() == AVAILABLE) { - val structureInfo = controlsComponent - .getControlsController().get().getPreferredStructure() - state.state = if (structureInfo.controls.isEmpty()) { + val selection = controlsComponent + .getControlsController().get().getPreferredSelection() + state.state = if (selection is SelectedItem.StructureItem && + selection.structure.controls.isEmpty()) { Tile.STATE_INACTIVE } else { Tile.STATE_ACTIVE } - val label = structureInfo.structure + val label = selection.name state.secondaryLabel = if (label == tileLabel) null else label } else { state.state = Tile.STATE_INACTIVE diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt index e3266115f7779..6c66f0bb1e475 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt @@ -140,6 +140,9 @@ public class DeviceControlsControllerImpl @Inject constructor( // is out of sync, perhaps through a device restore, and update the // preference addPackageToSeededSet(prefs, pkg) + } else if (it.panelActivity != null) { + // Do not seed for packages with panels + addPackageToSeededSet(prefs, pkg) } else { componentsToSeed.add(it.componentName) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/AppAdapterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/AppAdapterTest.kt index 1e4a9e42cce13..765c4c0ac0f07 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/management/AppAdapterTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/AppAdapterTest.kt @@ -27,6 +27,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.mock import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import org.junit.Before @@ -34,9 +35,8 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.Mock -import org.mockito.Mockito.mock -import org.mockito.Mockito.verify import org.mockito.Mockito.`when` +import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @SmallTest @@ -70,7 +70,7 @@ class AppAdapterTest : SysuiTestCase() { fun testOnServicesUpdated_nullLoadLabel() { val captor = ArgumentCaptor .forClass(ControlsListingController.ControlsListingCallback::class.java) - val controlsServiceInfo = mock(ControlsServiceInfo::class.java) + val controlsServiceInfo = mock() val serviceInfo = listOf(controlsServiceInfo) `when`(controlsServiceInfo.loadLabel()).thenReturn(null) verify(controlsListingController).observe(any(Lifecycle::class.java), captor.capture()) @@ -81,4 +81,32 @@ class AppAdapterTest : SysuiTestCase() { assertThat(adapter.itemCount).isEqualTo(serviceInfo.size) } + + @Test + fun testOnServicesUpdatedDoesntHavePanels() { + val captor = ArgumentCaptor + .forClass(ControlsListingController.ControlsListingCallback::class.java) + val serviceInfo = listOf( + ControlsServiceInfo("no panel", null), + ControlsServiceInfo("panel", mock()) + ) + verify(controlsListingController).observe(any(Lifecycle::class.java), captor.capture()) + + captor.value.onServicesUpdated(serviceInfo) + backgroundExecutor.runAllReady() + uiExecutor.runAllReady() + + assertThat(adapter.itemCount).isEqualTo(1) + } + + fun ControlsServiceInfo( + label: CharSequence, + panelComponentName: ComponentName? = null + ): ControlsServiceInfo { + return mock { + `when`(this.loadLabel()).thenReturn(label) + `when`(this.panelActivity).thenReturn(panelComponentName) + `when`(this.loadIcon()).thenReturn(mock()) + } + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt index 49c7442b9708a..e679b1391c770 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt @@ -17,8 +17,10 @@ package com.android.systemui.controls.ui import android.content.ComponentName +import android.content.Context import android.testing.AndroidTestingRunner import android.testing.TestableLooper +import android.widget.FrameLayout import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.controls.ControlsMetricsLogger @@ -26,6 +28,7 @@ import com.android.systemui.controls.CustomIconCache import com.android.systemui.controls.controller.ControlsController import com.android.systemui.controls.controller.StructureInfo import com.android.systemui.controls.management.ControlsListingController +import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.ActivityStarter import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker @@ -34,9 +37,12 @@ import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.FakeSharedPreferences import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.mockito.any import com.android.systemui.util.time.FakeSystemClock +import com.android.wm.shell.TaskViewFactory import com.google.common.truth.Truth.assertThat import dagger.Lazy +import java.util.Optional import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -44,7 +50,7 @@ import org.mockito.Mock import org.mockito.Mockito.anyInt import org.mockito.Mockito.anyString import org.mockito.Mockito.mock -import org.mockito.Mockito.times +import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations @@ -63,16 +69,22 @@ class ControlsUiControllerImplTest : SysuiTestCase() { @Mock lateinit var keyguardStateController: KeyguardStateController @Mock lateinit var userFileManager: UserFileManager @Mock lateinit var userTracker: UserTracker + @Mock lateinit var taskViewFactory: TaskViewFactory + @Mock lateinit var activityContext: Context + @Mock lateinit var dumpManager: DumpManager val sharedPreferences = FakeSharedPreferences() var uiExecutor = FakeExecutor(FakeSystemClock()) var bgExecutor = FakeExecutor(FakeSystemClock()) lateinit var underTest: ControlsUiControllerImpl + lateinit var parent: FrameLayout @Before fun setup() { MockitoAnnotations.initMocks(this) + parent = FrameLayout(mContext) + underTest = ControlsUiControllerImpl( Lazy { controlsController }, @@ -82,12 +94,13 @@ class ControlsUiControllerImplTest : SysuiTestCase() { Lazy { controlsListingController }, controlActionCoordinator, activityStarter, - shadeController, iconCache, controlsMetricsLogger, keyguardStateController, userFileManager, - userTracker + userTracker, + Optional.of(taskViewFactory), + dumpManager ) `when`( userFileManager.getSharedPreferences( @@ -105,8 +118,8 @@ class ControlsUiControllerImplTest : SysuiTestCase() { @Test fun testGetPreferredStructure() { val structureInfo = mock(StructureInfo::class.java) - underTest.getPreferredStructure(listOf(structureInfo)) - verify(userFileManager, times(2)) + underTest.getPreferredSelectedItem(listOf(structureInfo)) + verify(userFileManager) .getSharedPreferences( fileName = DeviceControlsControllerImpl.PREFS_CONTROLS_FILE, mode = 0, @@ -116,25 +129,30 @@ class ControlsUiControllerImplTest : SysuiTestCase() { @Test fun testGetPreferredStructure_differentUserId() { - val structureInfo = + val selectedItems = listOf( - StructureInfo(ComponentName.unflattenFromString("pkg/.cls1"), "a", ArrayList()), - StructureInfo(ComponentName.unflattenFromString("pkg/.cls2"), "b", ArrayList()), + SelectedItem.StructureItem( + StructureInfo(ComponentName.unflattenFromString("pkg/.cls1"), "a", ArrayList()) + ), + SelectedItem.StructureItem( + StructureInfo(ComponentName.unflattenFromString("pkg/.cls2"), "b", ArrayList()) + ), ) + val structures = selectedItems.map { it.structure } sharedPreferences .edit() - .putString("controls_component", structureInfo[0].componentName.flattenToString()) - .putString("controls_structure", structureInfo[0].structure.toString()) + .putString("controls_component", selectedItems[0].componentName.flattenToString()) + .putString("controls_structure", selectedItems[0].name.toString()) .commit() val differentSharedPreferences = FakeSharedPreferences() differentSharedPreferences .edit() - .putString("controls_component", structureInfo[1].componentName.flattenToString()) - .putString("controls_structure", structureInfo[1].structure.toString()) + .putString("controls_component", selectedItems[1].componentName.flattenToString()) + .putString("controls_structure", selectedItems[1].name.toString()) .commit() - val previousPreferredStructure = underTest.getPreferredStructure(structureInfo) + val previousPreferredStructure = underTest.getPreferredSelectedItem(structures) `when`( userFileManager.getSharedPreferences( @@ -146,10 +164,39 @@ class ControlsUiControllerImplTest : SysuiTestCase() { .thenReturn(differentSharedPreferences) `when`(userTracker.userId).thenReturn(1) - val currentPreferredStructure = underTest.getPreferredStructure(structureInfo) + val currentPreferredStructure = underTest.getPreferredSelectedItem(structures) - assertThat(previousPreferredStructure).isEqualTo(structureInfo[0]) - assertThat(currentPreferredStructure).isEqualTo(structureInfo[1]) + assertThat(previousPreferredStructure).isEqualTo(selectedItems[0]) + assertThat(currentPreferredStructure).isEqualTo(selectedItems[1]) assertThat(currentPreferredStructure).isNotEqualTo(previousPreferredStructure) } + + @Test + fun testGetPreferredPanel() { + val panel = SelectedItem.PanelItem("App name", ComponentName("pkg", "cls")) + sharedPreferences + .edit() + .putString("controls_component", panel.componentName.flattenToString()) + .putString("controls_structure", panel.appName.toString()) + .putBoolean("controls_is_panel", true) + .commit() + + val selected = underTest.getPreferredSelectedItem(emptyList()) + + assertThat(selected).isEqualTo(panel) + } + + @Test + fun testPanelDoesNotRefreshControls() { + val panel = SelectedItem.PanelItem("App name", ComponentName("pkg", "cls")) + sharedPreferences + .edit() + .putString("controls_component", panel.componentName.flattenToString()) + .putString("controls_structure", panel.appName.toString()) + .putBoolean("controls_is_panel", true) + .commit() + + underTest.show(parent, {}, activityContext) + verify(controlsController, never()).refreshStatus(any(), any()) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/PanelTaskViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/PanelTaskViewControllerTest.kt new file mode 100644 index 0000000000000..5cd2ace4604a4 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/PanelTaskViewControllerTest.kt @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2022 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.ActivityOptions +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK +import android.content.Intent.FLAG_ACTIVITY_NEW_TASK +import android.graphics.Rect +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.util.boundsOnScreen +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.argumentCaptor +import com.android.systemui.util.mockito.capture +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.time.FakeSystemClock +import com.android.wm.shell.TaskView +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +class PanelTaskViewControllerTest : SysuiTestCase() { + + companion object { + val FAKE_BOUNDS = Rect(10, 20, 30, 40) + } + + @Mock private lateinit var activityContext: Context + @Mock private lateinit var taskView: TaskView + @Mock private lateinit var pendingIntent: PendingIntent + @Mock private lateinit var hideRunnable: () -> Unit + + @Captor private lateinit var listenerCaptor: ArgumentCaptor + + private lateinit var uiExecutor: FakeExecutor + private lateinit var underTest: PanelTaskViewController + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + whenever(taskView.boundsOnScreen).thenAnswer { (it.arguments[0] as Rect).set(FAKE_BOUNDS) } + whenever(taskView.post(any())).thenAnswer { + uiExecutor.execute(it.arguments[0] as Runnable) + true + } + + uiExecutor = FakeExecutor(FakeSystemClock()) + + underTest = + PanelTaskViewController( + activityContext, + uiExecutor, + pendingIntent, + taskView, + hideRunnable + ) + } + + @Test + fun testLaunchTaskViewAttachedListener() { + underTest.launchTaskView() + verify(taskView).setListener(eq(uiExecutor), any()) + } + + @Test + fun testTaskViewOnInitializeStartsActivity() { + underTest.launchTaskView() + verify(taskView).setListener(any(), capture(listenerCaptor)) + + listenerCaptor.value.onInitialized() + uiExecutor.runAllReady() + + val intentCaptor = argumentCaptor() + val optionsCaptor = argumentCaptor() + + verify(taskView) + .startActivity( + eq(pendingIntent), + /* fillInIntent */ capture(intentCaptor), + capture(optionsCaptor), + eq(FAKE_BOUNDS) + ) + + assertThat(intentCaptor.value.flags) + .isEqualTo(FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_MULTIPLE_TASK) + assertThat(optionsCaptor.value.taskAlwaysOnTop).isTrue() + } + + @Test + fun testHideRunnableCalledWhenBackOnRoot() { + underTest.launchTaskView() + verify(taskView).setListener(any(), capture(listenerCaptor)) + + listenerCaptor.value.onBackPressedOnTaskRoot(0) + + verify(hideRunnable).invoke() + } + + @Test + fun testTaskViewReleasedOnDismiss() { + underTest.dismiss() + verify(taskView).release() + } + + @Test + fun testTaskViewReleasedOnBackOnRoot() { + underTest.launchTaskView() + verify(taskView).setListener(any(), capture(listenerCaptor)) + + listenerCaptor.value.onBackPressedOnTaskRoot(0) + verify(taskView).release() + } + + @Test + fun testOnTaskRemovalStarted() { + underTest.launchTaskView() + verify(taskView).setListener(any(), capture(listenerCaptor)) + + listenerCaptor.value.onTaskRemovalStarted(0) + verify(taskView).release() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/SelectionItemTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/SelectionItemTest.kt new file mode 100644 index 0000000000000..57176f0eabe0f --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/SelectionItemTest.kt @@ -0,0 +1,112 @@ +package com.android.systemui.controls.ui + +import android.content.ComponentName +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.controls.controller.StructureInfo +import com.android.systemui.util.mockito.mock +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class SelectionItemTest : SysuiTestCase() { + + @Test + fun testMatchBadComponentName_false() { + val selectionItem = + SelectionItem( + appName = "app", + structure = "structure", + icon = mock(), + componentName = ComponentName("pkg", "cls"), + uid = 0, + panelComponentName = null + ) + + assertThat( + selectionItem.matches( + SelectedItem.StructureItem( + StructureInfo(ComponentName("", ""), "s", emptyList()) + ) + ) + ) + .isFalse() + assertThat(selectionItem.matches(SelectedItem.PanelItem("name", ComponentName("", "")))) + .isFalse() + } + + @Test + fun testMatchSameComponentName_panelSelected_true() { + val componentName = ComponentName("pkg", "cls") + + val selectionItem = + SelectionItem( + appName = "app", + structure = "structure", + icon = mock(), + componentName = componentName, + uid = 0, + panelComponentName = null + ) + assertThat(selectionItem.matches(SelectedItem.PanelItem("name", componentName))).isTrue() + } + + @Test + fun testMatchSameComponentName_panelSelection_true() { + val componentName = ComponentName("pkg", "cls") + + val selectionItem = + SelectionItem( + appName = "app", + structure = "structure", + icon = mock(), + componentName = componentName, + uid = 0, + panelComponentName = ComponentName("pkg", "panel") + ) + assertThat(selectionItem.matches(SelectedItem.PanelItem("name", componentName))).isTrue() + } + + @Test + fun testMatchSameComponentSameStructure_true() { + val componentName = ComponentName("pkg", "cls") + val structureName = "structure" + + val structureItem = + SelectedItem.StructureItem(StructureInfo(componentName, structureName, emptyList())) + + val selectionItem = + SelectionItem( + appName = "app", + structure = structureName, + icon = mock(), + componentName = componentName, + uid = 0, + panelComponentName = null + ) + assertThat(selectionItem.matches(structureItem)).isTrue() + } + + @Test + fun testMatchSameComponentDifferentStructure_false() { + val componentName = ComponentName("pkg", "cls") + val structureName = "structure" + + val structureItem = + SelectedItem.StructureItem(StructureInfo(componentName, structureName, emptyList())) + + val selectionItem = + SelectionItem( + appName = "app", + structure = "other", + icon = mock(), + componentName = componentName, + uid = 0, + panelComponentName = null + ) + assertThat(selectionItem.matches(structureItem)).isFalse() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DeviceControlsTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DeviceControlsTileTest.kt index f7b9438e6535e..e0b3125fd62a8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DeviceControlsTileTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DeviceControlsTileTest.kt @@ -40,6 +40,7 @@ import com.android.systemui.controls.dagger.ControlsComponent import com.android.systemui.controls.management.ControlsListingController 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.plugins.ActivityStarter import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.qs.QSHost @@ -118,8 +119,9 @@ class DeviceControlsTileTest : SysuiTestCase() { `when`(qsHost.context).thenReturn(spiedContext) `when`(qsHost.uiEventLogger).thenReturn(uiEventLogger) `when`(controlsComponent.isEnabled()).thenReturn(true) - `when`(controlsController.getPreferredStructure()) - .thenReturn(StructureInfo(ComponentName("pkg", "cls"), "structure", listOf())) + `when`(controlsController.getPreferredSelection()) + .thenReturn(SelectedItem.StructureItem( + StructureInfo(ComponentName("pkg", "cls"), "structure", listOf()))) secureSettings.putInt(Settings.Secure.LOCKSCREEN_SHOW_CONTROLS, 1) setupControlsComponent() @@ -226,12 +228,12 @@ class DeviceControlsTileTest : SysuiTestCase() { capture(listingCallbackCaptor) ) `when`(controlsComponent.getVisibility()).thenReturn(ControlsComponent.Visibility.AVAILABLE) - `when`(controlsController.getPreferredStructure()).thenReturn( - StructureInfo( + `when`(controlsController.getPreferredSelection()).thenReturn( + SelectedItem.StructureItem(StructureInfo( ComponentName("pkg", "cls"), "structure", listOf(ControlInfo("id", "title", "subtitle", 1)) - ) + )) ) listingCallbackCaptor.value.onServicesUpdated(listOf(serviceInfo)) @@ -247,8 +249,9 @@ class DeviceControlsTileTest : SysuiTestCase() { capture(listingCallbackCaptor) ) `when`(controlsComponent.getVisibility()).thenReturn(ControlsComponent.Visibility.AVAILABLE) - `when`(controlsController.getPreferredStructure()) - .thenReturn(StructureInfo(ComponentName("pkg", "cls"), "structure", listOf())) + `when`(controlsController.getPreferredSelection()) + .thenReturn(SelectedItem.StructureItem( + StructureInfo(ComponentName("pkg", "cls"), "structure", listOf()))) listingCallbackCaptor.value.onServicesUpdated(listOf(serviceInfo)) testableLooper.processAllMessages() @@ -256,6 +259,22 @@ class DeviceControlsTileTest : SysuiTestCase() { assertThat(tile.state.state).isEqualTo(Tile.STATE_INACTIVE) } + @Test + fun testStateActiveIfPreferredIsPanel() { + verify(controlsListingController).observe( + any(LifecycleOwner::class.java), + capture(listingCallbackCaptor) + ) + `when`(controlsComponent.getVisibility()).thenReturn(ControlsComponent.Visibility.AVAILABLE) + `when`(controlsController.getPreferredSelection()) + .thenReturn(SelectedItem.PanelItem("appName", ComponentName("pkg", "cls"))) + + listingCallbackCaptor.value.onServicesUpdated(listOf(serviceInfo)) + testableLooper.processAllMessages() + + assertThat(tile.state.state).isEqualTo(Tile.STATE_ACTIVE) + } + @Test fun testStateInactiveIfLocked() { verify(controlsListingController).observe( @@ -303,12 +322,12 @@ class DeviceControlsTileTest : SysuiTestCase() { ) `when`(controlsComponent.getVisibility()).thenReturn(ControlsComponent.Visibility.AVAILABLE) `when`(controlsUiController.resolveActivity()).thenReturn(ControlsActivity::class.java) - `when`(controlsController.getPreferredStructure()).thenReturn( - StructureInfo( - ComponentName("pkg", "cls"), - "structure", - listOf(ControlInfo("id", "title", "subtitle", 1)) - ) + `when`(controlsController.getPreferredSelection()).thenReturn( + SelectedItem.StructureItem(StructureInfo( + ComponentName("pkg", "cls"), + "structure", + listOf(ControlInfo("id", "title", "subtitle", 1)) + )) ) listingCallbackCaptor.value.onServicesUpdated(listOf(serviceInfo)) @@ -334,12 +353,12 @@ class DeviceControlsTileTest : SysuiTestCase() { `when`(controlsComponent.getVisibility()) .thenReturn(ControlsComponent.Visibility.AVAILABLE_AFTER_UNLOCK) `when`(controlsUiController.resolveActivity()).thenReturn(ControlsActivity::class.java) - `when`(controlsController.getPreferredStructure()).thenReturn( - StructureInfo( + `when`(controlsController.getPreferredSelection()).thenReturn( + SelectedItem.StructureItem(StructureInfo( ComponentName("pkg", "cls"), "structure", listOf(ControlInfo("id", "title", "subtitle", 1)) - ) + )) ) listingCallbackCaptor.value.onServicesUpdated(listOf(serviceInfo))