Merge changes from topic "258442632" into tm-qpr-dev

* changes:
  Add panel using TaskView
  Distinguish between selected panel or structure
This commit is contained in:
Fabian Kozynski
2022-11-22 14:34:54 +00:00
committed by Android (Google) Code Review
19 changed files with 835 additions and 209 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. limitations under the License.
--> -->
<LinearLayout <FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/control_detail_root" android:id="@+id/control_detail_root"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> 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 <LinearLayout
android:id="@+id/global_actions_controls" android:id="@+id/global_actions_controls"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:clipChildren="false"
android:orientation="vertical" android:orientation="vertical"
android:clipToPadding="false"
android:paddingHorizontal="@dimen/controls_padding_horizontal" /> android:paddingHorizontal="@dimen/controls_padding_horizontal" />
</com.android.systemui.globalactions.MinHeightScrollView> </FrameLayout>
</LinearLayout>

View File

@@ -18,7 +18,7 @@
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
android:layout_marginTop="@dimen/controls_top_margin" android:layout_marginTop="@dimen/controls_top_margin"
android:layout_marginBottom="@dimen/controls_header_bottom_margin"> android:layout_marginBottom="@dimen/controls_header_bottom_margin">
@@ -71,5 +71,27 @@
android:background="?android:attr/selectableItemBackgroundBorderless" /> android:background="?android:attr/selectableItemBackgroundBorderless" />
</LinearLayout> </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> </merge>

View File

