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.content.ComponentName
25 import android.content.Context
26 import android.content.Intent
27 import android.content.SharedPreferences
28 import android.content.res.Configuration
29 import android.graphics.drawable.Drawable
30 import android.graphics.drawable.LayerDrawable
31 import android.service.controls.Control
32 import android.util.Log
33 import android.util.TypedValue
34 import android.view.ContextThemeWrapper
35 import android.view.LayoutInflater
36 import android.view.View
37 import android.view.ViewGroup
38 import android.view.animation.AccelerateInterpolator
39 import android.view.animation.DecelerateInterpolator
40 import android.widget.AdapterView
41 import android.widget.ArrayAdapter
42 import android.widget.ImageView
43 import android.widget.LinearLayout
44 import android.widget.ListPopupWindow
45 import android.widget.Space
46 import android.widget.TextView
47 import com.android.systemui.R
48 import com.android.systemui.controls.ControlsMetricsLogger
49 import com.android.systemui.controls.ControlsServiceInfo
50 import com.android.systemui.controls.CustomIconCache
51 import com.android.systemui.controls.controller.ControlInfo
52 import com.android.systemui.controls.controller.ControlsController
53 import com.android.systemui.controls.controller.StructureInfo
54 import com.android.systemui.controls.management.ControlsEditingActivity
55 import com.android.systemui.controls.management.ControlsFavoritingActivity
56 import com.android.systemui.controls.management.ControlsListingController
57 import com.android.systemui.controls.management.ControlsProviderSelectorActivity
58 import com.android.systemui.dagger.SysUISingleton
59 import com.android.systemui.dagger.qualifiers.Background
60 import com.android.systemui.dagger.qualifiers.Main
61 import com.android.systemui.globalactions.GlobalActionsPopupMenu
62 import com.android.systemui.plugins.ActivityStarter
63 import com.android.systemui.statusbar.phone.ShadeController
64 import com.android.systemui.statusbar.policy.KeyguardStateController
65 import com.android.systemui.util.concurrency.DelayableExecutor
66 import dagger.Lazy
67 import java.text.Collator
68 import java.util.function.Consumer
69 import javax.inject.Inject
70 
71 private data class ControlKey(val componentName: ComponentName, val controlId: String)
72 
73 @SysUISingleton
74 class ControlsUiControllerImpl @Inject constructor (
75     val controlsController: Lazy<ControlsController>,
76     val context: Context,
77     @Main val uiExecutor: DelayableExecutor,
78     @Background val bgExecutor: DelayableExecutor,
79     val controlsListingController: Lazy<ControlsListingController>,
80     @Main val sharedPreferences: SharedPreferences,
81     val controlActionCoordinator: ControlActionCoordinator,
82     private val activityStarter: ActivityStarter,
83     private val shadeController: ShadeController,
84     private val iconCache: CustomIconCache,
85     private val controlsMetricsLogger: ControlsMetricsLogger,
86     private val keyguardStateController: KeyguardStateController
87 ) : ControlsUiController {
88 
89     companion object {
90         private const val PREF_COMPONENT = "controls_component"
91         private const val PREF_STRUCTURE = "controls_structure"
92 
93         private const val FADE_IN_MILLIS = 200L
94 
95         private val EMPTY_COMPONENT = ComponentName("", "")
96         private val EMPTY_STRUCTURE = StructureInfo(
97             EMPTY_COMPONENT,
98             "",
99             mutableListOf<ControlInfo>()
100         )
101     }
102 
103     private var selectedStructure: StructureInfo = EMPTY_STRUCTURE
104     private lateinit var allStructures: List<StructureInfo>
105     private val controlsById = mutableMapOf<ControlKey, ControlWithState>()
106     private val controlViewsById = mutableMapOf<ControlKey, ControlViewHolder>()
107     private lateinit var parent: ViewGroup
108     private lateinit var lastItems: List<SelectionItem>
109     private var popup: ListPopupWindow? = null
110     private var hidden = true
111     private lateinit var onDismiss: Runnable
112     private val popupThemedContext = ContextThemeWrapper(context, R.style.Control_ListPopupWindow)
113     private var retainCache = false
114 
115     private val collator = Collator.getInstance(context.resources.configuration.locales[0])
116     private val localeComparator = compareBy<SelectionItem, CharSequence>(collator) {
117         it.getTitle()
118     }
119 
120     private val onSeedingComplete = Consumer<Boolean> {
121         accepted ->
122             if (accepted) {
123                 selectedStructure = controlsController.get().getFavorites().maxByOrNull {
124                     it.controls.size
125                 } ?: EMPTY_STRUCTURE
126                 updatePreferences(selectedStructure)
127             }
128             reload(parent)
129     }
130 
131     private lateinit var activityContext: Context
132     private lateinit var listingCallback: ControlsListingController.ControlsListingCallback
133 
134     private fun createCallback(
135         onResult: (List<SelectionItem>) -> Unit
136     ): ControlsListingController.ControlsListingCallback {
137         return object : ControlsListingController.ControlsListingCallback {
138             override fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>) {
139                 val lastItems = serviceInfos.map {
140                     val uid = it.serviceInfo.applicationInfo.uid
141                     SelectionItem(it.loadLabel(), "", it.loadIcon(), it.componentName, uid)
142                 }
143                 uiExecutor.execute {
144                     parent.removeAllViews()
145                     if (lastItems.size > 0) {
146                         onResult(lastItems)
147                     }
148                 }
149             }
150         }
151     }
152 
153     override fun show(
154         parent: ViewGroup,
155         onDismiss: Runnable,
156         activityContext: Context
157     ) {
158         Log.d(ControlsUiController.TAG, "show()")
159         this.parent = parent
160         this.onDismiss = onDismiss
161         this.activityContext = activityContext
162         hidden = false
163         retainCache = false
164 
165         controlActionCoordinator.activityContext = activityContext
166 
167         allStructures = controlsController.get().getFavorites()
168         selectedStructure = getPreferredStructure(allStructures)
169 
170         if (controlsController.get().addSeedingFavoritesCallback(onSeedingComplete)) {
171             listingCallback = createCallback(::showSeedingView)
172         } else if (selectedStructure.controls.isEmpty() && allStructures.size <= 1) {
173             // only show initial view if there are really no favorites across any structure
174             listingCallback = createCallback(::showInitialSetupView)
175         } else {
176             selectedStructure.controls.map {
177                 ControlWithState(selectedStructure.componentName, it, null)
178             }.associateByTo(controlsById) {
179                 ControlKey(selectedStructure.componentName, it.ci.controlId)
180             }
181             listingCallback = createCallback(::showControlsView)
182             controlsController.get().subscribeToFavorites(selectedStructure)
183         }
184 
185         controlsListingController.get().addCallback(listingCallback)
186     }
187 
188     private fun reload(parent: ViewGroup) {
189         if (hidden) return
190 
191         controlsListingController.get().removeCallback(listingCallback)
192         controlsController.get().unsubscribe()
193 
194         val fadeAnim = ObjectAnimator.ofFloat(parent, "alpha", 1.0f, 0.0f)
195         fadeAnim.setInterpolator(AccelerateInterpolator(1.0f))
196         fadeAnim.setDuration(FADE_IN_MILLIS)
197         fadeAnim.addListener(object : AnimatorListenerAdapter() {
198             override fun onAnimationEnd(animation: Animator) {
199                 controlViewsById.clear()
200                 controlsById.clear()
201 
202                 show(parent, onDismiss, activityContext)
203                 val showAnim = ObjectAnimator.ofFloat(parent, "alpha", 0.0f, 1.0f)
204                 showAnim.setInterpolator(DecelerateInterpolator(1.0f))
205                 showAnim.setDuration(FADE_IN_MILLIS)
206                 showAnim.start()
207             }
208         })
209         fadeAnim.start()
210     }
211 
212     private fun showSeedingView(items: List<SelectionItem>) {
213         val inflater = LayoutInflater.from(context)
214         inflater.inflate(R.layout.controls_no_favorites, parent, true)
215         val subtitle = parent.requireViewById<TextView>(R.id.controls_subtitle)
216         subtitle.setText(context.resources.getString(R.string.controls_seeding_in_progress))
217     }
218 
219     private fun showInitialSetupView(items: List<SelectionItem>) {
220         startProviderSelectorActivity()
221         onDismiss.run()
222     }
223 
224     private fun startFavoritingActivity(si: StructureInfo) {
225         startTargetedActivity(si, ControlsFavoritingActivity::class.java)
226     }
227 
228     private fun startEditingActivity(si: StructureInfo) {
229         startTargetedActivity(si, ControlsEditingActivity::class.java)
230     }
231 
232     private fun startTargetedActivity(si: StructureInfo, klazz: Class<*>) {
233         val i = Intent(activityContext, klazz)
234         putIntentExtras(i, si)
235         startActivity(i)
236 
237         retainCache = true
238     }
239 
240     private fun putIntentExtras(intent: Intent, si: StructureInfo) {
241         intent.apply {
242             putExtra(ControlsFavoritingActivity.EXTRA_APP,
243                     controlsListingController.get().getAppLabel(si.componentName))
244             putExtra(ControlsFavoritingActivity.EXTRA_STRUCTURE, si.structure)
245             putExtra(Intent.EXTRA_COMPONENT_NAME, si.componentName)
246         }
247     }
248 
249     private fun startProviderSelectorActivity() {
250         val i = Intent(activityContext, ControlsProviderSelectorActivity::class.java)
251         i.putExtra(ControlsProviderSelectorActivity.BACK_SHOULD_EXIT, true)
252         startActivity(i)
253     }
254 
255     private fun startActivity(intent: Intent) {
256         // Force animations when transitioning from a dialog to an activity
257         intent.putExtra(ControlsUiController.EXTRA_ANIMATE, true)
258 
259         if (keyguardStateController.isShowing()) {
260             activityStarter.postStartActivityDismissingKeyguard(intent, 0 /* delay */)
261         } else {
262             activityContext.startActivity(
263                 intent,
264                 ActivityOptions.makeSceneTransitionAnimation(activityContext as Activity).toBundle()
265             )
266         }
267     }
268 
269     private fun showControlsView(items: List<SelectionItem>) {
270         controlViewsById.clear()
271 
272         val itemsByComponent = items.associateBy { it.componentName }
273         val itemsWithStructure = mutableListOf<SelectionItem>()
274         allStructures.mapNotNullTo(itemsWithStructure) {
275             itemsByComponent.get(it.componentName)?.copy(structure = it.structure)
276         }
277         itemsWithStructure.sortWith(localeComparator)
278 
279         val selectionItem = findSelectionItem(selectedStructure, itemsWithStructure) ?: items[0]
280 
281         controlsMetricsLogger.refreshBegin(selectionItem.uid, !keyguardStateController.isUnlocked())
282 
283         createListView(selectionItem)
284         createDropDown(itemsWithStructure, selectionItem)
285         createMenu()
286     }
287 
288     private fun createMenu() {
289         val items = arrayOf(
290             context.resources.getString(R.string.controls_menu_add),
291             context.resources.getString(R.string.controls_menu_edit)
292         )
293         var adapter = ArrayAdapter<String>(context, R.layout.controls_more_item, items)
294 
295         val anchor = parent.requireViewById<ImageView>(R.id.controls_more)
296         anchor.setOnClickListener(object : View.OnClickListener {
297             override fun onClick(v: View) {
298                 popup = GlobalActionsPopupMenu(
299                         popupThemedContext,
300                         false /* isDropDownMode */
301                 ).apply {
302                     setAnchorView(anchor)
303                     setAdapter(adapter)
304                     setOnItemClickListener(object : AdapterView.OnItemClickListener {
305                         override fun onItemClick(
306                             parent: AdapterView<*>,
307                             view: View,
308                             pos: Int,
309                             id: Long
310                         ) {
311                             when (pos) {
312                                 // 0: Add Control
313                                 0 -> startFavoritingActivity(selectedStructure)
314                                 // 1: Edit controls
315                                 1 -> startEditingActivity(selectedStructure)
316                             }
317                             dismiss()
318                         }
319                     })
320                     show()
321                 }
322             }
323         })
324     }
325 
326     private fun createDropDown(items: List<SelectionItem>, selected: SelectionItem) {
327         items.forEach {
328             RenderInfo.registerComponentIcon(it.componentName, it.icon)
329         }
330 
331         var adapter = ItemAdapter(context, R.layout.controls_spinner_item).apply {
332             addAll(items)
333         }
334 
335         /*
336          * Default spinner widget does not work with the window type required
337          * for this dialog. Use a textView with the ListPopupWindow to achieve
338          * a similar effect
339          */
340         val spinner = parent.requireViewById<TextView>(R.id.app_or_structure_spinner).apply {
341             setText(selected.getTitle())
342             // override the default color on the dropdown drawable
343             (getBackground() as LayerDrawable).getDrawable(0)
344                 .setTint(context.resources.getColor(R.color.control_spinner_dropdown, null))
345         }
346 
347         if (items.size == 1) {
348             spinner.setBackground(null)
349             return
350         }
351 
352         val anchor = parent.requireViewById<ViewGroup>(R.id.controls_header)
353         anchor.setOnClickListener(object : View.OnClickListener {
354             override fun onClick(v: View) {
355                 popup = GlobalActionsPopupMenu(
356                         popupThemedContext,
357                         true /* isDropDownMode */
358                 ).apply {
359                     setAnchorView(anchor)
360                     setAdapter(adapter)
361 
362                     setOnItemClickListener(object : AdapterView.OnItemClickListener {
363                         override fun onItemClick(
364                             parent: AdapterView<*>,
365                             view: View,
366                             pos: Int,
367                             id: Long
368                         ) {
369                             val listItem = parent.getItemAtPosition(pos) as SelectionItem
370                             this@ControlsUiControllerImpl.switchAppOrStructure(listItem)
371                             dismiss()
372                         }
373                     })
374                     show()
375                 }
376             }
377         })
378     }
379 
380     private fun createListView(selected: SelectionItem) {
381         val inflater = LayoutInflater.from(context)
382         inflater.inflate(R.layout.controls_with_favorites, parent, true)
383 
384         parent.requireViewById<ImageView>(R.id.controls_close).apply {
385             setOnClickListener { _: View -> onDismiss.run() }
386             visibility = View.VISIBLE
387         }
388 
389         val maxColumns = findMaxColumns()
390 
391         val listView = parent.requireViewById(R.id.global_actions_controls_list) as ViewGroup
392         var lastRow: ViewGroup = createRow(inflater, listView)
393         selectedStructure.controls.forEach {
394             val key = ControlKey(selectedStructure.componentName, it.controlId)
395             controlsById.get(key)?.let {
396                 if (lastRow.getChildCount() == maxColumns) {
397                     lastRow = createRow(inflater, listView)
398                 }
399                 val baseLayout = inflater.inflate(
400                     R.layout.controls_base_item, lastRow, false) as ViewGroup
401                 lastRow.addView(baseLayout)
402 
403                 // Use ConstraintLayout in the future... for now, manually adjust margins
404                 if (lastRow.getChildCount() == 1) {
405                     val lp = baseLayout.getLayoutParams() as ViewGroup.MarginLayoutParams
406                     lp.setMarginStart(0)
407                 }
408                 val cvh = ControlViewHolder(
409                     baseLayout,
410                     controlsController.get(),
411                     uiExecutor,
412                     bgExecutor,
413                     controlActionCoordinator,
414                     controlsMetricsLogger,
415                     selected.uid
416                 )
417                 cvh.bindData(it, false /* isLocked, will be ignored on initial load */)
418                 controlViewsById.put(key, cvh)
419             }
420         }
421 
422         // add spacers if necessary to keep control size consistent
423         val mod = selectedStructure.controls.size % maxColumns
424         var spacersToAdd = if (mod == 0) 0 else maxColumns - mod
425         val margin = context.resources.getDimensionPixelSize(R.dimen.control_spacing)
426         while (spacersToAdd > 0) {
427             val lp = LinearLayout.LayoutParams(0, 0, 1f).apply {
428                 setMarginStart(margin)
429             }
430             lastRow.addView(Space(context), lp)
431             spacersToAdd--
432         }
433     }
434 
435     /**
436      * For low-dp width screens that also employ an increased font scale, adjust the
437      * number of columns. This helps prevent text truncation on these devices.
438      */
439     private fun findMaxColumns(): Int {
440         val res = context.resources
441         var maxColumns = res.getInteger(R.integer.controls_max_columns)
442         val maxColumnsAdjustWidth =
443             res.getInteger(R.integer.controls_max_columns_adjust_below_width_dp)
444 
445         val outValue = TypedValue()
446         res.getValue(R.dimen.controls_max_columns_adjust_above_font_scale, outValue, true)
447         val maxColumnsAdjustFontScale = outValue.getFloat()
448 
449         val config = res.configuration
450         val isPortrait = config.orientation == Configuration.ORIENTATION_PORTRAIT
451         if (isPortrait &&
452             config.screenWidthDp != Configuration.SCREEN_WIDTH_DP_UNDEFINED &&
453             config.screenWidthDp <= maxColumnsAdjustWidth &&
454             config.fontScale >= maxColumnsAdjustFontScale) {
455             maxColumns--
456         }
457 
458         return maxColumns
459     }
460 
461     override fun getPreferredStructure(structures: List<StructureInfo>): StructureInfo {
462         if (structures.isEmpty()) return EMPTY_STRUCTURE
463 
464         val component = sharedPreferences.getString(PREF_COMPONENT, null)?.let {
465             ComponentName.unflattenFromString(it)
466         } ?: EMPTY_COMPONENT
467         val structure = sharedPreferences.getString(PREF_STRUCTURE, "")
468 
469         return structures.firstOrNull {
470             component == it.componentName && structure == it.structure
471         } ?: structures.get(0)
472     }
473 
474     private fun updatePreferences(si: StructureInfo) {
475         if (si == EMPTY_STRUCTURE) return
476         sharedPreferences.edit()
477             .putString(PREF_COMPONENT, si.componentName.flattenToString())
478             .putString(PREF_STRUCTURE, si.structure.toString())
479             .commit()
480     }
481 
482     private fun switchAppOrStructure(item: SelectionItem) {
483         val newSelection = allStructures.first {
484             it.structure == item.structure && it.componentName == item.componentName
485         }
486 
487         if (newSelection != selectedStructure) {
488             selectedStructure = newSelection
489             updatePreferences(selectedStructure)
490             reload(parent)
491         }
492     }
493 
494     override fun closeDialogs(immediately: Boolean) {
495         if (immediately) {
496             popup?.dismissImmediate()
497         } else {
498             popup?.dismiss()
499         }
500         popup = null
501 
502         controlViewsById.forEach {
503             it.value.dismiss()
504         }
505         controlActionCoordinator.closeDialogs()
506     }
507 
508     override fun hide() {
509         hidden = true
510 
511         closeDialogs(true)
512         controlsController.get().unsubscribe()
513 
514         parent.removeAllViews()
515         controlsById.clear()
516         controlViewsById.clear()
517 
518         controlsListingController.get().removeCallback(listingCallback)
519 
520         if (!retainCache) RenderInfo.clearCache()
521     }
522 
523     override fun onRefreshState(componentName: ComponentName, controls: List<Control>) {
524         val isLocked = !keyguardStateController.isUnlocked()
525         controls.forEach { c ->
526             controlsById.get(ControlKey(componentName, c.getControlId()))?.let {
527                 Log.d(ControlsUiController.TAG, "onRefreshState() for id: " + c.getControlId())
528                 iconCache.store(componentName, c.controlId, c.customIcon)
529                 val cws = ControlWithState(componentName, it.ci, c)
530                 val key = ControlKey(componentName, c.getControlId())
531                 controlsById.put(key, cws)
532 
533                 controlViewsById.get(key)?.let {
534                     uiExecutor.execute { it.bindData(cws, isLocked) }
535                 }
536             }
537         }
538     }
539 
540     override fun onActionResponse(componentName: ComponentName, controlId: String, response: Int) {
541         val key = ControlKey(componentName, controlId)
542         uiExecutor.execute {
543             controlViewsById.get(key)?.actionResponse(response)
544         }
545     }
546 
547     private fun createRow(inflater: LayoutInflater, listView: ViewGroup): ViewGroup {
548         val row = inflater.inflate(R.layout.controls_row, listView, false) as ViewGroup
549         listView.addView(row)
550         return row
551     }
552 
553     private fun findSelectionItem(si: StructureInfo, items: List<SelectionItem>): SelectionItem? =
554         items.firstOrNull {
555             it.componentName == si.componentName && it.structure == si.structure
556         }
557 }
558 
559 private data class SelectionItem(
560     val appName: CharSequence,
561     val structure: CharSequence,
562     val icon: Drawable,
563     val componentName: ComponentName,
564     val uid: Int
565 ) {
566     fun getTitle() = if (structure.isEmpty()) { appName } else { structure }
567 }
568 
569 private class ItemAdapter(
570     val parentContext: Context,
571     val resource: Int
572 ) : ArrayAdapter<SelectionItem>(parentContext, resource) {
573 
574     val layoutInflater = LayoutInflater.from(context)
575 
576     override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
577         val item = getItem(position)
578         val view = convertView ?: layoutInflater.inflate(resource, parent, false)
579         view.requireViewById<TextView>(R.id.controls_spinner_item).apply {
580             setText(item.getTitle())
581         }
582         view.requireViewById<ImageView>(R.id.app_icon).apply {
583             setImageDrawable(item.icon)
584         }
585         return view
586     }
587 }
588