• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.controls.ui
18 
19 import android.animation.Animator
20 import android.animation.AnimatorListenerAdapter
21 import android.animation.ObjectAnimator
22 import android.app.Activity
23 import android.app.ActivityOptions
24 import android.app.Dialog
25 import android.app.PendingIntent
26 import android.content.ComponentName
27 import android.content.Context
28 import android.content.Intent
29 import android.content.pm.PackageManager
30 import android.graphics.drawable.Drawable
31 import android.graphics.drawable.LayerDrawable
32 import android.os.Trace
33 import android.service.controls.Control
34 import android.service.controls.ControlsProviderService
35 import android.util.Log
36 import android.view.ContextThemeWrapper
37 import android.view.LayoutInflater
38 import android.view.View
39 import android.view.ViewGroup
40 import android.view.animation.AccelerateInterpolator
41 import android.view.animation.DecelerateInterpolator
42 import android.widget.AdapterView
43 import android.widget.ArrayAdapter
44 import android.widget.BaseAdapter
45 import android.widget.FrameLayout
46 import android.widget.ImageView
47 import android.widget.LinearLayout
48 import android.widget.ListPopupWindow
49 import android.widget.Space
50 import android.widget.TextView
51 import androidx.annotation.VisibleForTesting
52 import com.android.systemui.Dumpable
53 import com.android.systemui.R
54 import com.android.systemui.controls.ControlsMetricsLogger
55 import com.android.systemui.controls.ControlsServiceInfo
56 import com.android.systemui.controls.CustomIconCache
57 import com.android.systemui.controls.controller.ControlsController
58 import com.android.systemui.controls.controller.StructureInfo
59 import com.android.systemui.controls.controller.StructureInfo.Companion.EMPTY_COMPONENT
60 import com.android.systemui.controls.controller.StructureInfo.Companion.EMPTY_STRUCTURE
61 import com.android.systemui.controls.management.ControlAdapter
62 import com.android.systemui.controls.management.ControlsEditingActivity
63 import com.android.systemui.controls.management.ControlsFavoritingActivity
64 import com.android.systemui.controls.management.ControlsListingController
65 import com.android.systemui.controls.management.ControlsProviderSelectorActivity
66 import com.android.systemui.controls.panels.AuthorizedPanelsRepository
67 import com.android.systemui.controls.panels.SelectedComponentRepository
68 import com.android.systemui.controls.settings.ControlsSettingsRepository
69 import com.android.systemui.dagger.SysUISingleton
70 import com.android.systemui.dagger.qualifiers.Background
71 import com.android.systemui.dagger.qualifiers.Main
72 import com.android.systemui.dump.DumpManager
73 import com.android.systemui.flags.FeatureFlags
74 import com.android.systemui.flags.Flags
75 import com.android.systemui.globalactions.GlobalActionsPopupMenu
76 import com.android.systemui.plugins.ActivityStarter
77 import com.android.systemui.settings.UserTracker
78 import com.android.systemui.statusbar.policy.KeyguardStateController
79 import com.android.systemui.util.asIndenting
80 import com.android.systemui.util.concurrency.DelayableExecutor
81 import com.android.systemui.util.indentIfPossible
82 import com.android.wm.shell.TaskViewFactory
83 import dagger.Lazy
84 import java.io.PrintWriter
85 import java.text.Collator
86 import java.util.Optional
87 import java.util.function.Consumer
88 import javax.inject.Inject
89 
90 private data class ControlKey(val componentName: ComponentName, val controlId: String)
91 
92 @SysUISingleton
93 class ControlsUiControllerImpl @Inject constructor (
94         val controlsController: Lazy<ControlsController>,
95         val context: Context,
96         private val packageManager: PackageManager,
97         @Main val uiExecutor: DelayableExecutor,
98         @Background val bgExecutor: DelayableExecutor,
99         val controlsListingController: Lazy<ControlsListingController>,
100         private val controlActionCoordinator: ControlActionCoordinator,
101         private val activityStarter: ActivityStarter,
102         private val iconCache: CustomIconCache,
103         private val controlsMetricsLogger: ControlsMetricsLogger,
104         private val keyguardStateController: KeyguardStateController,
105         private val userTracker: UserTracker,
106         private val taskViewFactory: Optional<TaskViewFactory>,
107         private val controlsSettingsRepository: ControlsSettingsRepository,
108         private val authorizedPanelsRepository: AuthorizedPanelsRepository,
109         private val selectedComponentRepository: SelectedComponentRepository,
110         private val featureFlags: FeatureFlags,
111         private val dialogsFactory: ControlsDialogsFactory,
112         dumpManager: DumpManager
113 ) : ControlsUiController, Dumpable {
114 
115     companion object {
116 
117         private const val FADE_IN_MILLIS = 200L
118 
119         private const val OPEN_APP_ID = 0L
120         private const val ADD_CONTROLS_ID = 1L
121         private const val ADD_APP_ID = 2L
122         private const val EDIT_CONTROLS_ID = 3L
123         private const val REMOVE_APP_ID = 4L
124     }
125 
126     private var selectedItem: SelectedItem = SelectedItem.EMPTY_SELECTION
127     private var selectionItem: SelectionItem? = null
128     private lateinit var allStructures: List<StructureInfo>
129     private val controlsById = mutableMapOf<ControlKey, ControlWithState>()
130     private val controlViewsById = mutableMapOf<ControlKey, ControlViewHolder>()
131     private lateinit var parent: ViewGroup
132     private var popup: ListPopupWindow? = null
133     private var hidden = true
134     private lateinit var onDismiss: Runnable
135     private val popupThemedContext = ContextThemeWrapper(context, R.style.Control_ListPopupWindow)
136     private var retainCache = false
137     private var lastSelections = emptyList<SelectionItem>()
138 
139     private var taskViewController: PanelTaskViewController? = null
140 
141     private val collator = Collator.getInstance(context.resources.configuration.locales[0])
142     private val localeComparator = compareBy<SelectionItem, CharSequence>(collator) {
143         it.getTitle()
144     }
145 
146     private var openAppIntent: Intent? = null
147     private var overflowMenuAdapter: BaseAdapter? = null
148     private var removeAppDialog: Dialog? = null
149 
150     private val onSeedingComplete = Consumer<Boolean> {
151         accepted ->
152             if (accepted) {
153                 selectedItem = controlsController.get().getFavorites().maxByOrNull {
154                     it.controls.size
155                 }?.let {
156                     SelectedItem.StructureItem(it)
157                 } ?: SelectedItem.EMPTY_SELECTION
158                 updatePreferences(selectedItem)
159             }
160             reload(parent)
161     }
162 
163     private lateinit var activityContext: Context
164     private lateinit var listingCallback: ControlsListingController.ControlsListingCallback
165 
166     override val isShowing: Boolean
167         get() = !hidden
168 
169     init {
170         dumpManager.registerDumpable(javaClass.name, this)
171     }
172 
173     private fun createCallback(
174         onResult: (List<SelectionItem>) -> Unit
175     ): ControlsListingController.ControlsListingCallback {
176         return object : ControlsListingController.ControlsListingCallback {
177             override fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>) {
178                 val authorizedPanels = authorizedPanelsRepository.getAuthorizedPanels()
179                 val lastItems = serviceInfos.map {
180                     val uid = it.serviceInfo.applicationInfo.uid
181 
182                     SelectionItem(
183                             it.loadLabel(),
184                             "",
185                             it.loadIcon(),
186                             it.componentName,
187                             uid,
188                             if (it.componentName.packageName in authorizedPanels) {
189                                 it.panelActivity
190                             } else {
191                                 null
192                             }
193                     )
194                 }
195                 uiExecutor.execute {
196                     parent.removeAllViews()
197                     if (lastItems.size > 0) {
198                         onResult(lastItems)
199                     }
200                 }
201             }
202         }
203     }
204 
205     override fun resolveActivity(): Class<*> {
206         val allStructures = controlsController.get().getFavorites()
207         val selected = getPreferredSelectedItem(allStructures)
208         val anyPanels = controlsListingController.get().getCurrentServices()
209                 .any { it.panelActivity != null }
210 
211         return if (controlsController.get().addSeedingFavoritesCallback(onSeedingComplete)) {
212             ControlsActivity::class.java
213         } else if (!selected.hasControls && allStructures.size <= 1 && !anyPanels) {
214             ControlsProviderSelectorActivity::class.java
215         } else {
216             ControlsActivity::class.java
217         }
218     }
219 
220     override fun show(
221         parent: ViewGroup,
222         onDismiss: Runnable,
223         activityContext: Context
224     ) {
225         Log.d(ControlsUiController.TAG, "show()")
226         Trace.instant(Trace.TRACE_TAG_APP, "ControlsUiControllerImpl#show")
227         this.parent = parent
228         this.onDismiss = onDismiss
229         this.activityContext = activityContext
230         this.openAppIntent = null
231         this.overflowMenuAdapter = null
232         hidden = false
233         retainCache = false
234         selectionItem = null
235 
236         controlActionCoordinator.activityContext = activityContext
237 
238         allStructures = controlsController.get().getFavorites()
239         selectedItem = getPreferredSelectedItem(allStructures)
240 
241         if (controlsController.get().addSeedingFavoritesCallback(onSeedingComplete)) {
242             listingCallback = createCallback(::showSeedingView)
243         } else if (
244                 selectedItem !is SelectedItem.PanelItem &&
245                 !selectedItem.hasControls &&
246                 allStructures.size <= 1
247         ) {
248             // only show initial view if there are really no favorites across any structure
249             listingCallback = createCallback(::initialView)
250         } else {
251             val selected = selectedItem
252             if (selected is SelectedItem.StructureItem) {
253                 selected.structure.controls.map {
254                     ControlWithState(selected.structure.componentName, it, null)
255                 }.associateByTo(controlsById) {
256                     ControlKey(selected.structure.componentName, it.ci.controlId)
257                 }
258                 controlsController.get().subscribeToFavorites(selected.structure)
259             } else {
260                 controlsController.get().bindComponentForPanel(selected.componentName)
261             }
262             listingCallback = createCallback(::showControlsView)
263         }
264 
265         controlsListingController.get().addCallback(listingCallback)
266     }
267 
268     private fun initialView(items: List<SelectionItem>) {
269         if (items.any { it.isPanel }) {
270             // We have at least a panel, so we'll end up showing that.
271             showControlsView(items)
272         } else {
273             showInitialSetupView(items)
274         }
275     }
276 
277     private fun reload(parent: ViewGroup, dismissTaskView: Boolean = true) {
278         if (hidden) return
279 
280         controlsListingController.get().removeCallback(listingCallback)
281         controlsController.get().unsubscribe()
282         taskViewController?.dismiss()
283         taskViewController = null
284 
285         val fadeAnim = ObjectAnimator.ofFloat(parent, "alpha", 1.0f, 0.0f)
286         fadeAnim.setInterpolator(AccelerateInterpolator(1.0f))
287         fadeAnim.setDuration(FADE_IN_MILLIS)
288         fadeAnim.addListener(object : AnimatorListenerAdapter() {
289             override fun onAnimationEnd(animation: Animator) {
290                 controlViewsById.clear()
291                 controlsById.clear()
292 
293                 show(parent, onDismiss, activityContext)
294                 val showAnim = ObjectAnimator.ofFloat(parent, "alpha", 0.0f, 1.0f)
295                 showAnim.setInterpolator(DecelerateInterpolator(1.0f))
296                 showAnim.setDuration(FADE_IN_MILLIS)
297                 showAnim.start()
298             }
299         })
300         fadeAnim.start()
301     }
302 
303     private fun showSeedingView(items: List<SelectionItem>) {
304         val inflater = LayoutInflater.from(context)
305         inflater.inflate(R.layout.controls_no_favorites, parent, true)
306         val subtitle = parent.requireViewById<TextView>(R.id.controls_subtitle)
307         subtitle.setText(context.resources.getString(R.string.controls_seeding_in_progress))
308     }
309 
310     private fun showInitialSetupView(items: List<SelectionItem>) {
311         startProviderSelectorActivity()
312         onDismiss.run()
313     }
314 
315     private fun startFavoritingActivity(si: StructureInfo) {
316         startTargetedActivity(si, ControlsFavoritingActivity::class.java)
317     }
318 
319     private fun startEditingActivity(si: StructureInfo) {
320         startTargetedActivity(si, ControlsEditingActivity::class.java)
321     }
322 
323     private fun startDefaultActivity() {
324         openAppIntent?.let {
325             startActivity(it, animateExtra = false)
326         }
327     }
328 
329     @VisibleForTesting
330     internal fun startRemovingApp(componentName: ComponentName, appName: CharSequence) {
331         removeAppDialog?.cancel()
332         removeAppDialog = dialogsFactory.createRemoveAppDialog(context, appName) {
333             if (!controlsController.get().removeFavorites(componentName)) {
334                 return@createRemoveAppDialog
335             }
336 
337             if (selectedComponentRepository.getSelectedComponent()?.componentName ==
338                     componentName) {
339                 selectedComponentRepository.removeSelectedComponent()
340             }
341 
342             val selectedItem = getPreferredSelectedItem(controlsController.get().getFavorites())
343             if (selectedItem == SelectedItem.EMPTY_SELECTION) {
344                 // User removed the last panel. In this case we start app selection flow and don't
345                 // want to auto-add it again
346                 selectedComponentRepository.setShouldAddDefaultComponent(false)
347             }
348             reload(parent)
349         }.apply { show() }
350     }
351 
352     private fun startTargetedActivity(si: StructureInfo, klazz: Class<*>) {
353         val i = Intent(activityContext, klazz)
354         putIntentExtras(i, si)
355         startActivity(i)
356 
357         retainCache = true
358     }
359 
360     private fun putIntentExtras(intent: Intent, si: StructureInfo) {
361         intent.apply {
362             putExtra(ControlsFavoritingActivity.EXTRA_APP,
363                     controlsListingController.get().getAppLabel(si.componentName))
364             putExtra(ControlsFavoritingActivity.EXTRA_STRUCTURE, si.structure)
365             putExtra(Intent.EXTRA_COMPONENT_NAME, si.componentName)
366         }
367     }
368 
369     private fun startProviderSelectorActivity() {
370         val i = Intent(activityContext, ControlsProviderSelectorActivity::class.java)
371         i.putExtra(ControlsProviderSelectorActivity.BACK_SHOULD_EXIT, true)
372         startActivity(i)
373     }
374 
375     private fun startActivity(intent: Intent, animateExtra: Boolean = true) {
376         // Force animations when transitioning from a dialog to an activity
377         if (animateExtra) {
378             intent.putExtra(ControlsUiController.EXTRA_ANIMATE, true)
379         }
380 
381         if (keyguardStateController.isShowing()) {
382             activityStarter.postStartActivityDismissingKeyguard(intent, 0 /* delay */)
383         } else {
384             activityContext.startActivity(
385                 intent,
386                 ActivityOptions.makeSceneTransitionAnimation(activityContext as Activity).toBundle()
387             )
388         }
389     }
390 
391     private fun showControlsView(items: List<SelectionItem>) {
392         controlViewsById.clear()
393 
394         val (panels, structures) = items.partition { it.isPanel }
395         val panelComponents = panels.map { it.componentName }.toSet()
396 
397         val itemsByComponent = structures.associateBy { it.componentName }
398                 .filterNot { it.key in panelComponents }
399         val panelsAndStructures = mutableListOf<SelectionItem>()
400         allStructures.mapNotNullTo(panelsAndStructures) {
401             itemsByComponent.get(it.componentName)?.copy(structure = it.structure)
402         }
403         panelsAndStructures.addAll(panels)
404 
405         panelsAndStructures.sortWith(localeComparator)
406 
407         lastSelections = panelsAndStructures
408 
409         val selectionItem = findSelectionItem(selectedItem, panelsAndStructures)
410                 ?: if (panels.isNotEmpty()) {
411                     // If we couldn't find a good selected item, but there's at least one panel,
412                     // show a panel.
413                     panels[0]
414                 } else {
415                     items[0]
416                 }
417         maybeUpdateSelectedItem(selectionItem)
418 
419         createControlsSpaceFrame()
420 
421         if (taskViewFactory.isPresent && selectionItem.isPanel) {
422             createPanelView(selectionItem.panelComponentName!!)
423         } else if (!selectionItem.isPanel) {
424             controlsMetricsLogger
425                     .refreshBegin(selectionItem.uid, !keyguardStateController.isUnlocked())
426             createListView(selectionItem)
427         } else {
428             Log.w(ControlsUiController.TAG, "Not TaskViewFactory to display panel $selectionItem")
429         }
430         this.selectionItem = selectionItem
431 
432         bgExecutor.execute {
433             val intent = Intent(Intent.ACTION_MAIN)
434                     .addCategory(Intent.CATEGORY_LAUNCHER)
435                     .setPackage(selectionItem.componentName.packageName)
436                     .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or
437                             Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED)
438             val intents = packageManager
439                     .queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(0L))
440             intents.firstOrNull { it.activityInfo.exported }?.let { resolved ->
441                 intent.setPackage(null)
442                 intent.setComponent(resolved.activityInfo.componentName)
443                 openAppIntent = intent
444                 parent.post {
445                     // This will call show on the PopupWindow in the same thread, so make sure this
446                     // happens in the view thread.
447                     overflowMenuAdapter?.notifyDataSetChanged()
448                 }
449             }
450         }
451         createDropDown(panelsAndStructures, selectionItem)
452 
453         val currentApps = panelsAndStructures.map { it.componentName }.toSet()
454         val allApps = controlsListingController.get()
455                 .getCurrentServices().map { it.componentName }.toSet()
456         createMenu(
457                 selectionItem = selectionItem,
458                 extraApps = (allApps - currentApps).isNotEmpty(),
459         )
460     }
461 
462     private fun createPanelView(componentName: ComponentName) {
463         val setting = controlsSettingsRepository
464                 .allowActionOnTrivialControlsInLockscreen.value
465         val pendingIntent = PendingIntent.getActivityAsUser(
466                 context,
467                 0,
468                 Intent()
469                         .setComponent(componentName)
470                         .putExtra(
471                                 ControlsProviderService.EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS,
472                                 setting
473                         ),
474                 PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
475                 null,
476                 userTracker.userHandle
477         )
478 
479         parent.requireViewById<View>(R.id.controls_scroll_view).visibility = View.GONE
480         val container = parent.requireViewById<FrameLayout>(R.id.controls_panel)
481         container.visibility = View.VISIBLE
482         container.post {
483             taskViewFactory.get().create(activityContext, uiExecutor) { taskView ->
484                 taskViewController = PanelTaskViewController(
485                         activityContext,
486                         uiExecutor,
487                         pendingIntent,
488                         taskView,
489                         onDismiss::run
490                 ).also {
491                     container.addView(taskView)
492                     it.launchTaskView()
493                 }
494             }
495         }
496     }
497 
498     private fun createMenu(selectionItem: SelectionItem, extraApps: Boolean) {
499         val isPanel = selectedItem is SelectedItem.PanelItem
500         val selectedStructure = (selectedItem as? SelectedItem.StructureItem)?.structure
501                 ?: EMPTY_STRUCTURE
502         val newFlows = featureFlags.isEnabled(Flags.CONTROLS_MANAGEMENT_NEW_FLOWS)
503 
504         val items = buildList {
505             add(OverflowMenuAdapter.MenuItem(
506                     context.getText(R.string.controls_open_app),
507                     OPEN_APP_ID
508             ))
509             if (newFlows || isPanel) {
510                 if (extraApps) {
511                     add(OverflowMenuAdapter.MenuItem(
512                             context.getText(R.string.controls_menu_add_another_app),
513                             ADD_APP_ID
514                     ))
515                 }
516                 if (featureFlags.isEnabled(Flags.APP_PANELS_REMOVE_APPS_ALLOWED)) {
517                     add(OverflowMenuAdapter.MenuItem(
518                             context.getText(R.string.controls_menu_remove),
519                             REMOVE_APP_ID,
520                     ))
521                 }
522             } else {
523                 add(OverflowMenuAdapter.MenuItem(
524                         context.getText(R.string.controls_menu_add),
525                         ADD_CONTROLS_ID
526                 ))
527             }
528             if (!isPanel) {
529                 add(OverflowMenuAdapter.MenuItem(
530                         context.getText(R.string.controls_menu_edit),
531                         EDIT_CONTROLS_ID
532                 ))
533             }
534         }
535 
536         val adapter = OverflowMenuAdapter(context, R.layout.controls_more_item, items) { position ->
537                 getItemId(position) != OPEN_APP_ID || openAppIntent != null
538         }
539 
540         val anchor = parent.requireViewById<ImageView>(R.id.controls_more)
541         anchor.setOnClickListener(object : View.OnClickListener {
542             override fun onClick(v: View) {
543                 popup = GlobalActionsPopupMenu(
544                         popupThemedContext,
545                         false /* isDropDownMode */
546                 ).apply {
547                     setAnchorView(anchor)
548                     setAdapter(adapter)
549                     setOnItemClickListener(object : AdapterView.OnItemClickListener {
550                         override fun onItemClick(
551                             parent: AdapterView<*>,
552                             view: View,
553                             pos: Int,
554                             id: Long
555                         ) {
556                             when (id) {
557                                 OPEN_APP_ID -> startDefaultActivity()
558                                 ADD_APP_ID -> startProviderSelectorActivity()
559                                 ADD_CONTROLS_ID -> startFavoritingActivity(selectedStructure)
560                                 EDIT_CONTROLS_ID -> startEditingActivity(selectedStructure)
561                                 REMOVE_APP_ID -> startRemovingApp(
562                                         selectionItem.componentName, selectionItem.appName
563                                 )
564                             }
565                             dismiss()
566                         }
567                     })
568                     show()
569                     listView?.post { listView?.requestAccessibilityFocus() }
570                 }
571             }
572         })
573         overflowMenuAdapter = adapter
574     }
575 
576     private fun createDropDown(items: List<SelectionItem>, selected: SelectionItem) {
577         items.forEach {
578             RenderInfo.registerComponentIcon(it.componentName, it.icon)
579         }
580 
581         val adapter = ItemAdapter(context, R.layout.controls_spinner_item).apply {
582             add(selected)
583             addAll(items
584                     .filter { it !== selected }
585                     .sortedBy { it.appName.toString() }
586             )
587         }
588 
589         val iconSize = context.resources
590                 .getDimensionPixelSize(R.dimen.controls_header_app_icon_size)
591 
592         /*
593          * Default spinner widget does not work with the window type required
594          * for this dialog. Use a textView with the ListPopupWindow to achieve
595          * a similar effect
596          */
597         val spinner = parent.requireViewById<TextView>(R.id.app_or_structure_spinner).apply {
598             setText(selected.getTitle())
599             // override the default color on the dropdown drawable
600             (getBackground() as LayerDrawable).getDrawable(0)
601                 .setTint(context.resources.getColor(R.color.control_spinner_dropdown, null))
602             selected.icon.setBounds(0, 0, iconSize, iconSize)
603             compoundDrawablePadding = (iconSize / 2.4f).toInt()
604             setCompoundDrawablesRelative(selected.icon, null, null, null)
605         }
606 
607         val anchor = parent.requireViewById<ViewGroup>(R.id.controls_header)
608         if (items.size == 1) {
609             spinner.setBackground(null)
610             anchor.setOnClickListener(null)
611             anchor.isClickable = false
612             return
613         } else {
614             spinner.background = parent.context.resources
615                     .getDrawable(R.drawable.control_spinner_background)
616         }
617 
618         anchor.setOnClickListener(object : View.OnClickListener {
619             override fun onClick(v: View) {
620                 popup = GlobalActionsPopupMenu(
621                         popupThemedContext,
622                         true /* isDropDownMode */
623                 ).apply {
624                     setAnchorView(anchor)
625                     setAdapter(adapter)
626 
627                     setOnItemClickListener(object : AdapterView.OnItemClickListener {
628                         override fun onItemClick(
629                             parent: AdapterView<*>,
630                             view: View,
631                             pos: Int,
632                             id: Long
633                         ) {
634                             val listItem = parent.getItemAtPosition(pos) as SelectionItem
635                             this@ControlsUiControllerImpl.switchAppOrStructure(listItem)
636                             dismiss()
637                         }
638                     })
639                     show()
640                     listView?.post { listView?.requestAccessibilityFocus() }
641                 }
642             }
643         })
644     }
645 
646     private fun createControlsSpaceFrame() {
647         val inflater = LayoutInflater.from(activityContext)
648         inflater.inflate(R.layout.controls_with_favorites, parent, true)
649 
650         parent.requireViewById<ImageView>(R.id.controls_close).apply {
651             setOnClickListener { _: View -> onDismiss.run() }
652             visibility = View.VISIBLE
653         }
654     }
655 
656     private fun createListView(selected: SelectionItem) {
657         if (selectedItem !is SelectedItem.StructureItem) return
658         val selectedStructure = (selectedItem as SelectedItem.StructureItem).structure
659         val inflater = LayoutInflater.from(activityContext)
660 
661         val maxColumns = ControlAdapter.findMaxColumns(activityContext.resources)
662 
663         val listView = parent.requireViewById(R.id.global_actions_controls_list) as ViewGroup
664         listView.removeAllViews()
665         var lastRow: ViewGroup = createRow(inflater, listView)
666         selectedStructure.controls.forEach {
667             val key = ControlKey(selectedStructure.componentName, it.controlId)
668             controlsById.get(key)?.let {
669                 if (lastRow.getChildCount() == maxColumns) {
670                     lastRow = createRow(inflater, listView)
671                 }
672                 val baseLayout = inflater.inflate(
673                     R.layout.controls_base_item, lastRow, false) as ViewGroup
674                 lastRow.addView(baseLayout)
675 
676                 // Use ConstraintLayout in the future... for now, manually adjust margins
677                 if (lastRow.getChildCount() == 1) {
678                     val lp = baseLayout.getLayoutParams() as ViewGroup.MarginLayoutParams
679                     lp.setMarginStart(0)
680                 }
681                 val cvh = ControlViewHolder(
682                     baseLayout,
683                     controlsController.get(),
684                     uiExecutor,
685                     bgExecutor,
686                     controlActionCoordinator,
687                     controlsMetricsLogger,
688                     selected.uid
689                 )
690                 cvh.bindData(it, false /* isLocked, will be ignored on initial load */)
691                 controlViewsById.put(key, cvh)
692             }
693         }
694 
695         // add spacers if necessary to keep control size consistent
696         val mod = selectedStructure.controls.size % maxColumns
697         var spacersToAdd = if (mod == 0) 0 else maxColumns - mod
698         val margin = context.resources.getDimensionPixelSize(R.dimen.control_spacing)
699         while (spacersToAdd > 0) {
700             val lp = LinearLayout.LayoutParams(0, 0, 1f).apply {
701                 setMarginStart(margin)
702             }
703             lastRow.addView(Space(context), lp)
704             spacersToAdd--
705         }
706     }
707 
708     override fun getPreferredSelectedItem(structures: List<StructureInfo>): SelectedItem {
709         val preferredPanel = selectedComponentRepository.getSelectedComponent()
710         val component = preferredPanel?.componentName ?: EMPTY_COMPONENT
711         return if (preferredPanel?.isPanel == true) {
712             SelectedItem.PanelItem(preferredPanel.name, component)
713         } else {
714             if (structures.isEmpty()) return SelectedItem.EMPTY_SELECTION
715             SelectedItem.StructureItem(structures.firstOrNull {
716                 component == it.componentName && preferredPanel?.name == it.structure
717             } ?: structures[0])
718         }
719     }
720 
721     private fun updatePreferences(selectedItem: SelectedItem) {
722         selectedComponentRepository.setSelectedComponent(
723                 SelectedComponentRepository.SelectedComponent(selectedItem)
724         )
725     }
726 
727     private fun maybeUpdateSelectedItem(item: SelectionItem): Boolean {
728         val newSelection = if (item.isPanel) {
729             SelectedItem.PanelItem(item.appName, item.componentName)
730         } else {
731             SelectedItem.StructureItem(allStructures.firstOrNull {
732                 it.structure == item.structure && it.componentName == item.componentName
733             } ?: EMPTY_STRUCTURE)
734         }
735         return if (newSelection != selectedItem ) {
736             selectedItem = newSelection
737             updatePreferences(selectedItem)
738             true
739         } else {
740             false
741         }
742     }
743 
744     private fun switchAppOrStructure(item: SelectionItem) {
745         if (maybeUpdateSelectedItem(item)) {
746             reload(parent)
747         }
748     }
749 
750     override fun closeDialogs(immediately: Boolean) {
751         if (immediately) {
752             popup?.dismissImmediate()
753         } else {
754             popup?.dismiss()
755         }
756         popup = null
757 
758         controlViewsById.forEach {
759             it.value.dismiss()
760         }
761         controlActionCoordinator.closeDialogs()
762         removeAppDialog?.cancel()
763     }
764 
765     override fun hide(parent: ViewGroup) {
766         // We need to check for the parent because it's possible that  we have started showing in a
767         // different activity. In that case, make sure to only clear things associated with the
768         // passed parent
769         if (parent == this.parent) {
770             Log.d(ControlsUiController.TAG, "hide()")
771             hidden = true
772 
773             closeDialogs(true)
774             controlsController.get().unsubscribe()
775             taskViewController?.dismiss()
776             taskViewController = null
777 
778             controlsById.clear()
779             controlViewsById.clear()
780 
781             controlsListingController.get().removeCallback(listingCallback)
782 
783             if (!retainCache) RenderInfo.clearCache()
784         }
785         parent.removeAllViews()
786     }
787 
788     override fun onRefreshState(componentName: ComponentName, controls: List<Control>) {
789         val isLocked = !keyguardStateController.isUnlocked()
790         controls.forEach { c ->
791             controlsById.get(ControlKey(componentName, c.getControlId()))?.let {
792                 Log.d(ControlsUiController.TAG, "onRefreshState() for id: " + c.getControlId())
793                 iconCache.store(componentName, c.controlId, c.customIcon)
794                 val cws = ControlWithState(componentName, it.ci, c)
795                 val key = ControlKey(componentName, c.getControlId())
796                 controlsById.put(key, cws)
797 
798                 controlViewsById.get(key)?.let {
799                     uiExecutor.execute { it.bindData(cws, isLocked) }
800                 }
801             }
802         }
803     }
804 
805     override fun onActionResponse(componentName: ComponentName, controlId: String, response: Int) {
806         val key = ControlKey(componentName, controlId)
807         uiExecutor.execute {
808             controlViewsById.get(key)?.actionResponse(response)
809         }
810     }
811 
812     override fun onSizeChange() {
813         selectionItem?.let {
814             when (selectedItem) {
815                 is SelectedItem.StructureItem -> createListView(it)
816                 is SelectedItem.PanelItem -> taskViewController?.refreshBounds() ?: reload(parent)
817             }
818         } ?: reload(parent)
819     }
820 
821     private fun createRow(inflater: LayoutInflater, listView: ViewGroup): ViewGroup {
822         val row = inflater.inflate(R.layout.controls_row, listView, false) as ViewGroup
823         listView.addView(row)
824         return row
825     }
826 
827     private fun findSelectionItem(si: SelectedItem, items: List<SelectionItem>): SelectionItem? =
828         items.firstOrNull { it.matches(si) }
829 
830     override fun dump(pw: PrintWriter, args: Array<out String>) {
831         pw.println("ControlsUiControllerImpl:")
832         pw.asIndenting().indentIfPossible {
833             println("hidden: $hidden")
834             println("selectedItem: $selectedItem")
835             println("lastSelections: $lastSelections")
836             println("setting: ${controlsSettingsRepository
837                     .allowActionOnTrivialControlsInLockscreen.value}")
838         }
839     }
840 }
841 
842 @VisibleForTesting
843 internal data class SelectionItem(
844     val appName: CharSequence,
845     val structure: CharSequence,
846     val icon: Drawable,
847     val componentName: ComponentName,
848     val uid: Int,
849     val panelComponentName: ComponentName?
850 ) {
getTitlenull851     fun getTitle() = if (structure.isEmpty()) { appName } else { structure }
852 
853     val isPanel: Boolean = panelComponentName != null
854 
matchesnull855     fun matches(selectedItem: SelectedItem): Boolean {
856         if (componentName != selectedItem.componentName) {
857             // Not the same component so they are not the same.
858             return false
859         }
860         if (isPanel || selectedItem is SelectedItem.PanelItem) {
861             // As they have the same component, if [this.isPanel] then we may be migrating from
862             // device controls API into panel. Want this to match, even if the selectedItem is not
863             // a panel. We don't want to match on app name because that can change with locale.
864             return true
865         }
866         // Return true if we find a structure with the correct name
867         return structure == (selectedItem as SelectedItem.StructureItem).structure.structure
868     }
869 }
870 
871 private class ItemAdapter(
872     val parentContext: Context,
873     val resource: Int
874 ) : ArrayAdapter<SelectionItem>(parentContext, resource) {
875 
876     val layoutInflater = LayoutInflater.from(context)
877 
getViewnull878     override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
879         val item = getItem(position)
880         val view = convertView ?: layoutInflater.inflate(resource, parent, false)
881         view.requireViewById<TextView>(R.id.controls_spinner_item).apply {
882             setText(item.getTitle())
883         }
884         view.requireViewById<ImageView>(R.id.app_icon).apply {
885             setImageDrawable(item.icon)
886         }
887         return view
888     }
889 }
890