@@ -24,6 +24,7 @@ import com.android.systemui.controls.ControlStatus
import com.android.systemui.util.UserAwareController import com.android.systemui.util.UserAwareController
import com.android.systemui.controls.management.ControlsFavoritingActivity import com.android.systemui.controls.management.ControlsFavoritingActivity
import com.android.systemui.controls.ui.ControlsUiController import com.android.systemui.controls.ui.ControlsUiController
import com.android.systemui.controls.ui.SelectedItem
import java.util.function.Consumer import java.util.function.Consumer
/** /**
@@ -184,8 +185,8 @@ interface ControlsController : UserAwareController {
*/ */
fun countFavoritesForComponent(componentName: ComponentName): Int fun countFavoritesForComponent(componentName: ComponentName): Int
/** See [ControlsUiController.getPreferredStructure]. */ /** See [ControlsUiController.getPreferredSelectedItem]. */
fun getPreferredStructure(): StructureInfo fun getPreferredSelection(): SelectedItem
/** /**
* Interface for structure to pass data to [ControlsFavoritingActivity]. * Interface for structure to pass data to [ControlsFavoritingActivity].

View File

@@ -38,6 +38,7 @@ import com.android.systemui.controls.ControlStatus
import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.controls.ControlsServiceInfo
import com.android.systemui.controls.management.ControlsListingController import com.android.systemui.controls.management.ControlsListingController
import com.android.systemui.controls.ui.ControlsUiController 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.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dump.DumpManager import com.android.systemui.dump.DumpManager
@@ -556,8 +557,8 @@ class ControlsControllerImpl @Inject constructor (
) )
} }
override fun getPreferredStructure(): StructureInfo { override fun getPreferredSelection(): SelectedItem {
return uiController.getPreferredStructure(getFavorites()) return uiController.getPreferredSelectedItem(getFavorites())
} }
override fun dump(pw: PrintWriter, args: Array<out String>) { override fun dump(pw: PrintWriter, args: Array<out String>) {

View File

@@ -31,4 +31,9 @@ data class StructureInfo(
val componentName: ComponentName, val componentName: ComponentName,
val structure: CharSequence, val structure: CharSequence,
val controls: List<ControlInfo> val controls: List<ControlInfo>
) ) {
companion object {
val EMPTY_COMPONENT = ComponentName("", "")
val EMPTY_STRUCTURE = StructureInfo(EMPTY_COMPONENT, "", mutableListOf())
}
}

View File

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

View File

@@ -27,10 +27,13 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.WindowInsets import android.view.WindowInsets
import android.view.WindowInsets.Type import android.view.WindowInsets.Type
import android.view.WindowManager
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import com.android.systemui.R import com.android.systemui.R
import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.controls.management.ControlsAnimations import com.android.systemui.controls.management.ControlsAnimations
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import javax.inject.Inject import javax.inject.Inject
/** /**
@@ -44,6 +47,7 @@ class ControlsActivity @Inject constructor(
private val uiController: ControlsUiController, private val uiController: ControlsUiController,
private val broadcastDispatcher: BroadcastDispatcher, private val broadcastDispatcher: BroadcastDispatcher,
private val dreamManager: IDreamManager, private val dreamManager: IDreamManager,
private val featureFlags: FeatureFlags
) : ComponentActivity() { ) : ComponentActivity() {
private lateinit var parent: ViewGroup private lateinit var parent: ViewGroup
@@ -52,6 +56,9 @@ class ControlsActivity @Inject constructor(
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
if (featureFlags.isEnabled(Flags.USE_APP_PANELS)) {
window.addPrivateFlags(WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY)
}
setContentView(R.layout.controls_fullscreen) 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 * 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<*> 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>): StructureInfo fun getPreferredSelectedItem(structures: List<StructureInfo>): 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)
}
} }

View File

@@ -21,6 +21,7 @@ import android.animation.AnimatorListenerAdapter
import android.animation.ObjectAnimator import android.animation.ObjectAnimator
import android.app.Activity import android.app.Activity
import android.app.ActivityOptions import android.app.ActivityOptions
import android.app.PendingIntent
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@@ -36,18 +37,22 @@ import android.view.animation.AccelerateInterpolator
import android.view.animation.DecelerateInterpolator import android.view.animation.DecelerateInterpolator
import android.widget.AdapterView import android.widget.AdapterView
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.FrameLayout
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.ListPopupWindow import android.widget.ListPopupWindow
import android.widget.Space import android.widget.Space
import android.widget.TextView import android.widget.TextView
import androidx.annotation.VisibleForTesting
import com.android.systemui.Dumpable
import com.android.systemui.R import com.android.systemui.R
import com.android.systemui.controls.ControlsMetricsLogger import com.android.systemui.controls.ControlsMetricsLogger
import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.controls.ControlsServiceInfo
import com.android.systemui.controls.CustomIconCache 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.ControlsController
import com.android.systemui.controls.controller.StructureInfo 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.ControlAdapter
import com.android.systemui.controls.management.ControlsEditingActivity import com.android.systemui.controls.management.ControlsEditingActivity
import com.android.systemui.controls.management.ControlsFavoritingActivity 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.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dump.DumpManager
import com.android.systemui.globalactions.GlobalActionsPopupMenu import com.android.systemui.globalactions.GlobalActionsPopupMenu
import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserFileManager
import com.android.systemui.settings.UserTracker 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.DeviceControlsControllerImpl
import com.android.systemui.statusbar.policy.KeyguardStateController 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.concurrency.DelayableExecutor
import com.android.systemui.util.indentIfPossible
import com.android.wm.shell.TaskViewFactory
import dagger.Lazy import dagger.Lazy
import java.io.PrintWriter
import java.text.Collator import java.text.Collator
import java.util.Optional
import java.util.function.Consumer import java.util.function.Consumer
import javax.inject.Inject import javax.inject.Inject
@@ -80,39 +90,34 @@ class ControlsUiControllerImpl @Inject constructor (
val controlsListingController: Lazy<ControlsListingController>, val controlsListingController: Lazy<ControlsListingController>,
val controlActionCoordinator: ControlActionCoordinator, val controlActionCoordinator: ControlActionCoordinator,
private val activityStarter: ActivityStarter, private val activityStarter: ActivityStarter,
private val shadeController: ShadeController,
private val iconCache: CustomIconCache, private val iconCache: CustomIconCache,
private val controlsMetricsLogger: ControlsMetricsLogger, private val controlsMetricsLogger: ControlsMetricsLogger,
private val keyguardStateController: KeyguardStateController, private val keyguardStateController: KeyguardStateController,
private val userFileManager: UserFileManager, private val userFileManager: UserFileManager,
private val userTracker: UserTracker, private val userTracker: UserTracker,
) : ControlsUiController { private val taskViewFactory: Optional<TaskViewFactory>,
dumpManager: DumpManager
) : ControlsUiController, Dumpable {
companion object { companion object {
private const val PREF_COMPONENT = "controls_component" 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 const val FADE_IN_MILLIS = 200L
private val EMPTY_COMPONENT = ComponentName("", "")
private val EMPTY_STRUCTURE = StructureInfo(
EMPTY_COMPONENT,
"",
mutableListOf<ControlInfo>()
)
} }
private var selectedStructure: StructureInfo = EMPTY_STRUCTURE private var selectedItem: SelectedItem = SelectedItem.EMPTY_SELECTION
private lateinit var allStructures: List<StructureInfo> private lateinit var allStructures: List<StructureInfo>
private val controlsById = mutableMapOf<ControlKey, ControlWithState>() private val controlsById = mutableMapOf<ControlKey, ControlWithState>()
private val controlViewsById = mutableMapOf<ControlKey, ControlViewHolder>() private val controlViewsById = mutableMapOf<ControlKey, ControlViewHolder>()
private lateinit var parent: ViewGroup private lateinit var parent: ViewGroup
private lateinit var lastItems: List<SelectionItem>
private var popup: ListPopupWindow? = null private var popup: ListPopupWindow? = null
private var hidden = true private var hidden = true
private lateinit var onDismiss: Runnable private lateinit var onDismiss: Runnable
private val popupThemedContext = ContextThemeWrapper(context, R.style.Control_ListPopupWindow) private val popupThemedContext = ContextThemeWrapper(context, R.style.Control_ListPopupWindow)
private var retainCache = false private var retainCache = false
private var lastSelections = emptyList<SelectionItem>()
private val sharedPreferences private val sharedPreferences
get() = userFileManager.getSharedPreferences( get() = userFileManager.getSharedPreferences(
fileName = DeviceControlsControllerImpl.PREFS_CONTROLS_FILE, fileName = DeviceControlsControllerImpl.PREFS_CONTROLS_FILE,
@@ -120,6 +125,8 @@ class ControlsUiControllerImpl @Inject constructor (
userId = userTracker.userId userId = userTracker.userId
) )
private var taskViewController: PanelTaskViewController? = null
private val collator = Collator.getInstance(context.resources.configuration.locales[0]) private val collator = Collator.getInstance(context.resources.configuration.locales[0])
private val localeComparator = compareBy<SelectionItem, CharSequence>(collator) { private val localeComparator = compareBy<SelectionItem, CharSequence>(collator) {
it.getTitle() it.getTitle()
@@ -128,10 +135,12 @@ class ControlsUiControllerImpl @Inject constructor (
private val onSeedingComplete = Consumer<Boolean> { private val onSeedingComplete = Consumer<Boolean> {
accepted -> accepted ->
if (accepted) { if (accepted) {
selectedStructure = controlsController.get().getFavorites().maxByOrNull { selectedItem = controlsController.get().getFavorites().maxByOrNull {
it.controls.size it.controls.size
} ?: EMPTY_STRUCTURE }?.let {
updatePreferences(selectedStructure) SelectedItem.StructureItem(it)
} ?: SelectedItem.EMPTY_SELECTION
updatePreferences(selectedItem)
} }
reload(parent) reload(parent)
} }
@@ -139,6 +148,10 @@ class ControlsUiControllerImpl @Inject constructor (
private lateinit var activityContext: Context private lateinit var activityContext: Context
private lateinit var listingCallback: ControlsListingController.ControlsListingCallback private lateinit var listingCallback: ControlsListingController.ControlsListingCallback
init {
dumpManager.registerDumpable(javaClass.name, this)
}
private fun createCallback( private fun createCallback(
onResult: (List<SelectionItem>) -> Unit onResult: (List<SelectionItem>) -> Unit
): ControlsListingController.ControlsListingCallback { ): ControlsListingController.ControlsListingCallback {
@@ -146,7 +159,15 @@ class ControlsUiControllerImpl @Inject constructor (
override fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>) { override fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>) {
val lastItems = serviceInfos.map { val lastItems = serviceInfos.map {
val uid = it.serviceInfo.applicationInfo.uid 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 { uiExecutor.execute {
parent.removeAllViews() parent.removeAllViews()
@@ -160,11 +181,13 @@ class ControlsUiControllerImpl @Inject constructor (
override fun resolveActivity(): Class<*> { override fun resolveActivity(): Class<*> {
val allStructures = controlsController.get().getFavorites() 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)) { return if (controlsController.get().addSeedingFavoritesCallback(onSeedingComplete)) {
ControlsActivity::class.java ControlsActivity::class.java
} else if (selectedStructure.controls.isEmpty() && allStructures.size <= 1) { } else if (!selected.hasControls && allStructures.size <= 1 && !anyPanels) {
ControlsProviderSelectorActivity::class.java ControlsProviderSelectorActivity::class.java
} else { } else {
ControlsActivity::class.java ControlsActivity::class.java
@@ -186,31 +209,49 @@ class ControlsUiControllerImpl @Inject constructor (
controlActionCoordinator.activityContext = activityContext controlActionCoordinator.activityContext = activityContext
allStructures = controlsController.get().getFavorites() allStructures = controlsController.get().getFavorites()
selectedStructure = getPreferredStructure(allStructures) selectedItem = getPreferredSelectedItem(allStructures)
if (controlsController.get().addSeedingFavoritesCallback(onSeedingComplete)) { if (controlsController.get().addSeedingFavoritesCallback(onSeedingComplete)) {
listingCallback = createCallback(::showSeedingView) 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 // only show initial view if there are really no favorites across any structure
listingCallback = createCallback(::showInitialSetupView) listingCallback = createCallback(::initialView)
} else { } else {
selectedStructure.controls.map { val selected = selectedItem
ControlWithState(selectedStructure.componentName, it, null) if (selected is SelectedItem.StructureItem) {
}.associateByTo(controlsById) { selected.structure.controls.map {
ControlKey(selectedStructure.componentName, it.ci.controlId) ControlWithState(selected.structure.componentName, it, null)
}.associateByTo(controlsById) {
ControlKey(selected.structure.componentName, it.ci.controlId)
}
controlsController.get().subscribeToFavorites(selected.structure)
} }
listingCallback = createCallback(::showControlsView) listingCallback = createCallback(::showControlsView)
controlsController.get().subscribeToFavorites(selectedStructure)
} }
controlsListingController.get().addCallback(listingCallback) 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) { private fun reload(parent: ViewGroup) {
if (hidden) return if (hidden) return
controlsListingController.get().removeCallback(listingCallback) controlsListingController.get().removeCallback(listingCallback)
controlsController.get().unsubscribe() controlsController.get().unsubscribe()
taskViewController?.dismiss()
taskViewController = null
val fadeAnim = ObjectAnimator.ofFloat(parent, "alpha", 1.0f, 0.0f) val fadeAnim = ObjectAnimator.ofFloat(parent, "alpha", 1.0f, 0.0f)
fadeAnim.setInterpolator(AccelerateInterpolator(1.0f)) fadeAnim.setInterpolator(AccelerateInterpolator(1.0f))
@@ -290,27 +331,90 @@ class ControlsUiControllerImpl @Inject constructor (
private fun showControlsView(items: List<SelectionItem>) { private fun showControlsView(items: List<SelectionItem>) {
controlViewsById.clear() controlViewsById.clear()
val itemsByComponent = items.associateBy { it.componentName } val (panels, structures) = items.partition { it.isPanel }
val itemsWithStructure = mutableListOf<SelectionItem>() val panelComponents = panels.map { it.componentName }.toSet()
allStructures.mapNotNullTo(itemsWithStructure) {
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) 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) val selectionItem = findSelectionItem(selectedItem, panelsAndStructures)
createDropDown(itemsWithStructure, selectionItem) ?: 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() createMenu()
} }
private fun createMenu() { private fun createPanelView(componentName: ComponentName) {
val items = arrayOf( val pendingIntent = PendingIntent.getActivity(
context.resources.getString(R.string.controls_menu_add), context,
context.resources.getString(R.string.controls_menu_edit) 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) var adapter = ArrayAdapter<String>(context, R.layout.controls_more_item, items)
val anchor = parent.requireViewById<ImageView>(R.id.controls_more) val anchor = parent.requireViewById<ImageView>(R.id.controls_more)
@@ -331,7 +435,13 @@ class ControlsUiControllerImpl @Inject constructor (
) { ) {
when (pos) { when (pos) {
// 0: Add Control // 0: Add Control
0 -> startFavoritingActivity(selectedStructure) 0 -> {
if (isPanel) {
startProviderSelectorActivity()
} else {
startFavoritingActivity(selectedStructure)
}
}
// 1: Edit controls // 1: Edit controls
1 -> startEditingActivity(selectedStructure) 1 -> startEditingActivity(selectedStructure)
} }
@@ -353,6 +463,9 @@ class ControlsUiControllerImpl @Inject constructor (
addAll(items) 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 * Default spinner widget does not work with the window type required
* for this dialog. Use a textView with the ListPopupWindow to achieve * 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 // override the default color on the dropdown drawable
(getBackground() as LayerDrawable).getDrawable(0) (getBackground() as LayerDrawable).getDrawable(0)
.setTint(context.resources.getColor(R.color.control_spinner_dropdown, null)) .setTint(context.resources.getColor(R.color.control_spinner_dropdown, null))
} selected.icon.setBounds(0, 0, iconSize, iconSize)
compoundDrawablePadding = (iconSize / 2.4f).toInt()
if (items.size == 1) { setCompoundDrawablesRelative(selected.icon, null, null, null)
spinner.setBackground(null)
return
} }
val anchor = parent.requireViewById<ViewGroup>(R.id.controls_header) 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 { anchor.setOnClickListener(object : View.OnClickListener {
override fun onClick(v: View) { override fun onClick(v: View) {
popup = GlobalActionsPopupMenu( popup = GlobalActionsPopupMenu(
@@ -398,14 +518,20 @@ class ControlsUiControllerImpl @Inject constructor (
}) })
} }
private fun createListView(selected: SelectionItem) { private fun createControlsSpaceFrame() {
val inflater = LayoutInflater.from(context) val inflater = LayoutInflater.from(activityContext)
inflater.inflate(R.layout.controls_with_favorites, parent, true) inflater.inflate(R.layout.controls_with_favorites, parent, true)
parent.requireViewById<ImageView>(R.id.controls_close).apply { parent.requireViewById<ImageView>(R.id.controls_close).apply {
setOnClickListener { _: View -> onDismiss.run() } setOnClickListener { _: View -> onDismiss.run() }
visibility = View.VISIBLE 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) val maxColumns = ControlAdapter.findMaxColumns(activityContext.resources)
@@ -453,35 +579,51 @@ class ControlsUiControllerImpl @Inject constructor (
} }
} }
override fun getPreferredStructure(structures: List<StructureInfo>): StructureInfo { override fun getPreferredSelectedItem(structures: List<StructureInfo>): SelectedItem {
if (structures.isEmpty()) return EMPTY_STRUCTURE val sp = sharedPreferences
val component = sharedPreferences.getString(PREF_COMPONENT, null)?.let { val component = sp.getString(PREF_COMPONENT, null)?.let {
ComponentName.unflattenFromString(it) ComponentName.unflattenFromString(it)
} ?: EMPTY_COMPONENT } ?: EMPTY_COMPONENT
val structure = sharedPreferences.getString(PREF_STRUCTURE, "") val name = sp.getString(PREF_STRUCTURE_OR_APP_NAME, "")!!
val isPanel = sp.getBoolean(PREF_IS_PANEL, false)
return structures.firstOrNull { return if (isPanel) {
component == it.componentName && structure == it.structure SelectedItem.PanelItem(name, component)
} ?: structures.get(0) } 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) { private fun updatePreferences(si: SelectedItem) {
if (si == EMPTY_STRUCTURE) return
sharedPreferences.edit() sharedPreferences.edit()
.putString(PREF_COMPONENT, si.componentName.flattenToString()) .putString(PREF_COMPONENT, si.componentName.flattenToString())
.putString(PREF_STRUCTURE, si.structure.toString()) .putString(PREF_STRUCTURE_OR_APP_NAME, si.name.toString())
.commit() .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) { private fun switchAppOrStructure(item: SelectionItem) {
val newSelection = allStructures.first { if (maybeUpdateSelectedItem(item)) {
it.structure == item.structure && it.componentName == item.componentName
}
if (newSelection != selectedStructure) {
selectedStructure = newSelection
updatePreferences(selectedStructure)
reload(parent) reload(parent)
} }
} }
@@ -505,6 +647,8 @@ class ControlsUiControllerImpl @Inject constructor (
closeDialogs(true) closeDialogs(true)
controlsController.get().unsubscribe() controlsController.get().unsubscribe()
taskViewController?.dismiss()
taskViewController = null
parent.removeAllViews() parent.removeAllViews()
controlsById.clear() controlsById.clear()
@@ -545,20 +689,46 @@ class ControlsUiControllerImpl @Inject constructor (
return row return row
} }
private fun findSelectionItem(si: StructureInfo, items: List<SelectionItem>): SelectionItem? = private fun findSelectionItem(si: SelectedItem, items: List<SelectionItem>): SelectionItem? =
items.firstOrNull { items.firstOrNull { it.matches(si) }
it.componentName == si.componentName && it.structure == si.structure
override fun dump(pw: PrintWriter, args: Array<out String>) {
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 appName: CharSequence,
val structure: CharSequence, val structure: CharSequence,
val icon: Drawable, val icon: Drawable,
val componentName: ComponentName, val componentName: ComponentName,
val uid: Int val uid: Int,
val panelComponentName: ComponentName?
) { ) {
fun getTitle() = if (structure.isEmpty()) { appName } else { structure } 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( private class ItemAdapter(

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

@@ -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.dagger.ControlsComponent.Visibility.AVAILABLE
import com.android.systemui.controls.management.ControlsListingController import com.android.systemui.controls.management.ControlsListingController
import com.android.systemui.controls.ui.ControlsUiController import com.android.systemui.controls.ui.ControlsUiController
import com.android.systemui.controls.ui.SelectedItem
import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.ActivityStarter
@@ -125,14 +126,15 @@ class DeviceControlsTile @Inject constructor(
state.icon = icon state.icon = icon
if (controlsComponent.isEnabled() && hasControlsApps.get()) { if (controlsComponent.isEnabled() && hasControlsApps.get()) {
if (controlsComponent.getVisibility() == AVAILABLE) { if (controlsComponent.getVisibility() == AVAILABLE) {
val structureInfo = controlsComponent val selection = controlsComponent
.getControlsController().get().getPreferredStructure() .getControlsController().get().getPreferredSelection()
state.state = if (structureInfo.controls.isEmpty()) { state.state = if (selection is SelectedItem.StructureItem &&
selection.structure.controls.isEmpty()) {
Tile.STATE_INACTIVE Tile.STATE_INACTIVE
} else { } else {
Tile.STATE_ACTIVE Tile.STATE_ACTIVE
} }
val label = structureInfo.structure val label = selection.name
state.secondaryLabel = if (label == tileLabel) null else label state.secondaryLabel = if (label == tileLabel) null else label
} else { } else {
state.state = Tile.STATE_INACTIVE state.state = Tile.STATE_INACTIVE

View File

@@ -140,6 +140,9 @@ public class DeviceControlsControllerImpl @Inject constructor(
// is out of sync, perhaps through a device restore, and update the // is out of sync, perhaps through a device restore, and update the
// preference // preference
addPackageToSeededSet(prefs, pkg) addPackageToSeededSet(prefs, pkg)
} else if (it.panelActivity != null) {
// Do not seed for packages with panels
addPackageToSeededSet(prefs, pkg)
} else { } else {
componentsToSeed.add(it.componentName) 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.controls.ControlsServiceInfo
import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.time.FakeSystemClock import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import org.junit.Before import org.junit.Before
@@ -34,9 +35,8 @@ import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor import org.mockito.ArgumentCaptor
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` import org.mockito.Mockito.`when`
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations import org.mockito.MockitoAnnotations
@SmallTest @SmallTest
@@ -70,7 +70,7 @@ class AppAdapterTest : SysuiTestCase() {
fun testOnServicesUpdated_nullLoadLabel() { fun testOnServicesUpdated_nullLoadLabel() {
val captor = ArgumentCaptor val captor = ArgumentCaptor
.forClass(ControlsListingController.ControlsListingCallback::class.java) .forClass(ControlsListingController.ControlsListingCallback::class.java)
val controlsServiceInfo = mock(ControlsServiceInfo::class.java) val controlsServiceInfo = mock<ControlsServiceInfo>()
val serviceInfo = listOf(controlsServiceInfo) val serviceInfo = listOf(controlsServiceInfo)
`when`(controlsServiceInfo.loadLabel()).thenReturn(null) `when`(controlsServiceInfo.loadLabel()).thenReturn(null)
verify(controlsListingController).observe(any(Lifecycle::class.java), captor.capture()) verify(controlsListingController).observe(any(Lifecycle::class.java), captor.capture())
@@ -81,4 +81,32 @@ class AppAdapterTest : SysuiTestCase() {
assertThat(adapter.itemCount).isEqualTo(serviceInfo.size) 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 package com.android.systemui.controls.ui
import android.content.ComponentName import android.content.ComponentName
import android.content.Context
import android.testing.AndroidTestingRunner import android.testing.AndroidTestingRunner
import android.testing.TestableLooper import android.testing.TestableLooper
import android.widget.FrameLayout
import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestCase
import com.android.systemui.controls.ControlsMetricsLogger 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.ControlsController
import com.android.systemui.controls.controller.StructureInfo import com.android.systemui.controls.controller.StructureInfo
import com.android.systemui.controls.management.ControlsListingController import com.android.systemui.controls.management.ControlsListingController
import com.android.systemui.dump.DumpManager
import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserFileManager
import com.android.systemui.settings.UserTracker import com.android.systemui.settings.UserTracker
@@ -34,9 +37,12 @@ import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl
import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.FakeSharedPreferences import com.android.systemui.util.FakeSharedPreferences
import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any
import com.android.systemui.util.time.FakeSystemClock import com.android.systemui.util.time.FakeSystemClock
import com.android.wm.shell.TaskViewFactory
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import dagger.Lazy import dagger.Lazy
import java.util.Optional
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@@ -44,7 +50,7 @@ import org.mockito.Mock
import org.mockito.Mockito.anyInt import org.mockito.Mockito.anyInt
import org.mockito.Mockito.anyString import org.mockito.Mockito.anyString
import org.mockito.Mockito.mock import org.mockito.Mockito.mock
import org.mockito.Mockito.times import org.mockito.Mockito.never
import org.mockito.Mockito.verify import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations import org.mockito.MockitoAnnotations
@@ -63,16 +69,22 @@ class ControlsUiControllerImplTest : SysuiTestCase() {
@Mock lateinit var keyguardStateController: KeyguardStateController @Mock lateinit var keyguardStateController: KeyguardStateController
@Mock lateinit var userFileManager: UserFileManager @Mock lateinit var userFileManager: UserFileManager
@Mock lateinit var userTracker: UserTracker @Mock lateinit var userTracker: UserTracker
@Mock lateinit var taskViewFactory: TaskViewFactory
@Mock lateinit var activityContext: Context
@Mock lateinit var dumpManager: DumpManager
val sharedPreferences = FakeSharedPreferences() val sharedPreferences = FakeSharedPreferences()
var uiExecutor = FakeExecutor(FakeSystemClock()) var uiExecutor = FakeExecutor(FakeSystemClock())
var bgExecutor = FakeExecutor(FakeSystemClock()) var bgExecutor = FakeExecutor(FakeSystemClock())
lateinit var underTest: ControlsUiControllerImpl lateinit var underTest: ControlsUiControllerImpl
lateinit var parent: FrameLayout
@Before @Before
fun setup() { fun setup() {
MockitoAnnotations.initMocks(this) MockitoAnnotations.initMocks(this)
parent = FrameLayout(mContext)
underTest = underTest =
ControlsUiControllerImpl( ControlsUiControllerImpl(
Lazy { controlsController }, Lazy { controlsController },
@@ -82,12 +94,13 @@ class ControlsUiControllerImplTest : SysuiTestCase() {
Lazy { controlsListingController }, Lazy { controlsListingController },
controlActionCoordinator, controlActionCoordinator,
activityStarter, activityStarter,
shadeController,
iconCache, iconCache,
controlsMetricsLogger, controlsMetricsLogger,
keyguardStateController, keyguardStateController,
userFileManager, userFileManager,
userTracker userTracker,
Optional.of(taskViewFactory),
dumpManager
) )
`when`( `when`(
userFileManager.getSharedPreferences( userFileManager.getSharedPreferences(
@@ -105,8 +118,8 @@ class ControlsUiControllerImplTest : SysuiTestCase() {
@Test @Test
fun testGetPreferredStructure() { fun testGetPreferredStructure() {
val structureInfo = mock(StructureInfo::class.java) val structureInfo = mock(StructureInfo::class.java)
underTest.getPreferredStructure(listOf(structureInfo)) underTest.getPreferredSelectedItem(listOf(structureInfo))
verify(userFileManager, times(2)) verify(userFileManager)
.getSharedPreferences( .getSharedPreferences(
fileName = DeviceControlsControllerImpl.PREFS_CONTROLS_FILE, fileName = DeviceControlsControllerImpl.PREFS_CONTROLS_FILE,
mode = 0, mode = 0,
@@ -116,25 +129,30 @@ class ControlsUiControllerImplTest : SysuiTestCase() {
@Test @Test
fun testGetPreferredStructure_differentUserId() { fun testGetPreferredStructure_differentUserId() {
val structureInfo = val selectedItems =
listOf( listOf(
StructureInfo(ComponentName.unflattenFromString("pkg/.cls1"), "a", ArrayList()), SelectedItem.StructureItem(
StructureInfo(ComponentName.unflattenFromString("pkg/.cls2"), "b", ArrayList()), StructureInfo(ComponentName.unflattenFromString("pkg/.cls1"), "a", ArrayList())
),
SelectedItem.StructureItem(
StructureInfo(ComponentName.unflattenFromString("pkg/.cls2"), "b", ArrayList())
),
) )
val structures = selectedItems.map { it.structure }
sharedPreferences sharedPreferences
.edit() .edit()
.putString("controls_component", structureInfo[0].componentName.flattenToString()) .putString("controls_component", selectedItems[0].componentName.flattenToString())
.putString("controls_structure", structureInfo[0].structure.toString()) .putString("controls_structure", selectedItems[0].name.toString())
.commit() .commit()
val differentSharedPreferences = FakeSharedPreferences() val differentSharedPreferences = FakeSharedPreferences()
differentSharedPreferences differentSharedPreferences
.edit() .edit()
.putString("controls_component", structureInfo[1].componentName.flattenToString()) .putString("controls_component", selectedItems[1].componentName.flattenToString())
.putString("controls_structure", structureInfo[1].structure.toString()) .putString("controls_structure", selectedItems[1].name.toString())
.commit() .commit()
val previousPreferredStructure = underTest.getPreferredStructure(structureInfo) val previousPreferredStructure = underTest.getPreferredSelectedItem(structures)
`when`( `when`(
userFileManager.getSharedPreferences( userFileManager.getSharedPreferences(
@@ -146,10 +164,39 @@ class ControlsUiControllerImplTest : SysuiTestCase() {
.thenReturn(differentSharedPreferences) .thenReturn(differentSharedPreferences)
`when`(userTracker.userId).thenReturn(1) `when`(userTracker.userId).thenReturn(1)
val currentPreferredStructure = underTest.getPreferredStructure(structureInfo) val currentPreferredStructure = underTest.getPreferredSelectedItem(structures)
assertThat(previousPreferredStructure).isEqualTo(structureInfo[0]) assertThat(previousPreferredStructure).isEqualTo(selectedItems[0])
assertThat(currentPreferredStructure).isEqualTo(structureInfo[1]) assertThat(currentPreferredStructure).isEqualTo(selectedItems[1])
assertThat(currentPreferredStructure).isNotEqualTo(previousPreferredStructure) 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())
}
} }

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

View File

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

View File

@@ -40,6 +40,7 @@ import com.android.systemui.controls.dagger.ControlsComponent
import com.android.systemui.controls.management.ControlsListingController import com.android.systemui.controls.management.ControlsListingController
import com.android.systemui.controls.ui.ControlsActivity import com.android.systemui.controls.ui.ControlsActivity
import com.android.systemui.controls.ui.ControlsUiController import com.android.systemui.controls.ui.ControlsUiController
import com.android.systemui.controls.ui.SelectedItem
import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.qs.QSHost import com.android.systemui.qs.QSHost
@@ -118,8 +119,9 @@ class DeviceControlsTileTest : SysuiTestCase() {
`when`(qsHost.context).thenReturn(spiedContext) `when`(qsHost.context).thenReturn(spiedContext)
`when`(qsHost.uiEventLogger).thenReturn(uiEventLogger) `when`(qsHost.uiEventLogger).thenReturn(uiEventLogger)
`when`(controlsComponent.isEnabled()).thenReturn(true) `when`(controlsComponent.isEnabled()).thenReturn(true)
`when`(controlsController.getPreferredStructure()) `when`(controlsController.getPreferredSelection())
.thenReturn(StructureInfo(ComponentName("pkg", "cls"), "structure", listOf())) .thenReturn(SelectedItem.StructureItem(
StructureInfo(ComponentName("pkg", "cls"), "structure", listOf())))
secureSettings.putInt(Settings.Secure.LOCKSCREEN_SHOW_CONTROLS, 1) secureSettings.putInt(Settings.Secure.LOCKSCREEN_SHOW_CONTROLS, 1)
setupControlsComponent() setupControlsComponent()
@@ -226,12 +228,12 @@ class DeviceControlsTileTest : SysuiTestCase() {
capture(listingCallbackCaptor) capture(listingCallbackCaptor)
) )
`when`(controlsComponent.getVisibility()).thenReturn(ControlsComponent.Visibility.AVAILABLE) `when`(controlsComponent.getVisibility()).thenReturn(ControlsComponent.Visibility.AVAILABLE)
`when`(controlsController.getPreferredStructure()).thenReturn( `when`(controlsController.getPreferredSelection()).thenReturn(
StructureInfo( SelectedItem.StructureItem(StructureInfo(
ComponentName("pkg", "cls"), ComponentName("pkg", "cls"),
"structure", "structure",
listOf(ControlInfo("id", "title", "subtitle", 1)) listOf(ControlInfo("id", "title", "subtitle", 1))
) ))
) )
listingCallbackCaptor.value.onServicesUpdated(listOf(serviceInfo)) listingCallbackCaptor.value.onServicesUpdated(listOf(serviceInfo))
@@ -247,8 +249,9 @@ class DeviceControlsTileTest : SysuiTestCase() {
capture(listingCallbackCaptor) capture(listingCallbackCaptor)
) )
`when`(controlsComponent.getVisibility()).thenReturn(ControlsComponent.Visibility.AVAILABLE) `when`(controlsComponent.getVisibility()).thenReturn(ControlsComponent.Visibility.AVAILABLE)
`when`(controlsController.getPreferredStructure()) `when`(controlsController.getPreferredSelection())
.thenReturn(StructureInfo(ComponentName("pkg", "cls"), "structure", listOf())) .thenReturn(SelectedItem.StructureItem(
StructureInfo(ComponentName("pkg", "cls"), "structure", listOf())))
listingCallbackCaptor.value.onServicesUpdated(listOf(serviceInfo)) listingCallbackCaptor.value.onServicesUpdated(listOf(serviceInfo))
testableLooper.processAllMessages() testableLooper.processAllMessages()
@@ -256,6 +259,22 @@ class DeviceControlsTileTest : SysuiTestCase() {
assertThat(tile.state.state).isEqualTo(Tile.STATE_INACTIVE) 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 @Test
fun testStateInactiveIfLocked() { fun testStateInactiveIfLocked() {
verify(controlsListingController).observe( verify(controlsListingController).observe(
@@ -303,12 +322,12 @@ class DeviceControlsTileTest : SysuiTestCase() {
) )
`when`(controlsComponent.getVisibility()).thenReturn(ControlsComponent.Visibility.AVAILABLE) `when`(controlsComponent.getVisibility()).thenReturn(ControlsComponent.Visibility.AVAILABLE)
`when`(controlsUiController.resolveActivity()).thenReturn(ControlsActivity::class.java) `when`(controlsUiController.resolveActivity()).thenReturn(ControlsActivity::class.java)
`when`(controlsController.getPreferredStructure()).thenReturn( `when`(controlsController.getPreferredSelection()).thenReturn(
StructureInfo( SelectedItem.StructureItem(StructureInfo(
ComponentName("pkg", "cls"), ComponentName("pkg", "cls"),
"structure", "structure",
listOf(ControlInfo("id", "title", "subtitle", 1)) listOf(ControlInfo("id", "title", "subtitle", 1))
) ))
) )
listingCallbackCaptor.value.onServicesUpdated(listOf(serviceInfo)) listingCallbackCaptor.value.onServicesUpdated(listOf(serviceInfo))
@@ -334,12 +353,12 @@ class DeviceControlsTileTest : SysuiTestCase() {
`when`(controlsComponent.getVisibility()) `when`(controlsComponent.getVisibility())
.thenReturn(ControlsComponent.Visibility.AVAILABLE_AFTER_UNLOCK) .thenReturn(ControlsComponent.Visibility.AVAILABLE_AFTER_UNLOCK)
`when`(controlsUiController.resolveActivity()).thenReturn(ControlsActivity::class.java) `when`(controlsUiController.resolveActivity()).thenReturn(ControlsActivity::class.java)
`when`(controlsController.getPreferredStructure()).thenReturn( `when`(controlsController.getPreferredSelection()).thenReturn(
StructureInfo( SelectedItem.StructureItem(StructureInfo(
ComponentName("pkg", "cls"), ComponentName("pkg", "cls"),
"structure", "structure",
listOf(ControlInfo("id", "title", "subtitle", 1)) listOf(ControlInfo("id", "title", "subtitle", 1))
) ))
) )
listingCallbackCaptor.value.onServicesUpdated(listOf(serviceInfo)) listingCallbackCaptor.value.onServicesUpdated(listOf(serviceInfo))