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