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.service.controls.flags.Flags.homePanelDream
36 import android.util.Log
37 import android.view.ContextThemeWrapper
38 import android.view.Gravity
39 import android.view.LayoutInflater
40 import android.view.View
41 import android.view.ViewGroup
42 import android.view.animation.AccelerateInterpolator
43 import android.view.animation.DecelerateInterpolator
44 import android.widget.AdapterView
45 import android.widget.ArrayAdapter
46 import android.widget.BaseAdapter
47 import android.widget.FrameLayout
48 import android.widget.ImageView
49 import android.widget.LinearLayout
50 import android.widget.ListPopupWindow
51 import android.widget.Space
52 import android.widget.TextView
53 import androidx.annotation.VisibleForTesting
54 import com.android.systemui.Dumpable
55 import com.android.systemui.res.R
56 import com.android.systemui.controls.ControlsMetricsLogger
57 import com.android.systemui.controls.ControlsServiceInfo
58 import com.android.systemui.controls.CustomIconCache
59 import com.android.systemui.controls.controller.ControlsController
60 import com.android.systemui.controls.controller.StructureInfo
61 import com.android.systemui.controls.controller.StructureInfo.Companion.EMPTY_COMPONENT
62 import com.android.systemui.controls.controller.StructureInfo.Companion.EMPTY_STRUCTURE
63 import com.android.systemui.controls.management.ControlAdapter
64 import com.android.systemui.controls.management.ControlsEditingActivity
65 import com.android.systemui.controls.management.ControlsFavoritingActivity
66 import com.android.systemui.controls.management.ControlsListingController
67 import com.android.systemui.controls.management.ControlsProviderSelectorActivity
68 import com.android.systemui.controls.panels.AuthorizedPanelsRepository
69 import com.android.systemui.controls.panels.SelectedComponentRepository
70 import com.android.systemui.controls.settings.ControlsSettingsRepository
71 import com.android.systemui.dagger.SysUISingleton
72 import com.android.systemui.dagger.qualifiers.Background
73 import com.android.systemui.dagger.qualifiers.Main
74 import com.android.systemui.dump.DumpManager
75 import com.android.systemui.flags.FeatureFlags
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.withIncreasedIndent
82 import com.android.wm.shell.taskview.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(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?.removeTask()
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         activityStarter.dismissKeyguardThenExecute({
332             showAppRemovalDialog(componentName, appName)
333             true
334         }, null, true)
335     }
336 
337     private fun showAppRemovalDialog(componentName: ComponentName, appName: CharSequence) {
338         removeAppDialog?.cancel()
339         removeAppDialog = dialogsFactory.createRemoveAppDialog(context, appName) { shouldRemove ->
340             if (!shouldRemove || !controlsController.get().removeFavorites(componentName)) {
341                 return@createRemoveAppDialog
342             }
343 
344             if (selectedComponentRepository.getSelectedComponent()?.componentName ==
345                     componentName) {
346                 selectedComponentRepository.removeSelectedComponent()
347             }
348 
349             val selectedItem = getPreferredSelectedItem(controlsController.get().getFavorites())
350             if (selectedItem == SelectedItem.EMPTY_SELECTION) {
351                 // User removed the last panel. In this case we start app selection flow and don't
352                 // want to auto-add it again
353                 selectedComponentRepository.setShouldAddDefaultComponent(false)
354             }
355             reload(parent)
356         }.apply { show() }
357     }
358 
359     private fun startTargetedActivity(si: StructureInfo, klazz: Class<*>) {
360         val i = Intent(activityContext, klazz)
361         putIntentExtras(i, si)
362         startActivity(i)
363 
364         retainCache = true
365     }
366 
367     private fun putIntentExtras(intent: Intent, si: StructureInfo) {
368         intent.apply {
369             putExtra(ControlsFavoritingActivity.EXTRA_APP,
370                     controlsListingController.get().getAppLabel(si.componentName))
371             putExtra(ControlsFavoritingActivity.EXTRA_STRUCTURE, si.structure)
372             putExtra(Intent.EXTRA_COMPONENT_NAME, si.componentName)
373         }
374     }
375 
376     private fun startProviderSelectorActivity() {
377         val i = Intent(activityContext, ControlsProviderSelectorActivity::class.java)
378         i.putExtra(ControlsProviderSelectorActivity.BACK_SHOULD_EXIT, true)
379         startActivity(i)
380     }
381 
382     private fun startActivity(intent: Intent, animateExtra: Boolean = true) {
383         // Force animations when transitioning from a dialog to an activity
384         if (animateExtra) {
385             intent.putExtra(ControlsUiController.EXTRA_ANIMATE, true)
386         }
387 
388         if (keyguardStateController.isShowing()) {
389             activityStarter.postStartActivityDismissingKeyguard(intent, 0 /* delay */)
390         } else {
391             activityContext.startActivity(
392                 intent,
393                 ActivityOptions.makeSceneTransitionAnimation(activityContext as Activity).toBundle()
394             )
395         }
396     }
397 
398     private fun showControlsView(items: List<SelectionItem>) {
399         controlViewsById.clear()
400 
401         val (panels, structures) = items.partition { it.isPanel }
402         val panelComponents = panels.map { it.componentName }.toSet()
403 
404         val itemsByComponent = structures.associateBy { it.componentName }
405                 .filterNot { it.key in panelComponents }
406         val panelsAndStructures = mutableListOf<SelectionItem>()
407         allStructures.mapNotNullTo(panelsAndStructures) {
408             itemsByComponent.get(it.componentName)?.copy(structure = it.structure)
409         }
410         panelsAndStructures.addAll(panels)
411 
412         panelsAndStructures.sortWith(localeComparator)
413 
414         lastSelections = panelsAndStructures
415 
416         val selectionItem = findSelectionItem(selectedItem, panelsAndStructures)
417                 ?: if (panels.isNotEmpty()) {
418                     // If we couldn't find a good selected item, but there's at least one panel,
419                     // show a panel.
420                     panels[0]
421                 } else {
422                     items[0]
423                 }
424         maybeUpdateSelectedItem(selectionItem)
425 
426         createControlsSpaceFrame()
427 
428         if (taskViewFactory.isPresent && selectionItem.isPanel) {
429             createPanelView(selectionItem.panelComponentName!!)
430         } else if (!selectionItem.isPanel) {
431             controlsMetricsLogger
432                     .refreshBegin(selectionItem.uid, !keyguardStateController.isUnlocked())
433             createListView(selectionItem)
434         } else {
435             Log.w(ControlsUiController.TAG, "Not TaskViewFactory to display panel $selectionItem")
436         }
437         this.selectionItem = selectionItem
438 
439         bgExecutor.execute {
440             val intent = Intent(Intent.ACTION_MAIN)
441                     .addCategory(Intent.CATEGORY_LAUNCHER)
442                     .setPackage(selectionItem.componentName.packageName)
443                     .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or
444                             Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED)
445             val intents = packageManager
446                     .queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(0L))
447             intents.firstOrNull { it.activityInfo.exported }?.let { resolved ->
448                 intent.setPackage(null)
449                 intent.setComponent(resolved.activityInfo.componentName)
450                 openAppIntent = intent
451                 parent.post {
452                     // This will call show on the PopupWindow in the same thread, so make sure this
453                     // happens in the view thread.
454                     overflowMenuAdapter?.notifyDataSetChanged()
455                 }
456             }
457         }
458         createDropDown(panelsAndStructures, selectionItem)
459 
460         val currentApps = panelsAndStructures.map { it.componentName }.toSet()
461         val allApps = controlsListingController.get()
462                 .getCurrentServices().map { it.componentName }.toSet()
463         createMenu(
464                 selectionItem = selectionItem,
465                 extraApps = (allApps - currentApps).isNotEmpty(),
466         )
467     }
468 
469     private fun createPanelView(componentName: ComponentName) {
470         val setting = controlsSettingsRepository
471                 .allowActionOnTrivialControlsInLockscreen.value
472         val pendingIntent = PendingIntent.getActivityAsUser(
473                 context,
474                 0,
475                 Intent().apply {
476                     component = componentName
477                     putExtra(
478                         ControlsProviderService.EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS,
479                         setting
480                     )
481                     if (homePanelDream()) {
482                         putExtra(ControlsProviderService.EXTRA_CONTROLS_SURFACE,
483                             ControlsProviderService.CONTROLS_SURFACE_ACTIVITY_PANEL)
484                     }
485                 },
486                 PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
487                 ActivityOptions.makeBasic()
488                     .setPendingIntentCreatorBackgroundActivityStartMode(
489                         ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED).toBundle(),
490                 userTracker.userHandle
491         )
492 
493         parent.requireViewById<View>(R.id.controls_scroll_view).visibility = View.GONE
494         val container = parent.requireViewById<FrameLayout>(R.id.controls_panel)
495         container.visibility = View.VISIBLE
496         container.post {
497             taskViewFactory.get().create(activityContext, uiExecutor) { taskView ->
498                 taskViewController = PanelTaskViewController(
499                         activityContext,
500                         uiExecutor,
501                         pendingIntent,
502                         taskView,
503                         onDismiss::run
504                 ).also {
505                     container.addView(taskView)
506                     it.launchTaskView()
507                 }
508             }
509         }
510     }
511 
512     private fun createMenu(selectionItem: SelectionItem, extraApps: Boolean) {
513         val isPanel = selectedItem is SelectedItem.PanelItem
514         val selectedStructure = (selectedItem as? SelectedItem.StructureItem)?.structure
515                 ?: EMPTY_STRUCTURE
516 
517         val items = buildList {
518             add(OverflowMenuAdapter.MenuItem(
519                     context.getText(R.string.controls_open_app),
520                     OPEN_APP_ID
521             ))
522             if (extraApps) {
523                 add(OverflowMenuAdapter.MenuItem(
524                         context.getText(R.string.controls_menu_add_another_app),
525                         ADD_APP_ID
526                 ))
527             }
528             add(OverflowMenuAdapter.MenuItem(
529                     context.getText(R.string.controls_menu_remove),
530                     REMOVE_APP_ID,
531             ))
532             if (!isPanel) {
533                 add(OverflowMenuAdapter.MenuItem(
534                         context.getText(R.string.controls_menu_edit),
535                         EDIT_CONTROLS_ID
536                 ))
537             }
538         }
539 
540         val adapter = OverflowMenuAdapter(context, R.layout.controls_more_item, items) { position ->
541                 getItemId(position) != OPEN_APP_ID || openAppIntent != null
542         }
543 
544         val anchor = parent.requireViewById<ImageView>(R.id.controls_more)
545         anchor.setOnClickListener(object : View.OnClickListener {
546             override fun onClick(v: View) {
547                 popup = ControlsPopupMenu(popupThemedContext).apply {
548                     width = ViewGroup.LayoutParams.WRAP_CONTENT
549                     anchorView = anchor
550                     setDropDownGravity(Gravity.END)
551                     setAdapter(adapter)
552 
553                     setOnItemClickListener(object : AdapterView.OnItemClickListener {
554                         override fun onItemClick(
555                             parent: AdapterView<*>,
556                             view: View,
557                             pos: Int,
558                             id: Long
559                         ) {
560                             when (id) {
561                                 OPEN_APP_ID -> startDefaultActivity()
562                                 ADD_APP_ID -> startProviderSelectorActivity()
563                                 ADD_CONTROLS_ID -> startFavoritingActivity(selectedStructure)
564                                 EDIT_CONTROLS_ID -> startEditingActivity(selectedStructure)
565                                 REMOVE_APP_ID -> startRemovingApp(
566                                         selectionItem.componentName, selectionItem.appName
567                                 )
568                             }
569                             dismiss()
570                         }
571                     })
572                     show()
573                     listView?.post { listView?.requestAccessibilityFocus() }
574                 }
575             }
576         })
577         overflowMenuAdapter = adapter
578     }
579 
580     private fun createDropDown(items: List<SelectionItem>, selected: SelectionItem) {
581         items.forEach {
582             RenderInfo.registerComponentIcon(it.componentName, it.icon)
583         }
584 
585         val adapter = ItemAdapter(context, R.layout.controls_spinner_item).apply {
586             add(selected)
587             addAll(items
588                     .filter { it !== selected }
589                     .sortedBy { it.appName.toString() }
590             )
591         }
592 
593         val iconSize = context.resources
594                 .getDimensionPixelSize(R.dimen.controls_header_app_icon_size)
595 
596         /*
597          * Default spinner widget does not work with the window type required
598          * for this dialog. Use a textView with the ListPopupWindow to achieve
599          * a similar effect
600          */
601         val spinner = parent.requireViewById<TextView>(R.id.app_or_structure_spinner).apply {
602             setText(selected.getTitle())
603             // override the default color on the dropdown drawable
604             (getBackground() as LayerDrawable).getDrawable(0)
605                 .setTint(context.resources.getColor(R.color.control_spinner_dropdown, null))
606             selected.icon.setBounds(0, 0, iconSize, iconSize)
607             compoundDrawablePadding = (iconSize / 2.4f).toInt()
608             setCompoundDrawablesRelative(selected.icon, null, null, null)
609         }
610 
611         val anchor = parent.requireViewById<View>(R.id.app_or_structure_spinner)
612         if (items.size == 1) {
613             spinner.setBackground(null)
614             anchor.setOnClickListener(null)
615             anchor.isClickable = false
616             return
617         } else {
618             spinner.background = parent.context.resources
619                     .getDrawable(R.drawable.control_spinner_background)
620         }
621 
622         anchor.setOnClickListener(object : View.OnClickListener {
623             override fun onClick(v: View) {
624                 popup = ControlsPopupMenu(popupThemedContext).apply {
625                     anchorView = anchor
626                     width = ViewGroup.LayoutParams.MATCH_PARENT
627                     setAdapter(adapter)
628 
629                     setOnItemClickListener(object : AdapterView.OnItemClickListener {
630                         override fun onItemClick(
631                             parent: AdapterView<*>,
632                             view: View,
633                             pos: Int,
634                             id: Long
635                         ) {
636                             val listItem = parent.getItemAtPosition(pos) as SelectionItem
637                             this@ControlsUiControllerImpl.switchAppOrStructure(listItem)
638                             dismiss()
639                         }
640                     })
641                     show()
642                     listView?.post { listView?.requestAccessibilityFocus() }
643                 }
644             }
645         })
646     }
647 
648     private fun createControlsSpaceFrame() {
649         val inflater = LayoutInflater.from(activityContext)
650         inflater.inflate(R.layout.controls_with_favorites, parent, true)
651 
652         parent.requireViewById<ImageView>(R.id.controls_close).apply {
653             setOnClickListener { _: View -> onDismiss.run() }
654             visibility = View.VISIBLE
655         }
656     }
657 
658     private fun createListView(selected: SelectionItem) {
659         if (selectedItem !is SelectedItem.StructureItem) return
660         val selectedStructure = (selectedItem as SelectedItem.StructureItem).structure
661         val inflater = LayoutInflater.from(activityContext)
662 
663         val maxColumns = ControlAdapter.findMaxColumns(activityContext.resources)
664 
665         val listView = parent.requireViewById(R.id.controls_list) as ViewGroup
666         listView.removeAllViews()
667         var lastRow: ViewGroup = createRow(inflater, listView)
668         selectedStructure.controls.forEach {
669             val key = ControlKey(selectedStructure.componentName, it.controlId)
670             controlsById.get(key)?.let {
671                 if (lastRow.getChildCount() == maxColumns) {
672                     lastRow = createRow(inflater, listView)
673                 }
674                 val baseLayout = inflater.inflate(
675                     R.layout.controls_base_item, lastRow, false) as ViewGroup
676                 lastRow.addView(baseLayout)
677 
678                 // Use ConstraintLayout in the future... for now, manually adjust margins
679                 if (lastRow.getChildCount() == 1) {
680                     val lp = baseLayout.getLayoutParams() as ViewGroup.MarginLayoutParams
681                     lp.setMarginStart(0)
682                 }
683                 val cvh = ControlViewHolder(
684                     baseLayout,
685                     controlsController.get(),
686                     uiExecutor,
687                     bgExecutor,
688                     controlActionCoordinator,
689                     controlsMetricsLogger,
690                     selected.uid,
691                     controlsController.get().currentUserId,
692                 )
693                 cvh.bindData(it, false /* isLocked, will be ignored on initial load */)
694                 controlViewsById.put(key, cvh)
695             }
696         }
697 
698         // add spacers if necessary to keep control size consistent
699         val mod = selectedStructure.controls.size % maxColumns
700         var spacersToAdd = if (mod == 0) 0 else maxColumns - mod
701         val margin = context.resources.getDimensionPixelSize(R.dimen.control_spacing)
702         while (spacersToAdd > 0) {
703             val lp = LinearLayout.LayoutParams(0, 0, 1f).apply {
704                 setMarginStart(margin)
705             }
706             lastRow.addView(Space(context), lp)
707             spacersToAdd--
708         }
709     }
710 
711     override fun getPreferredSelectedItem(structures: List<StructureInfo>): SelectedItem {
712         val preferredPanel = selectedComponentRepository.getSelectedComponent()
713         val component = preferredPanel?.componentName ?: EMPTY_COMPONENT
714         return if (preferredPanel?.isPanel == true) {
715             SelectedItem.PanelItem(preferredPanel.name, component)
716         } else {
717             if (structures.isEmpty()) return SelectedItem.EMPTY_SELECTION
718             SelectedItem.StructureItem(structures.firstOrNull {
719                 component == it.componentName && preferredPanel?.name == it.structure
720             } ?: structures[0])
721         }
722     }
723 
724     private fun updatePreferences(selectedItem: SelectedItem) {
725         selectedComponentRepository.setSelectedComponent(
726                 SelectedComponentRepository.SelectedComponent(selectedItem)
727         )
728     }
729 
730     private fun maybeUpdateSelectedItem(item: SelectionItem): Boolean {
731         val newSelection = if (item.isPanel) {
732             SelectedItem.PanelItem(item.appName, item.componentName)
733         } else {
734             SelectedItem.StructureItem(allStructures.firstOrNull {
735                 it.structure == item.structure && it.componentName == item.componentName
736             } ?: EMPTY_STRUCTURE)
737         }
738         return if (newSelection != selectedItem ) {
739             selectedItem = newSelection
740             updatePreferences(selectedItem)
741             true
742         } else {
743             false
744         }
745     }
746 
747     private fun switchAppOrStructure(item: SelectionItem) {
748         if (maybeUpdateSelectedItem(item)) {
749             reload(parent)
750         }
751     }
752 
753     override fun closeDialogs(immediately: Boolean) {
754         if (immediately) {
755             popup?.dismissImmediate()
756         } else {
757             popup?.dismiss()
758         }
759         popup = null
760 
761         controlViewsById.forEach {
762             it.value.dismiss()
763         }
764         controlActionCoordinator.closeDialogs()
765         removeAppDialog?.cancel()
766     }
767 
768     override fun hide(parent: ViewGroup) {
769         // We need to check for the parent because it's possible that  we have started showing in a
770         // different activity. In that case, make sure to only clear things associated with the
771         // passed parent
772         if (parent == this.parent) {
773             Log.d(ControlsUiController.TAG, "hide()")
774             hidden = true
775 
776             closeDialogs(true)
777             controlsController.get().unsubscribe()
778             taskViewController?.removeTask()
779             taskViewController = null
780 
781             controlsById.clear()
782             controlViewsById.clear()
783 
784             controlsListingController.get().removeCallback(listingCallback)
785 
786             if (!retainCache) RenderInfo.clearCache()
787         }
788         parent.removeAllViews()
789     }
790 
791     override fun onRefreshState(componentName: ComponentName, controls: List<Control>) {
792         val isLocked = !keyguardStateController.isUnlocked()
793         controls.forEach { c ->
794             controlsById.get(ControlKey(componentName, c.getControlId()))?.let {
795                 Log.d(ControlsUiController.TAG, "onRefreshState() for id: " + c.getControlId())
796                 iconCache.store(componentName, c.controlId, c.customIcon)
797                 val cws = ControlWithState(componentName, it.ci, c)
798                 val key = ControlKey(componentName, c.getControlId())
799                 controlsById.put(key, cws)
800 
801                 controlViewsById.get(key)?.let {
802                     uiExecutor.execute { it.bindData(cws, isLocked) }
803                 }
804             }
805         }
806     }
807 
808     override fun onActionResponse(componentName: ComponentName, controlId: String, response: Int) {
809         val key = ControlKey(componentName, controlId)
810         uiExecutor.execute {
811             controlViewsById.get(key)?.actionResponse(response)
812         }
813     }
814 
815     override fun onSizeChange() {
816         selectionItem?.let {
817             when (selectedItem) {
818                 is SelectedItem.StructureItem -> createListView(it)
819                 is SelectedItem.PanelItem -> taskViewController?.refreshBounds() ?: reload(parent)
820             }
821         } ?: reload(parent)
822     }
823 
824     private fun createRow(inflater: LayoutInflater, listView: ViewGroup): ViewGroup {
825         val row = inflater.inflate(R.layout.controls_row, listView, false) as ViewGroup
826         listView.addView(row)
827         return row
828     }
829 
830     private fun findSelectionItem(si: SelectedItem, items: List<SelectionItem>): SelectionItem? =
831         items.firstOrNull { it.matches(si) }
832 
833     override fun dump(pw: PrintWriter, args: Array<out String>) = pw.asIndenting().run {
834         println("ControlsUiControllerImpl:")
835         withIncreasedIndent {
836             println("hidden: $hidden")
837             println("selectedItem: $selectedItem")
838             println("lastSelections: $lastSelections")
839             println("setting: ${controlsSettingsRepository
840                     .allowActionOnTrivialControlsInLockscreen.value}")
841         }
842     }
843 }
844 
845 @VisibleForTesting
846 internal data class SelectionItem(
847     val appName: CharSequence,
848     val structure: CharSequence,
849     val icon: Drawable,
850     val componentName: ComponentName,
851     val uid: Int,
852     val panelComponentName: ComponentName?
853 ) {
getTitlenull854     fun getTitle() = if (structure.isEmpty()) { appName } else { structure }
855 
856     val isPanel: Boolean = panelComponentName != null
857 
matchesnull858     fun matches(selectedItem: SelectedItem): Boolean {
859         if (componentName != selectedItem.componentName) {
860             // Not the same component so they are not the same.
861             return false
862         }
863         if (isPanel || selectedItem is SelectedItem.PanelItem) {
864             // As they have the same component, if [this.isPanel] then we may be migrating from
865             // device controls API into panel. Want this to match, even if the selectedItem is not
866             // a panel. We don't want to match on app name because that can change with locale.
867             return true
868         }
869         // Return true if we find a structure with the correct name
870         return structure == (selectedItem as SelectedItem.StructureItem).structure.structure
871     }
872 }
873 
874 private class ItemAdapter(parentContext: Context, val resource: Int) :
875         ArrayAdapter<SelectionItem>(parentContext, resource) {
876 
877     private val layoutInflater = LayoutInflater.from(context)!!
878 
getViewnull879     override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
880         val item: SelectionItem = getItem(position)!!
881         val view = convertView ?: layoutInflater.inflate(resource, parent, false)
882         with(view.tag as? ViewHolder ?: ViewHolder(view).also { view.tag = it }) {
883             titleView.text = item.getTitle()
884             iconView.setImageDrawable(item.icon)
885         }
886         return view
887     }
888 
889     private class ViewHolder(itemView: View) {
890 
891         val titleView: TextView = itemView.requireViewById(R.id.controls_spinner_item)
892         val iconView: ImageView = itemView.requireViewById(R.id.app_icon)
893     }
894 }
895