From 64fdc5c527d60c146b56be236949d8ae8765c337 Mon Sep 17 00:00:00 2001 From: Fabian Kozynski Date: Mon, 14 Nov 2022 15:28:08 -0500 Subject: [PATCH] Add panel using TaskView Adds support for panel view. Controls that have favorites should migrate seamlessly to Panels (see SelectionItem#matches). A couple notes: * Apps with panels will not participate in seeding * If there is at least one app with panel, we show that instead of initial view. * If there is at least one app with panel, we don't open the app picker from the tile. * We save the last selection immediately so selectedItem is always in sync with SharedPreferences. * If we are seeing a panel, the overflow menu doesn't show Edit, and Add directs to the app picker. * The panel is shown inside a Red rectangle. This is temporary for debugging purposes. * Apps with panels are not shown in the Provider selector activity Note that if the flag (2000), no ControlsServiceInfo will have a panel component, and therefore there will be no panels, so we default to old behavior Test: manual Test: atest com.android.systemui.controls.ui Test: disable flag and see that everything behaves correctly, including scroll view Test: atest AppAdapterTest Fixes: 243392556 Change-Id: If260e193e57bec8948581dd4b0fa2f29e610c866 --- .../global_actions_controls_list_view.xml | 30 --- .../res/layout/controls_fullscreen.xml | 15 +- .../res/layout/controls_with_favorites.xml | 26 ++- .../controls/management/AppAdapter.kt | 3 +- .../systemui/controls/ui/ControlsActivity.kt | 7 + .../controls/ui/ControlsUiController.kt | 2 +- .../controls/ui/ControlsUiControllerImpl.kt | 203 +++++++++++++++--- .../controls/ui/PanelTaskViewController.kt | 102 +++++++++ .../globalactions/MinHeightScrollView.java | 43 ---- .../policy/DeviceControlsControllerImpl.kt | 3 + .../controls/management/AppAdapterTest.kt | 34 ++- .../ui/ControlsUiControllerImplTest.kt | 32 ++- .../ui/PanelTaskViewControllerTest.kt | 154 +++++++++++++ 13 files changed, 525 insertions(+), 129 deletions(-) delete mode 100644 packages/SystemUI/res/layout-sw600dp/global_actions_controls_list_view.xml create mode 100644 packages/SystemUI/src/com/android/systemui/controls/ui/PanelTaskViewController.kt delete mode 100644 packages/SystemUI/src/com/android/systemui/globalactions/MinHeightScrollView.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/controls/ui/PanelTaskViewControllerTest.kt 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/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 180e09e5aaf02..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<*> 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 905636f563d13..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,12 +37,14 @@ 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 @@ -49,6 +52,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.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 @@ -57,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 @@ -81,13 +90,14 @@ 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" @@ -107,6 +117,7 @@ class ControlsUiControllerImpl @Inject constructor ( 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, @@ -114,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() @@ -135,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 { @@ -165,10 +182,12 @@ class ControlsUiControllerImpl @Inject constructor ( override fun resolveActivity(): Class<*> { val allStructures = controlsController.get().getFavorites() val selected = getPreferredSelectedItem(allStructures) + val anyPanels = controlsListingController.get().getCurrentServices() + .none { it.panelActivity != null } return if (controlsController.get().addSeedingFavoritesCallback(onSeedingComplete)) { ControlsActivity::class.java - } else if (!selected.hasControls && allStructures.size <= 1) { + } else if (!selected.hasControls && allStructures.size <= 1 && !anyPanels) { ControlsProviderSelectorActivity::class.java } else { ControlsActivity::class.java @@ -194,9 +213,13 @@ class ControlsUiControllerImpl @Inject constructor ( if (controlsController.get().addSeedingFavoritesCallback(onSeedingComplete)) { listingCallback = createCallback(::showSeedingView) - } else if (!selectedItem.hasControls && 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 { val selected = selectedItem if (selected is SelectedItem.StructureItem) { @@ -213,11 +236,22 @@ class ControlsUiControllerImpl @Inject constructor ( 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)) @@ -297,29 +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(selectedItem, 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() { - if (selectedItem !is SelectedItem.StructureItem) return - val selectedStructure = (selectedItem as SelectedItem.StructureItem).structure - 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) @@ -340,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) } @@ -362,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 @@ -372,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( @@ -407,16 +518,20 @@ class ControlsUiControllerImpl @Inject constructor ( }) } - private fun createListView(selected: SelectionItem) { - if (selectedItem !is SelectedItem.StructureItem) return - val selectedStructure = (selectedItem as SelectedItem.StructureItem).structure - 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) @@ -490,18 +605,25 @@ class ControlsUiControllerImpl @Inject constructor ( .commit() } - private fun switchAppOrStructure(item: SelectionItem) { + private fun maybeUpdateSelectedItem(item: SelectionItem): Boolean { val newSelection = if (item.isPanel) { SelectedItem.PanelItem(item.appName, item.componentName) } else { - SelectedItem.StructureItem(allStructures.first { + SelectedItem.StructureItem(allStructures.firstOrNull { it.structure == item.structure && it.componentName == item.componentName - }) + } ?: EMPTY_STRUCTURE) } - - if (newSelection != selectedItem) { + return if (newSelection != selectedItem ) { selectedItem = newSelection updatePreferences(selectedItem) + true + } else { + false + } + } + + private fun switchAppOrStructure(item: SelectionItem) { + if (maybeUpdateSelectedItem(item)) { reload(parent) } } @@ -525,6 +647,8 @@ class ControlsUiControllerImpl @Inject constructor ( closeDialogs(true) controlsController.get().unsubscribe() + taskViewController?.dismiss() + taskViewController = null parent.removeAllViews() controlsById.clear() @@ -567,6 +691,15 @@ class ControlsUiControllerImpl @Inject constructor ( 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") + } + } } @VisibleForTesting 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/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 a86cb00b3a818..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,6 +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.never import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations @@ -62,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 }, @@ -81,12 +94,13 @@ class ControlsUiControllerImplTest : SysuiTestCase() { Lazy { controlsListingController }, controlActionCoordinator, activityStarter, - shadeController, iconCache, controlsMetricsLogger, keyguardStateController, userFileManager, - userTracker + userTracker, + Optional.of(taskViewFactory), + dumpManager ) `when`( userFileManager.getSharedPreferences( @@ -171,4 +185,18 @@ class ControlsUiControllerImplTest : SysuiTestCase() { 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() + } +}