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
This commit is contained in:
Fabian Kozynski
2022-11-14 15:28:08 -05:00
parent 82587b1a42
commit 64fdc5c527
13 changed files with 525 additions and 129 deletions

View File

@@ -1,30 +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.
-->
<com.android.systemui.globalactions.MinHeightScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:scrollbars="none">
<LinearLayout
android:id="@+id/global_actions_controls_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginLeft="@dimen/global_actions_side_margin"
android:layout_marginRight="@dimen/global_actions_side_margin" />
</com.android.systemui.globalactions.MinHeightScrollView>

View File

@@ -15,28 +15,19 @@
limitations under the License.
-->
<LinearLayout
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/control_detail_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.android.systemui.globalactions.MinHeightScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="vertical"
android:scrollbars="none">
<LinearLayout
android:id="@+id/global_actions_controls"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipChildren="false"
android:layout_height="match_parent"
android:orientation="vertical"
android:clipToPadding="false"
android:paddingHorizontal="@dimen/controls_padding_horizontal" />
</com.android.systemui.globalactions.MinHeightScrollView>
</LinearLayout>
</FrameLayout>

View File

@@ -18,7 +18,7 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="@dimen/controls_top_margin"
android:layout_marginBottom="@dimen/controls_header_bottom_margin">
@@ -71,5 +71,27 @@
android:background="?android:attr/selectableItemBackgroundBorderless" />
</LinearLayout>
<include layout="@layout/global_actions_controls_list_view" />
<ScrollView
android:id="@+id/controls_scroll_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="vertical"
android:clipChildren="true"
android:scrollbars="none">
<include layout="@layout/global_actions_controls_list_view" />
</ScrollView>
<FrameLayout
android:id="@+id/controls_panel"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginLeft="@dimen/global_actions_side_margin"
android:layout_marginRight="@dimen/global_actions_side_margin"
android:background="#ff0000"
android:padding="@dimen/global_actions_side_margin"
android:visibility="gone"
/>
</merge>

View File

@@ -64,7 +64,8 @@ class AppAdapter(
val localeComparator = compareBy<ControlsServiceInfo, CharSequence>(collator) {
it.loadLabel() ?: ""
}
listOfServices = serviceInfos.sortedWith(localeComparator)
listOfServices = serviceInfos.filter { it.panelActivity == null }
.sortedWith(localeComparator)
uiExecutor.execute(::notifyDataSetChanged)
}
}

View File

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

View File

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

View File

@@ -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<ControlsListingController>,
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<TaskViewFactory>,
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<SelectionItem>()
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<SelectionItem, CharSequence>(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<SelectionItem>) -> 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<SelectionItem>) {
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<SelectionItem>) {
controlViewsById.clear()
val itemsByComponent = items.associateBy { it.componentName }
val itemsWithStructure = mutableListOf<SelectionItem>()
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<SelectionItem>()
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<View>(R.id.controls_scroll_view).visibility = View.GONE
val container = parent.requireViewById<FrameLayout>(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<String>(context, R.layout.controls_more_item, items)
val anchor = parent.requireViewById<ImageView>(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<ViewGroup>(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<ImageView>(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>): SelectionItem? =
items.firstOrNull { it.matches(si) }
override fun dump(pw: PrintWriter, args: Array<out String>) {
pw.println("ControlsUiControllerImpl:")
pw.asIndenting().indentIfPossible {
println("hidden: $hidden")
println("selectedItem: $selectedItem")
println("lastSelections: $lastSelections")
}
}
}
@VisibleForTesting

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<TaskView.Listener>
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<Intent>()
val optionsCaptor = argumentCaptor<ActivityOptions>()
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()
}
}