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