blob: fab6fc7357dd8c9a8ee0282007f99d986d800be7 [file] [log] [blame]
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -05001/*
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
17package com.android.systemui.controls.ui
18
Matt Pietalcd757c82020-04-08 10:20:48 -040019import android.animation.Animator
20import android.animation.AnimatorListenerAdapter
21import android.animation.ObjectAnimator
22import android.app.AlertDialog
Matt Pietalb7da66c2020-03-06 10:49:31 -050023import android.app.Dialog
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -050024import android.content.ComponentName
Matt Pietal22231792020-01-23 09:51:09 -050025import android.content.Context
Matt Pietalcd757c82020-04-08 10:20:48 -040026import android.content.DialogInterface
Matt Pietal22231792020-01-23 09:51:09 -050027import android.content.Intent
Matt Pietal638253a2020-03-02 09:10:43 -050028import android.content.SharedPreferences
Matt Pietal5f478c72020-04-01 15:53:54 -040029import android.content.res.Configuration
Matt Pietal37cc1902020-02-06 10:25:56 -050030import android.graphics.drawable.Drawable
Matt Pietal9ef947a2020-03-04 08:22:03 -050031import android.graphics.drawable.LayerDrawable
Matt Pietalcd757c82020-04-08 10:20:48 -040032import android.os.Process
Matt Pietaldc78c842020-03-30 08:09:18 -040033import android.provider.Settings
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -050034import android.service.controls.Control
Matt Pietalb7da66c2020-03-06 10:49:31 -050035import android.service.controls.actions.ControlAction
Matt Pietal22231792020-01-23 09:51:09 -050036import android.util.Log
Fabian Kozynski8765d352020-04-06 21:16:02 -040037import android.util.TypedValue
Matt Pietal638253a2020-03-02 09:10:43 -050038import android.view.ContextThemeWrapper
Matt Pietal22231792020-01-23 09:51:09 -050039import android.view.LayoutInflater
40import android.view.View
Matt Pietal5ebb7fe2020-03-20 12:58:54 -040041import android.view.View.MeasureSpec
Matt Pietal22231792020-01-23 09:51:09 -050042import android.view.ViewGroup
Matt Pietal638253a2020-03-02 09:10:43 -050043import android.view.WindowManager
Fabian Kozynski8765d352020-04-06 21:16:02 -040044import android.view.animation.AccelerateInterpolator
45import android.view.animation.DecelerateInterpolator
Matt Pietal638253a2020-03-02 09:10:43 -050046import android.widget.AdapterView
47import android.widget.ArrayAdapter
Matt Pietal37cc1902020-02-06 10:25:56 -050048import android.widget.ImageView
Matt Pietale0661b62020-01-29 14:35:31 -050049import android.widget.LinearLayout
Matt Pietal638253a2020-03-02 09:10:43 -050050import android.widget.ListPopupWindow
Matt Pietale0661b62020-01-29 14:35:31 -050051import android.widget.Space
Matt Pietal638253a2020-03-02 09:10:43 -050052import android.widget.TextView
Fabian Kozynski8765d352020-04-06 21:16:02 -040053import com.android.systemui.R
Matt Pietal638253a2020-03-02 09:10:43 -050054import com.android.systemui.controls.ControlsServiceInfo
Matt Pietal22231792020-01-23 09:51:09 -050055import com.android.systemui.controls.controller.ControlInfo
Matt Pietal313f37d2020-02-24 11:27:22 -050056import com.android.systemui.controls.controller.ControlsController
57import com.android.systemui.controls.controller.StructureInfo
Fabian Kozynski8765d352020-04-06 21:16:02 -040058import com.android.systemui.controls.management.ControlsEditingActivity
Matt Pietald2ea5312020-04-06 11:27:02 -040059import com.android.systemui.controls.management.ControlsFavoritingActivity
Matt Pietal37cc1902020-02-06 10:25:56 -050060import com.android.systemui.controls.management.ControlsListingController
Matt Pietal22231792020-01-23 09:51:09 -050061import com.android.systemui.controls.management.ControlsProviderSelectorActivity
Matt Pietal37cc1902020-02-06 10:25:56 -050062import com.android.systemui.dagger.qualifiers.Background
Matt Pietal22231792020-01-23 09:51:09 -050063import com.android.systemui.dagger.qualifiers.Main
Matt Pietal1aac43b2020-02-04 15:43:31 -050064import com.android.systemui.util.concurrency.DelayableExecutor
Matt Pietal22231792020-01-23 09:51:09 -050065import dagger.Lazy
Matt Pietal37cc1902020-02-06 10:25:56 -050066import java.text.Collator
Matt Pietal61266442020-03-17 12:53:44 -040067import java.util.function.Consumer
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -050068import javax.inject.Inject
69import javax.inject.Singleton
70
Matt Pietal1aac43b2020-02-04 15:43:31 -050071private data class ControlKey(val componentName: ComponentName, val controlId: String)
72
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -050073@Singleton
Matt Pietal22231792020-01-23 09:51:09 -050074class ControlsUiControllerImpl @Inject constructor (
75 val controlsController: Lazy<ControlsController>,
76 val context: Context,
Matt Pietal37cc1902020-02-06 10:25:56 -050077 @Main val uiExecutor: DelayableExecutor,
78 @Background val bgExecutor: DelayableExecutor,
Matt Pietal638253a2020-03-02 09:10:43 -050079 val controlsListingController: Lazy<ControlsListingController>,
80 @Main val sharedPreferences: SharedPreferences
Matt Pietal22231792020-01-23 09:51:09 -050081) : ControlsUiController {
82
Matt Pietal313f37d2020-02-24 11:27:22 -050083 companion object {
Matt Pietal638253a2020-03-02 09:10:43 -050084 private const val PREF_COMPONENT = "controls_component"
85 private const val PREF_STRUCTURE = "controls_structure"
86
Matt Pietaldc78c842020-03-30 08:09:18 -040087 private const val USE_PANELS = "systemui.controls_use_panel"
Matt Pietalcd757c82020-04-08 10:20:48 -040088 private const val FADE_IN_MILLIS = 225L
89
Matt Pietal638253a2020-03-02 09:10:43 -050090 private val EMPTY_COMPONENT = ComponentName("", "")
Matt Pietal313f37d2020-02-24 11:27:22 -050091 private val EMPTY_STRUCTURE = StructureInfo(
Matt Pietal638253a2020-03-02 09:10:43 -050092 EMPTY_COMPONENT,
Matt Pietal313f37d2020-02-24 11:27:22 -050093 "",
94 mutableListOf<ControlInfo>()
95 )
96 }
97
98 private var selectedStructure: StructureInfo = EMPTY_STRUCTURE
99 private lateinit var allStructures: List<StructureInfo>
Matt Pietal1aac43b2020-02-04 15:43:31 -0500100 private val controlsById = mutableMapOf<ControlKey, ControlWithState>()
101 private val controlViewsById = mutableMapOf<ControlKey, ControlViewHolder>()
Matt Pietal22231792020-01-23 09:51:09 -0500102 private lateinit var parent: ViewGroup
Matt Pietal638253a2020-03-02 09:10:43 -0500103 private lateinit var lastItems: List<SelectionItem>
104 private var popup: ListPopupWindow? = null
Matt Pietalb7da66c2020-03-06 10:49:31 -0500105 private var activeDialog: Dialog? = null
Matt Pietal61266442020-03-17 12:53:44 -0400106 private var hidden = true
Matt Pietal9ef947a2020-03-04 08:22:03 -0500107
Fabian Kozynski8b540452020-02-04 15:16:30 -0500108 override val available: Boolean
109 get() = controlsController.get().available
110
Matt Pietal638253a2020-03-02 09:10:43 -0500111 private lateinit var listingCallback: ControlsListingController.ControlsListingCallback
Matt Pietal37cc1902020-02-06 10:25:56 -0500112
Matt Pietal638253a2020-03-02 09:10:43 -0500113 private fun createCallback(
114 onResult: (List<SelectionItem>) -> Unit
115 ): ControlsListingController.ControlsListingCallback {
116 return object : ControlsListingController.ControlsListingCallback {
117 override fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>) {
118 bgExecutor.execute {
119 val collator = Collator.getInstance(context.resources.configuration.locales[0])
120 val localeComparator = compareBy<ControlsServiceInfo, CharSequence>(collator) {
121 it.loadLabel()
122 }
123
124 val mList = serviceInfos.toMutableList()
125 mList.sortWith(localeComparator)
126 lastItems = mList.map {
127 SelectionItem(it.loadLabel(), "", it.loadIcon(), it.componentName)
128 }
129 uiExecutor.execute {
130 onResult(lastItems)
131 }
132 }
Matt Pietal37cc1902020-02-06 10:25:56 -0500133 }
134 }
135 }
136
Matt Pietal22231792020-01-23 09:51:09 -0500137 override fun show(parent: ViewGroup) {
Matt Pietal307b1ef2020-02-10 07:27:01 -0500138 Log.d(ControlsUiController.TAG, "show()")
Matt Pietal22231792020-01-23 09:51:09 -0500139 this.parent = parent
Matt Pietal61266442020-03-17 12:53:44 -0400140 hidden = false
Matt Pietal22231792020-01-23 09:51:09 -0500141
Matt Pietal313f37d2020-02-24 11:27:22 -0500142 allStructures = controlsController.get().getFavorites()
Matt Pietal638253a2020-03-02 09:10:43 -0500143 selectedStructure = loadPreference(allStructures)
Matt Pietal22231792020-01-23 09:51:09 -0500144
Matt Pietal61266442020-03-17 12:53:44 -0400145 val cb = Consumer<Boolean> { _ -> reload(parent) }
146 if (controlsController.get().addSeedingFavoritesCallback(cb)) {
147 listingCallback = createCallback(::showSeedingView)
148 } else if (selectedStructure.controls.isEmpty() && allStructures.size <= 1) {
Matt Pietal638253a2020-03-02 09:10:43 -0500149 // only show initial view if there are really no favorites across any structure
150 listingCallback = createCallback(::showInitialSetupView)
Matt Pietal22231792020-01-23 09:51:09 -0500151 } else {
Matt Pietal313f37d2020-02-24 11:27:22 -0500152 selectedStructure.controls.map {
153 ControlWithState(selectedStructure.componentName, it, null)
154 }.associateByTo(controlsById) {
155 ControlKey(selectedStructure.componentName, it.ci.controlId)
156 }
Matt Pietal638253a2020-03-02 09:10:43 -0500157 listingCallback = createCallback(::showControlsView)
Matt Pietalfec10fa2020-03-09 14:42:46 -0400158 controlsController.get().subscribeToFavorites(selectedStructure)
Matt Pietal22231792020-01-23 09:51:09 -0500159 }
160
Matt Pietal638253a2020-03-02 09:10:43 -0500161 controlsListingController.get().addCallback(listingCallback)
Matt Pietal22231792020-01-23 09:51:09 -0500162 }
163
Matt Pietal61266442020-03-17 12:53:44 -0400164 private fun reload(parent: ViewGroup) {
165 if (hidden) return
Matt Pietalcd757c82020-04-08 10:20:48 -0400166
167 val fadeAnim = ObjectAnimator.ofFloat(parent, "alpha", 1.0f, 0.0f)
168 fadeAnim.setInterpolator(AccelerateInterpolator(1.0f))
169 fadeAnim.setDuration(FADE_IN_MILLIS)
170 fadeAnim.addListener(object : AnimatorListenerAdapter() {
171 override fun onAnimationEnd(animation: Animator) {
172 show(parent)
173 val showAnim = ObjectAnimator.ofFloat(parent, "alpha", 0.0f, 1.0f)
174 showAnim.setInterpolator(DecelerateInterpolator(1.0f))
175 showAnim.setDuration(FADE_IN_MILLIS)
176 showAnim.start()
177 }
178 })
179 fadeAnim.start()
Matt Pietal61266442020-03-17 12:53:44 -0400180 }
181
182 private fun showSeedingView(items: List<SelectionItem>) {
183 parent.removeAllViews()
184
185 val inflater = LayoutInflater.from(context)
186 inflater.inflate(R.layout.controls_no_favorites, parent, true)
187 val subtitle = parent.requireViewById<TextView>(R.id.controls_subtitle)
Matt Pietal9e3d40d2020-03-24 14:38:39 -0400188 subtitle.setText(context.resources.getString(R.string.controls_seeding_in_progress))
Matt Pietal61266442020-03-17 12:53:44 -0400189 }
190
Matt Pietal638253a2020-03-02 09:10:43 -0500191 private fun showInitialSetupView(items: List<SelectionItem>) {
192 parent.removeAllViews()
193
Matt Pietal22231792020-01-23 09:51:09 -0500194 val inflater = LayoutInflater.from(context)
195 inflater.inflate(R.layout.controls_no_favorites, parent, true)
196
Matt Pietal37cc1902020-02-06 10:25:56 -0500197 val viewGroup = parent.requireViewById(R.id.controls_no_favorites_group) as ViewGroup
Matt Pietald2ea5312020-04-06 11:27:02 -0400198 viewGroup.setOnClickListener { v: View -> startProviderSelectorActivity(v.context) }
Matt Pietal37cc1902020-02-06 10:25:56 -0500199
Matt Pietal9e3d40d2020-03-24 14:38:39 -0400200 val subtitle = parent.requireViewById<TextView>(R.id.controls_subtitle)
201 subtitle.setText(context.resources.getString(R.string.quick_controls_subtitle))
202
Matt Pietal638253a2020-03-02 09:10:43 -0500203 val iconRowGroup = parent.requireViewById(R.id.controls_icon_row) as ViewGroup
204 items.forEach {
205 val imageView = inflater.inflate(R.layout.controls_icon, viewGroup, false) as ImageView
206 imageView.setContentDescription(it.getTitle())
207 imageView.setImageDrawable(it.icon)
208 iconRowGroup.addView(imageView)
Matt Pietal37cc1902020-02-06 10:25:56 -0500209 }
Matt Pietale0661b62020-01-29 14:35:31 -0500210 }
211
Matt Pietald2ea5312020-04-06 11:27:02 -0400212 private fun startFavoritingActivity(context: Context, si: StructureInfo) {
Fabian Kozynski8765d352020-04-06 21:16:02 -0400213 startTargetedActivity(context, si, ControlsFavoritingActivity::class.java)
214 }
215
216 private fun startEditingActivity(context: Context, si: StructureInfo) {
217 startTargetedActivity(context, si, ControlsEditingActivity::class.java)
218 }
219
220 private fun startTargetedActivity(context: Context, si: StructureInfo, klazz: Class<*>) {
221 val i = Intent(context, klazz).apply {
Matt Pietald2ea5312020-04-06 11:27:02 -0400222 addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
Matt Pietal22231792020-01-23 09:51:09 -0500223 }
Fabian Kozynski8765d352020-04-06 21:16:02 -0400224 putIntentExtras(i, si)
Matt Pietald2ea5312020-04-06 11:27:02 -0400225 startActivity(context, i)
226 }
227
Fabian Kozynski8765d352020-04-06 21:16:02 -0400228 private fun putIntentExtras(intent: Intent, si: StructureInfo) {
229 intent.apply {
230 putExtra(ControlsFavoritingActivity.EXTRA_APP,
231 controlsListingController.get().getAppLabel(si.componentName))
232 putExtra(ControlsFavoritingActivity.EXTRA_STRUCTURE, si.structure)
233 putExtra(Intent.EXTRA_COMPONENT_NAME, si.componentName)
234 }
235 }
236
Matt Pietald2ea5312020-04-06 11:27:02 -0400237 private fun startProviderSelectorActivity(context: Context) {
238 val i = Intent(context, ControlsProviderSelectorActivity::class.java).apply {
239 addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
240 }
241 startActivity(context, i)
242 }
243
244 private fun startActivity(context: Context, intent: Intent) {
245 val closeDialog = Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)
246 context.sendBroadcast(closeDialog)
247 context.startActivity(intent)
Matt Pietal22231792020-01-23 09:51:09 -0500248 }
249
Matt Pietal638253a2020-03-02 09:10:43 -0500250 private fun showControlsView(items: List<SelectionItem>) {
251 parent.removeAllViews()
252 controlViewsById.clear()
253
Matt Pietal53a8bbd2020-03-05 16:10:34 -0500254 createListView()
255 createDropDown(items)
Matt Pietal5ebb7fe2020-03-20 12:58:54 -0400256 createMenu()
257 }
258
259 private fun createPopup(): ListPopupWindow {
260 return ListPopupWindow(
261 ContextThemeWrapper(context, R.style.Control_ListPopupWindow)).apply {
262 setWindowLayoutType(WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY)
263 setModal(true)
264 }
265 }
266
267 private fun createMenu() {
268 val items = arrayOf(
Matt Pietalcd757c82020-04-08 10:20:48 -0400269 context.resources.getString(R.string.controls_menu_add),
Fabian Kozynski8765d352020-04-06 21:16:02 -0400270 context.resources.getString(R.string.controls_menu_edit),
Matt Pietalcd757c82020-04-08 10:20:48 -0400271 "Reset"
Matt Pietal5ebb7fe2020-03-20 12:58:54 -0400272 )
273 var adapter = ArrayAdapter<String>(context, R.layout.controls_more_item, items)
274
275 val anchor = parent.requireViewById<ImageView>(R.id.controls_more)
276 anchor.setOnClickListener(object : View.OnClickListener {
277 override fun onClick(v: View) {
278 popup = createPopup().apply {
279 setAnchorView(anchor)
280 setAdapter(adapter)
281 setOnItemClickListener(object : AdapterView.OnItemClickListener {
282 override fun onItemClick(
283 parent: AdapterView<*>,
284 view: View,
285 pos: Int,
286 id: Long
287 ) {
288 when (pos) {
289 // 0: Add Control
Matt Pietald2ea5312020-04-06 11:27:02 -0400290 0 -> startFavoritingActivity(view.context, selectedStructure)
Fabian Kozynski8765d352020-04-06 21:16:02 -0400291 // 1: Edit controls
292 1 -> startEditingActivity(view.context, selectedStructure)
293 // 2: TEMPORARY for reset controls
294 2 -> showResetConfirmation()
Matt Pietal5ebb7fe2020-03-20 12:58:54 -0400295 else -> Log.w(ControlsUiController.TAG,
296 "Unsupported index ($pos) on 'more' menu selection")
297 }
298 dismiss()
299 }
300 })
301 // need to call show() first in order to construct the listView
302 show()
303 var width = 0
304 getListView()?.apply {
305 // width should be between [.5, .9] of screen
306 val parentWidth = this@ControlsUiControllerImpl.parent.getWidth()
307 val widthSpec = MeasureSpec.makeMeasureSpec(
308 (parentWidth * 0.9).toInt(), MeasureSpec.AT_MOST)
309 val child = adapter.getView(0, null, this)
310 child.measure(widthSpec, MeasureSpec.UNSPECIFIED)
311 width = Math.max(child.getMeasuredWidth(), (parentWidth * 0.5).toInt())
312 }
313 setWidth(width)
314 setHorizontalOffset(-width + anchor.getWidth())
315 show()
316 }
317 }
318 })
Matt Pietal53a8bbd2020-03-05 16:10:34 -0500319 }
Matt Pietal22231792020-01-23 09:51:09 -0500320
Matt Pietalcd757c82020-04-08 10:20:48 -0400321 private fun showResetConfirmation() {
322 val builder = AlertDialog.Builder(
323 context,
324 android.R.style.Theme_DeviceDefault_Dialog_Alert
325 ).apply {
326 setMessage("For testing purposes: Would you like to " +
327 "reset your favorited device controls?")
328 setPositiveButton(
329 android.R.string.ok,
330 DialogInterface.OnClickListener { dialog, _ ->
331 val userHandle = Process.myUserHandle()
332 val userContext = context.createContextAsUser(userHandle, 0)
333 val prefs = userContext.getSharedPreferences(
334 "controls_prefs", Context.MODE_PRIVATE)
335 prefs.edit().putBoolean("ControlsSeedingCompleted", false).apply()
336 controlsController.get().resetFavorites()
337 dialog.dismiss()
338 context.sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))
339 })
340 setNegativeButton(
341 android.R.string.cancel,
342 DialogInterface.OnClickListener {
343 dialog, _ -> dialog.cancel()
344 }
345 )
346 }
347 builder.create().apply {
348 getWindow().apply {
349 setType(WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY)
350 }
351 }.show()
352 }
353
Matt Pietal53a8bbd2020-03-05 16:10:34 -0500354 private fun createDropDown(items: List<SelectionItem>) {
355 items.forEach {
356 RenderInfo.registerComponentIcon(it.componentName, it.icon)
Matt Pietal22231792020-01-23 09:51:09 -0500357 }
Matt Pietale0661b62020-01-29 14:35:31 -0500358
Matt Pietal638253a2020-03-02 09:10:43 -0500359 val itemsByComponent = items.associateBy { it.componentName }
Matt Pietal53a8bbd2020-03-05 16:10:34 -0500360 val itemsWithStructure = allStructures.mapNotNull {
361 itemsByComponent.get(it.componentName)?.copy(structure = it.structure)
362 }
363 val selectionItem = findSelectionItem(selectedStructure, itemsWithStructure) ?: items[0]
Matt Pietal638253a2020-03-02 09:10:43 -0500364
Matt Pietal53a8bbd2020-03-05 16:10:34 -0500365 var adapter = ItemAdapter(context, R.layout.controls_spinner_item).apply {
Matt Pietal5ebb7fe2020-03-20 12:58:54 -0400366 addAll(itemsWithStructure)
Matt Pietal638253a2020-03-02 09:10:43 -0500367 }
368
369 /*
370 * Default spinner widget does not work with the window type required
371 * for this dialog. Use a textView with the ListPopupWindow to achieve
372 * a similar effect
373 */
Matt Pietal5ebb7fe2020-03-20 12:58:54 -0400374 val spinner = parent.requireViewById<TextView>(R.id.app_or_structure_spinner).apply {
Matt Pietal53a8bbd2020-03-05 16:10:34 -0500375 setText(selectionItem.getTitle())
Matt Pietal9ef947a2020-03-04 08:22:03 -0500376 // override the default color on the dropdown drawable
377 (getBackground() as LayerDrawable).getDrawable(1)
378 .setTint(context.resources.getColor(R.color.control_spinner_dropdown, null))
Matt Pietalaaccdbe2020-03-04 08:22:03 -0500379 }
380 parent.requireViewById<ImageView>(R.id.app_icon).apply {
Matt Pietal53a8bbd2020-03-05 16:10:34 -0500381 setImageDrawable(selectionItem.icon)
Matt Pietal638253a2020-03-02 09:10:43 -0500382 }
Matt Pietal5ebb7fe2020-03-20 12:58:54 -0400383
384 if (itemsWithStructure.size == 1) {
385 spinner.setBackground(null)
386 return
387 }
388
Matt Pietal638253a2020-03-02 09:10:43 -0500389 val anchor = parent.requireViewById<ViewGroup>(R.id.controls_header)
390 anchor.setOnClickListener(object : View.OnClickListener {
391 override fun onClick(v: View) {
Matt Pietal5ebb7fe2020-03-20 12:58:54 -0400392 popup = createPopup().apply {
Matt Pietal638253a2020-03-02 09:10:43 -0500393 setAnchorView(anchor)
394 setAdapter(adapter)
Matt Pietal638253a2020-03-02 09:10:43 -0500395 setOnItemClickListener(object : AdapterView.OnItemClickListener {
396 override fun onItemClick(
397 parent: AdapterView<*>,
398 view: View,
399 pos: Int,
400 id: Long
401 ) {
402 val listItem = parent.getItemAtPosition(pos) as SelectionItem
403 this@ControlsUiControllerImpl.switchAppOrStructure(listItem)
404 dismiss()
405 }
406 })
407 // need to call show() first in order to construct the listView
408 show()
409 getListView()?.apply {
410 setDividerHeight(
411 context.resources.getDimensionPixelSize(R.dimen.control_list_divider))
412 setDivider(
413 context.resources.getDrawable(R.drawable.controls_list_divider))
414 }
415 show()
416 }
417 }
418 })
Matt Pietal638253a2020-03-02 09:10:43 -0500419 }
420
Matt Pietal53a8bbd2020-03-05 16:10:34 -0500421 private fun createListView() {
422 val inflater = LayoutInflater.from(context)
423 inflater.inflate(R.layout.controls_with_favorites, parent, true)
424
Matt Pietal5f478c72020-04-01 15:53:54 -0400425 val maxColumns = findMaxColumns()
426
Matt Pietaldc78c842020-03-30 08:09:18 -0400427 // use flag only temporarily for testing
428 val usePanels = Settings.Secure.getInt(context.contentResolver, USE_PANELS, 0) == 1
429
Matt Pietal53a8bbd2020-03-05 16:10:34 -0500430 val listView = parent.requireViewById(R.id.global_actions_controls_list) as ViewGroup
431 var lastRow: ViewGroup = createRow(inflater, listView)
432 selectedStructure.controls.forEach {
Matt Pietal5f478c72020-04-01 15:53:54 -0400433 if (lastRow.getChildCount() == maxColumns) {
Matt Pietal53a8bbd2020-03-05 16:10:34 -0500434 lastRow = createRow(inflater, listView)
435 }
436 val baseLayout = inflater.inflate(
437 R.layout.controls_base_item, lastRow, false) as ViewGroup
438 lastRow.addView(baseLayout)
439 val cvh = ControlViewHolder(
440 baseLayout,
441 controlsController.get(),
442 uiExecutor,
Matt Pietaldc78c842020-03-30 08:09:18 -0400443 bgExecutor,
444 usePanels
Matt Pietal53a8bbd2020-03-05 16:10:34 -0500445 )
446 val key = ControlKey(selectedStructure.componentName, it.controlId)
447 cvh.bindData(controlsById.getValue(key))
448 controlViewsById.put(key, cvh)
449 }
450
Matt Pietal5f478c72020-04-01 15:53:54 -0400451 // add spacers if necessary to keep control size consistent
Lucas Dupin1ffaf8c2020-04-09 11:13:07 -0700452 val mod = selectedStructure.controls.size % maxColumns
453 var spacersToAdd = if (mod == 0) 0 else maxColumns - mod
Matt Pietal5f478c72020-04-01 15:53:54 -0400454 while (spacersToAdd > 0) {
Matt Pietal53a8bbd2020-03-05 16:10:34 -0500455 lastRow.addView(Space(context), LinearLayout.LayoutParams(0, 0, 1f))
Matt Pietal5f478c72020-04-01 15:53:54 -0400456 spacersToAdd--
Matt Pietal53a8bbd2020-03-05 16:10:34 -0500457 }
458 }
459
Matt Pietal5f478c72020-04-01 15:53:54 -0400460 /**
461 * For low-dp width screens that also employ an increased font scale, adjust the
462 * number of columns. This helps prevent text truncation on these devices.
463 */
464 private fun findMaxColumns(): Int {
465 val res = context.resources
466 var maxColumns = res.getInteger(R.integer.controls_max_columns)
467 val maxColumnsAdjustWidth =
468 res.getInteger(R.integer.controls_max_columns_adjust_below_width_dp)
469
470 val outValue = TypedValue()
471 res.getValue(R.dimen.controls_max_columns_adjust_above_font_scale, outValue, true)
472 val maxColumnsAdjustFontScale = outValue.getFloat()
473
474 val config = res.configuration
475 val isPortrait = config.orientation == Configuration.ORIENTATION_PORTRAIT
476 if (isPortrait &&
477 config.screenWidthDp != Configuration.SCREEN_WIDTH_DP_UNDEFINED &&
478 config.screenWidthDp <= maxColumnsAdjustWidth &&
479 config.fontScale >= maxColumnsAdjustFontScale) {
480 maxColumns--
481 }
482
483 return maxColumns
484 }
485
Matt Pietal638253a2020-03-02 09:10:43 -0500486 private fun loadPreference(structures: List<StructureInfo>): StructureInfo {
487 if (structures.isEmpty()) return EMPTY_STRUCTURE
488
489 val component = sharedPreferences.getString(PREF_COMPONENT, null)?.let {
490 ComponentName.unflattenFromString(it)
491 } ?: EMPTY_COMPONENT
492 val structure = sharedPreferences.getString(PREF_STRUCTURE, "")
493
494 return structures.firstOrNull {
495 component == it.componentName && structure == it.structure
496 } ?: structures.get(0)
497 }
498
499 private fun updatePreferences(si: StructureInfo) {
500 sharedPreferences.edit()
501 .putString(PREF_COMPONENT, si.componentName.flattenToString())
502 .putString(PREF_STRUCTURE, si.structure.toString())
503 .commit()
504 }
505
506 private fun switchAppOrStructure(item: SelectionItem) {
Matt Pietal5ebb7fe2020-03-20 12:58:54 -0400507 val newSelection = allStructures.first {
508 it.structure == item.structure && it.componentName == item.componentName
509 }
Matt Pietal638253a2020-03-02 09:10:43 -0500510
Matt Pietal5ebb7fe2020-03-20 12:58:54 -0400511 if (newSelection != selectedStructure) {
512 selectedStructure = newSelection
513 updatePreferences(selectedStructure)
514 controlsListingController.get().removeCallback(listingCallback)
515 reload(parent)
Matt Pietal638253a2020-03-02 09:10:43 -0500516 }
Matt Pietal22231792020-01-23 09:51:09 -0500517 }
518
519 override fun hide() {
Matt Pietal307b1ef2020-02-10 07:27:01 -0500520 Log.d(ControlsUiController.TAG, "hide()")
Matt Pietal61266442020-03-17 12:53:44 -0400521 hidden = true
Matt Pietal638253a2020-03-02 09:10:43 -0500522 popup?.dismiss()
Matt Pietalb7da66c2020-03-06 10:49:31 -0500523 activeDialog?.dismiss()
Matt Pietaldc78c842020-03-30 08:09:18 -0400524 ControlActionCoordinator.closeDialog()
Matt Pietal638253a2020-03-02 09:10:43 -0500525
Matt Pietal22231792020-01-23 09:51:09 -0500526 controlsController.get().unsubscribe()
Matt Pietal22231792020-01-23 09:51:09 -0500527
528 parent.removeAllViews()
529 controlsById.clear()
530 controlViewsById.clear()
Matt Pietal37cc1902020-02-06 10:25:56 -0500531 controlsListingController.get().removeCallback(listingCallback)
Matt Pietal53a8bbd2020-03-05 16:10:34 -0500532
533 RenderInfo.clearCache()
Matt Pietal22231792020-01-23 09:51:09 -0500534 }
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500535
536 override fun onRefreshState(componentName: ComponentName, controls: List<Control>) {
Matt Pietal307b1ef2020-02-10 07:27:01 -0500537 Log.d(ControlsUiController.TAG, "onRefreshState()")
Matt Pietal22231792020-01-23 09:51:09 -0500538 controls.forEach { c ->
Matt Pietal1aac43b2020-02-04 15:43:31 -0500539 controlsById.get(ControlKey(componentName, c.getControlId()))?.let {
Matt Pietal307b1ef2020-02-10 07:27:01 -0500540 Log.d(ControlsUiController.TAG, "onRefreshState() for id: " + c.getControlId())
Matt Pietal313f37d2020-02-24 11:27:22 -0500541 val cws = ControlWithState(componentName, it.ci, c)
Matt Pietal1aac43b2020-02-04 15:43:31 -0500542 val key = ControlKey(componentName, c.getControlId())
543 controlsById.put(key, cws)
Matt Pietal22231792020-01-23 09:51:09 -0500544
545 uiExecutor.execute {
Matt Pietal1aac43b2020-02-04 15:43:31 -0500546 controlViewsById.get(key)?.bindData(cws)
Matt Pietal22231792020-01-23 09:51:09 -0500547 }
548 }
549 }
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500550 }
551
552 override fun onActionResponse(componentName: ComponentName, controlId: String, response: Int) {
Matt Pietal1aac43b2020-02-04 15:43:31 -0500553 val key = ControlKey(componentName, controlId)
554 uiExecutor.execute {
Matt Pietalb7da66c2020-03-06 10:49:31 -0500555 controlViewsById.get(key)?.let { cvh ->
556 when (response) {
557 ControlAction.RESPONSE_CHALLENGE_PIN -> {
Matt Pietalea87e742020-03-26 13:28:02 -0400558 activeDialog = ChallengeDialogs.createPinDialog(cvh, false)
559 activeDialog?.show()
560 }
561 ControlAction.RESPONSE_CHALLENGE_PASSPHRASE -> {
562 activeDialog = ChallengeDialogs.createPinDialog(cvh, true)
563 activeDialog?.show()
564 }
565 ControlAction.RESPONSE_CHALLENGE_ACK -> {
566 activeDialog = ChallengeDialogs.createConfirmationDialog(cvh)
Matt Pietalb7da66c2020-03-06 10:49:31 -0500567 activeDialog?.show()
568 }
569 else -> cvh.actionResponse(response)
570 }
571 }
Matt Pietal1aac43b2020-02-04 15:43:31 -0500572 }
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500573 }
Matt Pietal22231792020-01-23 09:51:09 -0500574
Matt Pietal37cc1902020-02-06 10:25:56 -0500575 private fun createRow(inflater: LayoutInflater, listView: ViewGroup): ViewGroup {
576 val row = inflater.inflate(R.layout.controls_row, listView, false) as ViewGroup
577 listView.addView(row)
Matt Pietal22231792020-01-23 09:51:09 -0500578 return row
579 }
Matt Pietal53a8bbd2020-03-05 16:10:34 -0500580
581 private fun findSelectionItem(si: StructureInfo, items: List<SelectionItem>): SelectionItem? =
582 items.firstOrNull {
583 it.componentName == si.componentName && it.structure == si.structure
584 }
Matt Pietal22231792020-01-23 09:51:09 -0500585}
Matt Pietal638253a2020-03-02 09:10:43 -0500586
587private data class SelectionItem(
588 val appName: CharSequence,
589 val structure: CharSequence,
590 val icon: Drawable,
591 val componentName: ComponentName
592) {
593 fun getTitle() = if (structure.isEmpty()) { appName } else { structure }
594}
595
596private class ItemAdapter(
597 val parentContext: Context,
598 val resource: Int
599) : ArrayAdapter<SelectionItem>(parentContext, resource) {
600
601 val layoutInflater = LayoutInflater.from(context)
602
603 override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
604 val item = getItem(position)
605 val view = convertView ?: layoutInflater.inflate(resource, parent, false)
606 view.requireViewById<TextView>(R.id.controls_spinner_item).apply {
607 setText(item.getTitle())
608 }
609 view.requireViewById<ImageView>(R.id.app_icon).apply {
Matt Pietal638253a2020-03-02 09:10:43 -0500610 setImageDrawable(item.icon)
611 }
612 return view
613 }
Matt Pietal638253a2020-03-02 09:10:43 -0500614}