blob: 2adfb1bd6d4f0bf0160020fb44a5a784974a6199 [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
Lucas Dupind60b3322020-04-15 18:06:47 -0700212 override fun onFocusChanged(focusedControl: ControlWithState?) {
213 controlViewsById.forEach { key: ControlKey, viewHolder: ControlViewHolder ->
214 val state = controlsById.get(key) ?: return@forEach
215 val shouldBeDimmed = focusedControl != null && state != focusedControl
216 if (viewHolder.dimmed == shouldBeDimmed) {
217 return@forEach
218 }
219
220 uiExecutor.execute {
221 viewHolder.dimmed = shouldBeDimmed
222 }
223 }
224 }
225
Matt Pietald2ea5312020-04-06 11:27:02 -0400226 private fun startFavoritingActivity(context: Context, si: StructureInfo) {
Fabian Kozynski8765d352020-04-06 21:16:02 -0400227 startTargetedActivity(context, si, ControlsFavoritingActivity::class.java)
228 }
229
230 private fun startEditingActivity(context: Context, si: StructureInfo) {
231 startTargetedActivity(context, si, ControlsEditingActivity::class.java)
232 }
233
234 private fun startTargetedActivity(context: Context, si: StructureInfo, klazz: Class<*>) {
235 val i = Intent(context, klazz).apply {
Matt Pietald2ea5312020-04-06 11:27:02 -0400236 addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
Matt Pietal22231792020-01-23 09:51:09 -0500237 }
Fabian Kozynski8765d352020-04-06 21:16:02 -0400238 putIntentExtras(i, si)
Matt Pietald2ea5312020-04-06 11:27:02 -0400239 startActivity(context, i)
240 }
241
Fabian Kozynski8765d352020-04-06 21:16:02 -0400242 private fun putIntentExtras(intent: Intent, si: StructureInfo) {
243 intent.apply {
244 putExtra(ControlsFavoritingActivity.EXTRA_APP,
245 controlsListingController.get().getAppLabel(si.componentName))
246 putExtra(ControlsFavoritingActivity.EXTRA_STRUCTURE, si.structure)
247 putExtra(Intent.EXTRA_COMPONENT_NAME, si.componentName)
248 }
249 }
250
Matt Pietald2ea5312020-04-06 11:27:02 -0400251 private fun startProviderSelectorActivity(context: Context) {
252 val i = Intent(context, ControlsProviderSelectorActivity::class.java).apply {
253 addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
254 }
255 startActivity(context, i)
256 }
257
258 private fun startActivity(context: Context, intent: Intent) {
259 val closeDialog = Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)
260 context.sendBroadcast(closeDialog)
261 context.startActivity(intent)
Matt Pietal22231792020-01-23 09:51:09 -0500262 }
263
Matt Pietal638253a2020-03-02 09:10:43 -0500264 private fun showControlsView(items: List<SelectionItem>) {
265 parent.removeAllViews()
266 controlViewsById.clear()
267
Matt Pietal53a8bbd2020-03-05 16:10:34 -0500268 createListView()
269 createDropDown(items)
Matt Pietal5ebb7fe2020-03-20 12:58:54 -0400270 createMenu()
271 }
272
273 private fun createPopup(): ListPopupWindow {
274 return ListPopupWindow(
275 ContextThemeWrapper(context, R.style.Control_ListPopupWindow)).apply {
276 setWindowLayoutType(WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY)
277 setModal(true)
278 }
279 }
280
281 private fun createMenu() {
282 val items = arrayOf(
Matt Pietalcd757c82020-04-08 10:20:48 -0400283 context.resources.getString(R.string.controls_menu_add),
Fabian Kozynski8765d352020-04-06 21:16:02 -0400284 context.resources.getString(R.string.controls_menu_edit),
Matt Pietalcd757c82020-04-08 10:20:48 -0400285 "Reset"
Matt Pietal5ebb7fe2020-03-20 12:58:54 -0400286 )
287 var adapter = ArrayAdapter<String>(context, R.layout.controls_more_item, items)
288
289 val anchor = parent.requireViewById<ImageView>(R.id.controls_more)
290 anchor.setOnClickListener(object : View.OnClickListener {
291 override fun onClick(v: View) {
292 popup = createPopup().apply {
293 setAnchorView(anchor)
294 setAdapter(adapter)
295 setOnItemClickListener(object : AdapterView.OnItemClickListener {
296 override fun onItemClick(
297 parent: AdapterView<*>,
298 view: View,
299 pos: Int,
300 id: Long
301 ) {
302 when (pos) {
303 // 0: Add Control
Matt Pietald2ea5312020-04-06 11:27:02 -0400304 0 -> startFavoritingActivity(view.context, selectedStructure)
Fabian Kozynski8765d352020-04-06 21:16:02 -0400305 // 1: Edit controls
306 1 -> startEditingActivity(view.context, selectedStructure)
307 // 2: TEMPORARY for reset controls
308 2 -> showResetConfirmation()
Matt Pietal5ebb7fe2020-03-20 12:58:54 -0400309 else -> Log.w(ControlsUiController.TAG,
310 "Unsupported index ($pos) on 'more' menu selection")
311 }
312 dismiss()
313 }
314 })
315 // need to call show() first in order to construct the listView
316 show()
317 var width = 0
318 getListView()?.apply {
319 // width should be between [.5, .9] of screen
320 val parentWidth = this@ControlsUiControllerImpl.parent.getWidth()
321 val widthSpec = MeasureSpec.makeMeasureSpec(
322 (parentWidth * 0.9).toInt(), MeasureSpec.AT_MOST)
323 val child = adapter.getView(0, null, this)
324 child.measure(widthSpec, MeasureSpec.UNSPECIFIED)
325 width = Math.max(child.getMeasuredWidth(), (parentWidth * 0.5).toInt())
326 }
327 setWidth(width)
328 setHorizontalOffset(-width + anchor.getWidth())
329 show()
330 }
331 }
332 })
Matt Pietal53a8bbd2020-03-05 16:10:34 -0500333 }
Matt Pietal22231792020-01-23 09:51:09 -0500334
Matt Pietalcd757c82020-04-08 10:20:48 -0400335 private fun showResetConfirmation() {
336 val builder = AlertDialog.Builder(
337 context,
338 android.R.style.Theme_DeviceDefault_Dialog_Alert
339 ).apply {
340 setMessage("For testing purposes: Would you like to " +
341 "reset your favorited device controls?")
342 setPositiveButton(
343 android.R.string.ok,
344 DialogInterface.OnClickListener { dialog, _ ->
345 val userHandle = Process.myUserHandle()
346 val userContext = context.createContextAsUser(userHandle, 0)
347 val prefs = userContext.getSharedPreferences(
348 "controls_prefs", Context.MODE_PRIVATE)
349 prefs.edit().putBoolean("ControlsSeedingCompleted", false).apply()
350 controlsController.get().resetFavorites()
351 dialog.dismiss()
352 context.sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))
353 })
354 setNegativeButton(
355 android.R.string.cancel,
356 DialogInterface.OnClickListener {
357 dialog, _ -> dialog.cancel()
358 }
359 )
360 }
361 builder.create().apply {
362 getWindow().apply {
363 setType(WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY)
364 }
365 }.show()
366 }
367
Matt Pietal53a8bbd2020-03-05 16:10:34 -0500368 private fun createDropDown(items: List<SelectionItem>) {
369 items.forEach {
370 RenderInfo.registerComponentIcon(it.componentName, it.icon)
Matt Pietal22231792020-01-23 09:51:09 -0500371 }
Matt Pietale0661b62020-01-29 14:35:31 -0500372
Matt Pietal638253a2020-03-02 09:10:43 -0500373 val itemsByComponent = items.associateBy { it.componentName }
Matt Pietal53a8bbd2020-03-05 16:10:34 -0500374 val itemsWithStructure = allStructures.mapNotNull {
375 itemsByComponent.get(it.componentName)?.copy(structure = it.structure)
376 }
377 val selectionItem = findSelectionItem(selectedStructure, itemsWithStructure) ?: items[0]
Matt Pietal638253a2020-03-02 09:10:43 -0500378
Matt Pietal53a8bbd2020-03-05 16:10:34 -0500379 var adapter = ItemAdapter(context, R.layout.controls_spinner_item).apply {
Matt Pietal5ebb7fe2020-03-20 12:58:54 -0400380 addAll(itemsWithStructure)
Matt Pietal638253a2020-03-02 09:10:43 -0500381 }
382
383 /*
384 * Default spinner widget does not work with the window type required
385 * for this dialog. Use a textView with the ListPopupWindow to achieve
386 * a similar effect
387 */
Matt Pietal5ebb7fe2020-03-20 12:58:54 -0400388 val spinner = parent.requireViewById<TextView>(R.id.app_or_structure_spinner).apply {
Matt Pietal53a8bbd2020-03-05 16:10:34 -0500389 setText(selectionItem.getTitle())
Matt Pietal9ef947a2020-03-04 08:22:03 -0500390 // override the default color on the dropdown drawable
391 (getBackground() as LayerDrawable).getDrawable(1)
392 .setTint(context.resources.getColor(R.color.control_spinner_dropdown, null))
Matt Pietalaaccdbe2020-03-04 08:22:03 -0500393 }
394 parent.requireViewById<ImageView>(R.id.app_icon).apply {
Matt Pietal53a8bbd2020-03-05 16:10:34 -0500395 setImageDrawable(selectionItem.icon)
Matt Pietal638253a2020-03-02 09:10:43 -0500396 }
Matt Pietal5ebb7fe2020-03-20 12:58:54 -0400397
398 if (itemsWithStructure.size == 1) {
399 spinner.setBackground(null)
400 return
401 }
402
Matt Pietal638253a2020-03-02 09:10:43 -0500403 val anchor = parent.requireViewById<ViewGroup>(R.id.controls_header)
404 anchor.setOnClickListener(object : View.OnClickListener {
405 override fun onClick(v: View) {
Matt Pietal5ebb7fe2020-03-20 12:58:54 -0400406 popup = createPopup().apply {
Matt Pietal638253a2020-03-02 09:10:43 -0500407 setAnchorView(anchor)
408 setAdapter(adapter)
Matt Pietal638253a2020-03-02 09:10:43 -0500409 setOnItemClickListener(object : AdapterView.OnItemClickListener {
410 override fun onItemClick(
411 parent: AdapterView<*>,
412 view: View,
413 pos: Int,
414 id: Long
415 ) {
416 val listItem = parent.getItemAtPosition(pos) as SelectionItem
417 this@ControlsUiControllerImpl.switchAppOrStructure(listItem)
418 dismiss()
419 }
420 })
421 // need to call show() first in order to construct the listView
422 show()
423 getListView()?.apply {
424 setDividerHeight(
425 context.resources.getDimensionPixelSize(R.dimen.control_list_divider))
426 setDivider(
427 context.resources.getDrawable(R.drawable.controls_list_divider))
428 }
429 show()
430 }
431 }
432 })
Matt Pietal638253a2020-03-02 09:10:43 -0500433 }
434
Matt Pietal53a8bbd2020-03-05 16:10:34 -0500435 private fun createListView() {
436 val inflater = LayoutInflater.from(context)
437 inflater.inflate(R.layout.controls_with_favorites, parent, true)
438
Matt Pietal5f478c72020-04-01 15:53:54 -0400439 val maxColumns = findMaxColumns()
440
Matt Pietaldc78c842020-03-30 08:09:18 -0400441 // use flag only temporarily for testing
442 val usePanels = Settings.Secure.getInt(context.contentResolver, USE_PANELS, 0) == 1
443
Matt Pietal53a8bbd2020-03-05 16:10:34 -0500444 val listView = parent.requireViewById(R.id.global_actions_controls_list) as ViewGroup
445 var lastRow: ViewGroup = createRow(inflater, listView)
446 selectedStructure.controls.forEach {
Matt Pietal5f478c72020-04-01 15:53:54 -0400447 if (lastRow.getChildCount() == maxColumns) {
Matt Pietal53a8bbd2020-03-05 16:10:34 -0500448 lastRow = createRow(inflater, listView)
449 }
450 val baseLayout = inflater.inflate(
451 R.layout.controls_base_item, lastRow, false) as ViewGroup
452 lastRow.addView(baseLayout)
453 val cvh = ControlViewHolder(
454 baseLayout,
455 controlsController.get(),
456 uiExecutor,
Matt Pietaldc78c842020-03-30 08:09:18 -0400457 bgExecutor,
458 usePanels
Matt Pietal53a8bbd2020-03-05 16:10:34 -0500459 )
460 val key = ControlKey(selectedStructure.componentName, it.controlId)
461 cvh.bindData(controlsById.getValue(key))
462 controlViewsById.put(key, cvh)
463 }
464
Matt Pietal5f478c72020-04-01 15:53:54 -0400465 // add spacers if necessary to keep control size consistent
Lucas Dupin1ffaf8c2020-04-09 11:13:07 -0700466 val mod = selectedStructure.controls.size % maxColumns
467 var spacersToAdd = if (mod == 0) 0 else maxColumns - mod
Matt Pietal5f478c72020-04-01 15:53:54 -0400468 while (spacersToAdd > 0) {
Matt Pietal53a8bbd2020-03-05 16:10:34 -0500469 lastRow.addView(Space(context), LinearLayout.LayoutParams(0, 0, 1f))
Matt Pietal5f478c72020-04-01 15:53:54 -0400470 spacersToAdd--
Matt Pietal53a8bbd2020-03-05 16:10:34 -0500471 }
472 }
473
Matt Pietal5f478c72020-04-01 15:53:54 -0400474 /**
475 * For low-dp width screens that also employ an increased font scale, adjust the
476 * number of columns. This helps prevent text truncation on these devices.
477 */
478 private fun findMaxColumns(): Int {
479 val res = context.resources
480 var maxColumns = res.getInteger(R.integer.controls_max_columns)
481 val maxColumnsAdjustWidth =
482 res.getInteger(R.integer.controls_max_columns_adjust_below_width_dp)
483
484 val outValue = TypedValue()
485 res.getValue(R.dimen.controls_max_columns_adjust_above_font_scale, outValue, true)
486 val maxColumnsAdjustFontScale = outValue.getFloat()
487
488 val config = res.configuration
489 val isPortrait = config.orientation == Configuration.ORIENTATION_PORTRAIT
490 if (isPortrait &&
491 config.screenWidthDp != Configuration.SCREEN_WIDTH_DP_UNDEFINED &&
492 config.screenWidthDp <= maxColumnsAdjustWidth &&
493 config.fontScale >= maxColumnsAdjustFontScale) {
494 maxColumns--
495 }
496
497 return maxColumns
498 }
499
Matt Pietal638253a2020-03-02 09:10:43 -0500500 private fun loadPreference(structures: List<StructureInfo>): StructureInfo {
501 if (structures.isEmpty()) return EMPTY_STRUCTURE
502
503 val component = sharedPreferences.getString(PREF_COMPONENT, null)?.let {
504 ComponentName.unflattenFromString(it)
505 } ?: EMPTY_COMPONENT
506 val structure = sharedPreferences.getString(PREF_STRUCTURE, "")
507
508 return structures.firstOrNull {
509 component == it.componentName && structure == it.structure
510 } ?: structures.get(0)
511 }
512
513 private fun updatePreferences(si: StructureInfo) {
514 sharedPreferences.edit()
515 .putString(PREF_COMPONENT, si.componentName.flattenToString())
516 .putString(PREF_STRUCTURE, si.structure.toString())
517 .commit()
518 }
519
520 private fun switchAppOrStructure(item: SelectionItem) {
Matt Pietal5ebb7fe2020-03-20 12:58:54 -0400521 val newSelection = allStructures.first {
522 it.structure == item.structure && it.componentName == item.componentName
523 }
Matt Pietal638253a2020-03-02 09:10:43 -0500524
Matt Pietal5ebb7fe2020-03-20 12:58:54 -0400525 if (newSelection != selectedStructure) {
526 selectedStructure = newSelection
527 updatePreferences(selectedStructure)
528 controlsListingController.get().removeCallback(listingCallback)
529 reload(parent)
Matt Pietal638253a2020-03-02 09:10:43 -0500530 }
Matt Pietal22231792020-01-23 09:51:09 -0500531 }
532
533 override fun hide() {
Matt Pietal307b1ef2020-02-10 07:27:01 -0500534 Log.d(ControlsUiController.TAG, "hide()")
Matt Pietal61266442020-03-17 12:53:44 -0400535 hidden = true
Matt Pietal638253a2020-03-02 09:10:43 -0500536 popup?.dismiss()
Matt Pietalb7da66c2020-03-06 10:49:31 -0500537 activeDialog?.dismiss()
Matt Pietaldc78c842020-03-30 08:09:18 -0400538 ControlActionCoordinator.closeDialog()
Matt Pietal638253a2020-03-02 09:10:43 -0500539
Matt Pietal22231792020-01-23 09:51:09 -0500540 controlsController.get().unsubscribe()
Matt Pietal22231792020-01-23 09:51:09 -0500541
542 parent.removeAllViews()
543 controlsById.clear()
544 controlViewsById.clear()
Matt Pietal37cc1902020-02-06 10:25:56 -0500545 controlsListingController.get().removeCallback(listingCallback)
Matt Pietal53a8bbd2020-03-05 16:10:34 -0500546
547 RenderInfo.clearCache()
Matt Pietal22231792020-01-23 09:51:09 -0500548 }
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500549
550 override fun onRefreshState(componentName: ComponentName, controls: List<Control>) {
Matt Pietal307b1ef2020-02-10 07:27:01 -0500551 Log.d(ControlsUiController.TAG, "onRefreshState()")
Matt Pietal22231792020-01-23 09:51:09 -0500552 controls.forEach { c ->
Matt Pietal1aac43b2020-02-04 15:43:31 -0500553 controlsById.get(ControlKey(componentName, c.getControlId()))?.let {
Matt Pietal307b1ef2020-02-10 07:27:01 -0500554 Log.d(ControlsUiController.TAG, "onRefreshState() for id: " + c.getControlId())
Matt Pietal313f37d2020-02-24 11:27:22 -0500555 val cws = ControlWithState(componentName, it.ci, c)
Matt Pietal1aac43b2020-02-04 15:43:31 -0500556 val key = ControlKey(componentName, c.getControlId())
557 controlsById.put(key, cws)
Matt Pietal22231792020-01-23 09:51:09 -0500558
559 uiExecutor.execute {
Matt Pietal1aac43b2020-02-04 15:43:31 -0500560 controlViewsById.get(key)?.bindData(cws)
Matt Pietal22231792020-01-23 09:51:09 -0500561 }
562 }
563 }
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500564 }
565
566 override fun onActionResponse(componentName: ComponentName, controlId: String, response: Int) {
Matt Pietal1aac43b2020-02-04 15:43:31 -0500567 val key = ControlKey(componentName, controlId)
568 uiExecutor.execute {
Matt Pietalb7da66c2020-03-06 10:49:31 -0500569 controlViewsById.get(key)?.let { cvh ->
570 when (response) {
571 ControlAction.RESPONSE_CHALLENGE_PIN -> {
Matt Pietalea87e742020-03-26 13:28:02 -0400572 activeDialog = ChallengeDialogs.createPinDialog(cvh, false)
573 activeDialog?.show()
574 }
575 ControlAction.RESPONSE_CHALLENGE_PASSPHRASE -> {
576 activeDialog = ChallengeDialogs.createPinDialog(cvh, true)
577 activeDialog?.show()
578 }
579 ControlAction.RESPONSE_CHALLENGE_ACK -> {
580 activeDialog = ChallengeDialogs.createConfirmationDialog(cvh)
Matt Pietalb7da66c2020-03-06 10:49:31 -0500581 activeDialog?.show()
582 }
583 else -> cvh.actionResponse(response)
584 }
585 }
Matt Pietal1aac43b2020-02-04 15:43:31 -0500586 }
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500587 }
Matt Pietal22231792020-01-23 09:51:09 -0500588
Matt Pietal37cc1902020-02-06 10:25:56 -0500589 private fun createRow(inflater: LayoutInflater, listView: ViewGroup): ViewGroup {
590 val row = inflater.inflate(R.layout.controls_row, listView, false) as ViewGroup
591 listView.addView(row)
Matt Pietal22231792020-01-23 09:51:09 -0500592 return row
593 }
Matt Pietal53a8bbd2020-03-05 16:10:34 -0500594
595 private fun findSelectionItem(si: StructureInfo, items: List<SelectionItem>): SelectionItem? =
596 items.firstOrNull {
597 it.componentName == si.componentName && it.structure == si.structure
598 }
Matt Pietal22231792020-01-23 09:51:09 -0500599}
Matt Pietal638253a2020-03-02 09:10:43 -0500600
601private data class SelectionItem(
602 val appName: CharSequence,
603 val structure: CharSequence,
604 val icon: Drawable,
605 val componentName: ComponentName
606) {
607 fun getTitle() = if (structure.isEmpty()) { appName } else { structure }
608}
609
610private class ItemAdapter(
611 val parentContext: Context,
612 val resource: Int
613) : ArrayAdapter<SelectionItem>(parentContext, resource) {
614
615 val layoutInflater = LayoutInflater.from(context)
616
617 override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
618 val item = getItem(position)
619 val view = convertView ?: layoutInflater.inflate(resource, parent, false)
620 view.requireViewById<TextView>(R.id.controls_spinner_item).apply {
621 setText(item.getTitle())
622 }
623 view.requireViewById<ImageView>(R.id.app_icon).apply {
Matt Pietal638253a2020-03-02 09:10:43 -0500624 setImageDrawable(item.icon)
625 }
626 return view
627 }
Matt Pietal638253a2020-03-02 09:10:43 -0500628}