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