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.app.Activity 23 import android.app.ActivityOptions 24 import android.app.Dialog 25 import android.app.PendingIntent 26 import android.content.ComponentName 27 import android.content.Context 28 import android.content.Intent 29 import android.content.pm.PackageManager 30 import android.graphics.drawable.Drawable 31 import android.graphics.drawable.LayerDrawable 32 import android.os.Trace 33 import android.service.controls.Control 34 import android.service.controls.ControlsProviderService 35 import android.util.Log 36 import android.view.ContextThemeWrapper 37 import android.view.LayoutInflater 38 import android.view.View 39 import android.view.ViewGroup 40 import android.view.animation.AccelerateInterpolator 41 import android.view.animation.DecelerateInterpolator 42 import android.widget.AdapterView 43 import android.widget.ArrayAdapter 44 import android.widget.BaseAdapter 45 import android.widget.FrameLayout 46 import android.widget.ImageView 47 import android.widget.LinearLayout 48 import android.widget.ListPopupWindow 49 import android.widget.Space 50 import android.widget.TextView 51 import androidx.annotation.VisibleForTesting 52 import com.android.systemui.Dumpable 53 import com.android.systemui.R 54 import com.android.systemui.controls.ControlsMetricsLogger 55 import com.android.systemui.controls.ControlsServiceInfo 56 import com.android.systemui.controls.CustomIconCache 57 import com.android.systemui.controls.controller.ControlsController 58 import com.android.systemui.controls.controller.StructureInfo 59 import com.android.systemui.controls.controller.StructureInfo.Companion.EMPTY_COMPONENT 60 import com.android.systemui.controls.controller.StructureInfo.Companion.EMPTY_STRUCTURE 61 import com.android.systemui.controls.management.ControlAdapter 62 import com.android.systemui.controls.management.ControlsEditingActivity 63 import com.android.systemui.controls.management.ControlsFavoritingActivity 64 import com.android.systemui.controls.management.ControlsListingController 65 import com.android.systemui.controls.management.ControlsProviderSelectorActivity 66 import com.android.systemui.controls.panels.AuthorizedPanelsRepository 67 import com.android.systemui.controls.panels.SelectedComponentRepository 68 import com.android.systemui.controls.settings.ControlsSettingsRepository 69 import com.android.systemui.dagger.SysUISingleton 70 import com.android.systemui.dagger.qualifiers.Background 71 import com.android.systemui.dagger.qualifiers.Main 72 import com.android.systemui.dump.DumpManager 73 import com.android.systemui.flags.FeatureFlags 74 import com.android.systemui.flags.Flags 75 import com.android.systemui.globalactions.GlobalActionsPopupMenu 76 import com.android.systemui.plugins.ActivityStarter 77 import com.android.systemui.settings.UserTracker 78 import com.android.systemui.statusbar.policy.KeyguardStateController 79 import com.android.systemui.util.asIndenting 80 import com.android.systemui.util.concurrency.DelayableExecutor 81 import com.android.systemui.util.indentIfPossible 82 import com.android.wm.shell.TaskViewFactory 83 import dagger.Lazy 84 import java.io.PrintWriter 85 import java.text.Collator 86 import java.util.Optional 87 import java.util.function.Consumer 88 import javax.inject.Inject 89 90 private data class ControlKey(val componentName: ComponentName, val controlId: String) 91 92 @SysUISingleton 93 class ControlsUiControllerImpl @Inject constructor ( 94 val controlsController: Lazy<ControlsController>, 95 val context: Context, 96 private val packageManager: PackageManager, 97 @Main val uiExecutor: DelayableExecutor, 98 @Background val bgExecutor: DelayableExecutor, 99 val controlsListingController: Lazy<ControlsListingController>, 100 private val controlActionCoordinator: ControlActionCoordinator, 101 private val activityStarter: ActivityStarter, 102 private val iconCache: CustomIconCache, 103 private val controlsMetricsLogger: ControlsMetricsLogger, 104 private val keyguardStateController: KeyguardStateController, 105 private val userTracker: UserTracker, 106 private val taskViewFactory: Optional<TaskViewFactory>, 107 private val controlsSettingsRepository: ControlsSettingsRepository, 108 private val authorizedPanelsRepository: AuthorizedPanelsRepository, 109 private val selectedComponentRepository: SelectedComponentRepository, 110 private val featureFlags: FeatureFlags, 111 private val dialogsFactory: ControlsDialogsFactory, 112 dumpManager: DumpManager 113 ) : ControlsUiController, Dumpable { 114 115 companion object { 116 117 private const val FADE_IN_MILLIS = 200L 118 119 private const val OPEN_APP_ID = 0L 120 private const val ADD_CONTROLS_ID = 1L 121 private const val ADD_APP_ID = 2L 122 private const val EDIT_CONTROLS_ID = 3L 123 private const val REMOVE_APP_ID = 4L 124 } 125 126 private var selectedItem: SelectedItem = SelectedItem.EMPTY_SELECTION 127 private var selectionItem: SelectionItem? = null 128 private lateinit var allStructures: List<StructureInfo> 129 private val controlsById = mutableMapOf<ControlKey, ControlWithState>() 130 private val controlViewsById = mutableMapOf<ControlKey, ControlViewHolder>() 131 private lateinit var parent: ViewGroup 132 private var popup: ListPopupWindow? = null 133 private var hidden = true 134 private lateinit var onDismiss: Runnable 135 private val popupThemedContext = ContextThemeWrapper(context, R.style.Control_ListPopupWindow) 136 private var retainCache = false 137 private var lastSelections = emptyList<SelectionItem>() 138 139 private var taskViewController: PanelTaskViewController? = null 140 141 private val collator = Collator.getInstance(context.resources.configuration.locales[0]) 142 private val localeComparator = compareBy<SelectionItem, CharSequence>(collator) { 143 it.getTitle() 144 } 145 146 private var openAppIntent: Intent? = null 147 private var overflowMenuAdapter: BaseAdapter? = null 148 private var removeAppDialog: Dialog? = null 149 150 private val onSeedingComplete = Consumer<Boolean> { 151 accepted -> 152 if (accepted) { 153 selectedItem = controlsController.get().getFavorites().maxByOrNull { 154 it.controls.size 155 }?.let { 156 SelectedItem.StructureItem(it) 157 } ?: SelectedItem.EMPTY_SELECTION 158 updatePreferences(selectedItem) 159 } 160 reload(parent) 161 } 162 163 private lateinit var activityContext: Context 164 private lateinit var listingCallback: ControlsListingController.ControlsListingCallback 165 166 override val isShowing: Boolean 167 get() = !hidden 168 169 init { 170 dumpManager.registerDumpable(javaClass.name, this) 171 } 172 173 private fun createCallback( 174 onResult: (List<SelectionItem>) -> Unit 175 ): ControlsListingController.ControlsListingCallback { 176 return object : ControlsListingController.ControlsListingCallback { 177 override fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>) { 178 val authorizedPanels = authorizedPanelsRepository.getAuthorizedPanels() 179 val lastItems = serviceInfos.map { 180 val uid = it.serviceInfo.applicationInfo.uid 181 182 SelectionItem( 183 it.loadLabel(), 184 "", 185 it.loadIcon(), 186 it.componentName, 187 uid, 188 if (it.componentName.packageName in authorizedPanels) { 189 it.panelActivity 190 } else { 191 null 192 } 193 ) 194 } 195 uiExecutor.execute { 196 parent.removeAllViews() 197 if (lastItems.size > 0) { 198 onResult(lastItems) 199 } 200 } 201 } 202 } 203 } 204 205 override fun resolveActivity(): Class<*> { 206 val allStructures = controlsController.get().getFavorites() 207 val selected = getPreferredSelectedItem(allStructures) 208 val anyPanels = controlsListingController.get().getCurrentServices() 209 .any { it.panelActivity != null } 210 211 return if (controlsController.get().addSeedingFavoritesCallback(onSeedingComplete)) { 212 ControlsActivity::class.java 213 } else if (!selected.hasControls && allStructures.size <= 1 && !anyPanels) { 214 ControlsProviderSelectorActivity::class.java 215 } else { 216 ControlsActivity::class.java 217 } 218 } 219 220 override fun show( 221 parent: ViewGroup, 222 onDismiss: Runnable, 223 activityContext: Context 224 ) { 225 Log.d(ControlsUiController.TAG, "show()") 226 Trace.instant(Trace.TRACE_TAG_APP, "ControlsUiControllerImpl#show") 227 this.parent = parent 228 this.onDismiss = onDismiss 229 this.activityContext = activityContext 230 this.openAppIntent = null 231 this.overflowMenuAdapter = null 232 hidden = false 233 retainCache = false 234 selectionItem = null 235 236 controlActionCoordinator.activityContext = activityContext 237 238 allStructures = controlsController.get().getFavorites() 239 selectedItem = getPreferredSelectedItem(allStructures) 240 241 if (controlsController.get().addSeedingFavoritesCallback(onSeedingComplete)) { 242 listingCallback = createCallback(::showSeedingView) 243 } else if ( 244 selectedItem !is SelectedItem.PanelItem && 245 !selectedItem.hasControls && 246 allStructures.size <= 1 247 ) { 248 // only show initial view if there are really no favorites across any structure 249 listingCallback = createCallback(::initialView) 250 } else { 251 val selected = selectedItem 252 if (selected is SelectedItem.StructureItem) { 253 selected.structure.controls.map { 254 ControlWithState(selected.structure.componentName, it, null) 255 }.associateByTo(controlsById) { 256 ControlKey(selected.structure.componentName, it.ci.controlId) 257 } 258 controlsController.get().subscribeToFavorites(selected.structure) 259 } else { 260 controlsController.get().bindComponentForPanel(selected.componentName) 261 } 262 listingCallback = createCallback(::showControlsView) 263 } 264 265 controlsListingController.get().addCallback(listingCallback) 266 } 267 268 private fun initialView(items: List<SelectionItem>) { 269 if (items.any { it.isPanel }) { 270 // We have at least a panel, so we'll end up showing that. 271 showControlsView(items) 272 } else { 273 showInitialSetupView(items) 274 } 275 } 276 277 private fun reload(parent: ViewGroup, dismissTaskView: Boolean = true) { 278 if (hidden) return 279 280 controlsListingController.get().removeCallback(listingCallback) 281 controlsController.get().unsubscribe() 282 taskViewController?.dismiss() 283 taskViewController = null 284 285 val fadeAnim = ObjectAnimator.ofFloat(parent, "alpha", 1.0f, 0.0f) 286 fadeAnim.setInterpolator(AccelerateInterpolator(1.0f)) 287 fadeAnim.setDuration(FADE_IN_MILLIS) 288 fadeAnim.addListener(object : AnimatorListenerAdapter() { 289 override fun onAnimationEnd(animation: Animator) { 290 controlViewsById.clear() 291 controlsById.clear() 292 293 show(parent, onDismiss, activityContext) 294 val showAnim = ObjectAnimator.ofFloat(parent, "alpha", 0.0f, 1.0f) 295 showAnim.setInterpolator(DecelerateInterpolator(1.0f)) 296 showAnim.setDuration(FADE_IN_MILLIS) 297 showAnim.start() 298 } 299 }) 300 fadeAnim.start() 301 } 302 303 private fun showSeedingView(items: List<SelectionItem>) { 304 val inflater = LayoutInflater.from(context) 305 inflater.inflate(R.layout.controls_no_favorites, parent, true) 306 val subtitle = parent.requireViewById<TextView>(R.id.controls_subtitle) 307 subtitle.setText(context.resources.getString(R.string.controls_seeding_in_progress)) 308 } 309 310 private fun showInitialSetupView(items: List<SelectionItem>) { 311 startProviderSelectorActivity() 312 onDismiss.run() 313 } 314 315 private fun startFavoritingActivity(si: StructureInfo) { 316 startTargetedActivity(si, ControlsFavoritingActivity::class.java) 317 } 318 319 private fun startEditingActivity(si: StructureInfo) { 320 startTargetedActivity(si, ControlsEditingActivity::class.java) 321 } 322 323 private fun startDefaultActivity() { 324 openAppIntent?.let { 325 startActivity(it, animateExtra = false) 326 } 327 } 328 329 @VisibleForTesting 330 internal fun startRemovingApp(componentName: ComponentName, appName: CharSequence) { 331 removeAppDialog?.cancel() 332 removeAppDialog = dialogsFactory.createRemoveAppDialog(context, appName) { 333 if (!controlsController.get().removeFavorites(componentName)) { 334 return@createRemoveAppDialog 335 } 336 337 if (selectedComponentRepository.getSelectedComponent()?.componentName == 338 componentName) { 339 selectedComponentRepository.removeSelectedComponent() 340 } 341 342 val selectedItem = getPreferredSelectedItem(controlsController.get().getFavorites()) 343 if (selectedItem == SelectedItem.EMPTY_SELECTION) { 344 // User removed the last panel. In this case we start app selection flow and don't 345 // want to auto-add it again 346 selectedComponentRepository.setShouldAddDefaultComponent(false) 347 } 348 reload(parent) 349 }.apply { show() } 350 } 351 352 private fun startTargetedActivity(si: StructureInfo, klazz: Class<*>) { 353 val i = Intent(activityContext, klazz) 354 putIntentExtras(i, si) 355 startActivity(i) 356 357 retainCache = true 358 } 359 360 private fun putIntentExtras(intent: Intent, si: StructureInfo) { 361 intent.apply { 362 putExtra(ControlsFavoritingActivity.EXTRA_APP, 363 controlsListingController.get().getAppLabel(si.componentName)) 364 putExtra(ControlsFavoritingActivity.EXTRA_STRUCTURE, si.structure) 365 putExtra(Intent.EXTRA_COMPONENT_NAME, si.componentName) 366 } 367 } 368 369 private fun startProviderSelectorActivity() { 370 val i = Intent(activityContext, ControlsProviderSelectorActivity::class.java) 371 i.putExtra(ControlsProviderSelectorActivity.BACK_SHOULD_EXIT, true) 372 startActivity(i) 373 } 374 375 private fun startActivity(intent: Intent, animateExtra: Boolean = true) { 376 // Force animations when transitioning from a dialog to an activity 377 if (animateExtra) { 378 intent.putExtra(ControlsUiController.EXTRA_ANIMATE, true) 379 } 380 381 if (keyguardStateController.isShowing()) { 382 activityStarter.postStartActivityDismissingKeyguard(intent, 0 /* delay */) 383 } else { 384 activityContext.startActivity( 385 intent, 386 ActivityOptions.makeSceneTransitionAnimation(activityContext as Activity).toBundle() 387 ) 388 } 389 } 390 391 private fun showControlsView(items: List<SelectionItem>) { 392 controlViewsById.clear() 393 394 val (panels, structures) = items.partition { it.isPanel } 395 val panelComponents = panels.map { it.componentName }.toSet() 396 397 val itemsByComponent = structures.associateBy { it.componentName } 398 .filterNot { it.key in panelComponents } 399 val panelsAndStructures = mutableListOf<SelectionItem>() 400 allStructures.mapNotNullTo(panelsAndStructures) { 401 itemsByComponent.get(it.componentName)?.copy(structure = it.structure) 402 } 403 panelsAndStructures.addAll(panels) 404 405 panelsAndStructures.sortWith(localeComparator) 406 407 lastSelections = panelsAndStructures 408 409 val selectionItem = findSelectionItem(selectedItem, panelsAndStructures) 410 ?: if (panels.isNotEmpty()) { 411 // If we couldn't find a good selected item, but there's at least one panel, 412 // show a panel. 413 panels[0] 414 } else { 415 items[0] 416 } 417 maybeUpdateSelectedItem(selectionItem) 418 419 createControlsSpaceFrame() 420 421 if (taskViewFactory.isPresent && selectionItem.isPanel) { 422 createPanelView(selectionItem.panelComponentName!!) 423 } else if (!selectionItem.isPanel) { 424 controlsMetricsLogger 425 .refreshBegin(selectionItem.uid, !keyguardStateController.isUnlocked()) 426 createListView(selectionItem) 427 } else { 428 Log.w(ControlsUiController.TAG, "Not TaskViewFactory to display panel $selectionItem") 429 } 430 this.selectionItem = selectionItem 431 432 bgExecutor.execute { 433 val intent = Intent(Intent.ACTION_MAIN) 434 .addCategory(Intent.CATEGORY_LAUNCHER) 435 .setPackage(selectionItem.componentName.packageName) 436 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or 437 Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) 438 val intents = packageManager 439 .queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(0L)) 440 intents.firstOrNull { it.activityInfo.exported }?.let { resolved -> 441 intent.setPackage(null) 442 intent.setComponent(resolved.activityInfo.componentName) 443 openAppIntent = intent 444 parent.post { 445 // This will call show on the PopupWindow in the same thread, so make sure this 446 // happens in the view thread. 447 overflowMenuAdapter?.notifyDataSetChanged() 448 } 449 } 450 } 451 createDropDown(panelsAndStructures, selectionItem) 452 453 val currentApps = panelsAndStructures.map { it.componentName }.toSet() 454 val allApps = controlsListingController.get() 455 .getCurrentServices().map { it.componentName }.toSet() 456 createMenu( 457 selectionItem = selectionItem, 458 extraApps = (allApps - currentApps).isNotEmpty(), 459 ) 460 } 461 462 private fun createPanelView(componentName: ComponentName) { 463 val setting = controlsSettingsRepository 464 .allowActionOnTrivialControlsInLockscreen.value 465 val pendingIntent = PendingIntent.getActivityAsUser( 466 context, 467 0, 468 Intent() 469 .setComponent(componentName) 470 .putExtra( 471 ControlsProviderService.EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS, 472 setting 473 ), 474 PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, 475 null, 476 userTracker.userHandle 477 ) 478 479 parent.requireViewById<View>(R.id.controls_scroll_view).visibility = View.GONE 480 val container = parent.requireViewById<FrameLayout>(R.id.controls_panel) 481 container.visibility = View.VISIBLE 482 container.post { 483 taskViewFactory.get().create(activityContext, uiExecutor) { taskView -> 484 taskViewController = PanelTaskViewController( 485 activityContext, 486 uiExecutor, 487 pendingIntent, 488 taskView, 489 onDismiss::run 490 ).also { 491 container.addView(taskView) 492 it.launchTaskView() 493 } 494 } 495 } 496 } 497 498 private fun createMenu(selectionItem: SelectionItem, extraApps: Boolean) { 499 val isPanel = selectedItem is SelectedItem.PanelItem 500 val selectedStructure = (selectedItem as? SelectedItem.StructureItem)?.structure 501 ?: EMPTY_STRUCTURE 502 val newFlows = featureFlags.isEnabled(Flags.CONTROLS_MANAGEMENT_NEW_FLOWS) 503 504 val items = buildList { 505 add(OverflowMenuAdapter.MenuItem( 506 context.getText(R.string.controls_open_app), 507 OPEN_APP_ID 508 )) 509 if (newFlows || isPanel) { 510 if (extraApps) { 511 add(OverflowMenuAdapter.MenuItem( 512 context.getText(R.string.controls_menu_add_another_app), 513 ADD_APP_ID 514 )) 515 } 516 if (featureFlags.isEnabled(Flags.APP_PANELS_REMOVE_APPS_ALLOWED)) { 517 add(OverflowMenuAdapter.MenuItem( 518 context.getText(R.string.controls_menu_remove), 519 REMOVE_APP_ID, 520 )) 521 } 522 } else { 523 add(OverflowMenuAdapter.MenuItem( 524 context.getText(R.string.controls_menu_add), 525 ADD_CONTROLS_ID 526 )) 527 } 528 if (!isPanel) { 529 add(OverflowMenuAdapter.MenuItem( 530 context.getText(R.string.controls_menu_edit), 531 EDIT_CONTROLS_ID 532 )) 533 } 534 } 535 536 val adapter = OverflowMenuAdapter(context, R.layout.controls_more_item, items) { position -> 537 getItemId(position) != OPEN_APP_ID || openAppIntent != null 538 } 539 540 val anchor = parent.requireViewById<ImageView>(R.id.controls_more) 541 anchor.setOnClickListener(object : View.OnClickListener { 542 override fun onClick(v: View) { 543 popup = GlobalActionsPopupMenu( 544 popupThemedContext, 545 false /* isDropDownMode */ 546 ).apply { 547 setAnchorView(anchor) 548 setAdapter(adapter) 549 setOnItemClickListener(object : AdapterView.OnItemClickListener { 550 override fun onItemClick( 551 parent: AdapterView<*>, 552 view: View, 553 pos: Int, 554 id: Long 555 ) { 556 when (id) { 557 OPEN_APP_ID -> startDefaultActivity() 558 ADD_APP_ID -> startProviderSelectorActivity() 559 ADD_CONTROLS_ID -> startFavoritingActivity(selectedStructure) 560 EDIT_CONTROLS_ID -> startEditingActivity(selectedStructure) 561 REMOVE_APP_ID -> startRemovingApp( 562 selectionItem.componentName, selectionItem.appName 563 ) 564 } 565 dismiss() 566 } 567 }) 568 show() 569 listView?.post { listView?.requestAccessibilityFocus() } 570 } 571 } 572 }) 573 overflowMenuAdapter = adapter 574 } 575 576 private fun createDropDown(items: List<SelectionItem>, selected: SelectionItem) { 577 items.forEach { 578 RenderInfo.registerComponentIcon(it.componentName, it.icon) 579 } 580 581 val adapter = ItemAdapter(context, R.layout.controls_spinner_item).apply { 582 add(selected) 583 addAll(items 584 .filter { it !== selected } 585 .sortedBy { it.appName.toString() } 586 ) 587 } 588 589 val iconSize = context.resources 590 .getDimensionPixelSize(R.dimen.controls_header_app_icon_size) 591 592 /* 593 * Default spinner widget does not work with the window type required 594 * for this dialog. Use a textView with the ListPopupWindow to achieve 595 * a similar effect 596 */ 597 val spinner = parent.requireViewById<TextView>(R.id.app_or_structure_spinner).apply { 598 setText(selected.getTitle()) 599 // override the default color on the dropdown drawable 600 (getBackground() as LayerDrawable).getDrawable(0) 601 .setTint(context.resources.getColor(R.color.control_spinner_dropdown, null)) 602 selected.icon.setBounds(0, 0, iconSize, iconSize) 603 compoundDrawablePadding = (iconSize / 2.4f).toInt() 604 setCompoundDrawablesRelative(selected.icon, null, null, null) 605 } 606 607 val anchor = parent.requireViewById<ViewGroup>(R.id.controls_header) 608 if (items.size == 1) { 609 spinner.setBackground(null) 610 anchor.setOnClickListener(null) 611 anchor.isClickable = false 612 return 613 } else { 614 spinner.background = parent.context.resources 615 .getDrawable(R.drawable.control_spinner_background) 616 } 617 618 anchor.setOnClickListener(object : View.OnClickListener { 619 override fun onClick(v: View) { 620 popup = GlobalActionsPopupMenu( 621 popupThemedContext, 622 true /* isDropDownMode */ 623 ).apply { 624 setAnchorView(anchor) 625 setAdapter(adapter) 626 627 setOnItemClickListener(object : AdapterView.OnItemClickListener { 628 override fun onItemClick( 629 parent: AdapterView<*>, 630 view: View, 631 pos: Int, 632 id: Long 633 ) { 634 val listItem = parent.getItemAtPosition(pos) as SelectionItem 635 this@ControlsUiControllerImpl.switchAppOrStructure(listItem) 636 dismiss() 637 } 638 }) 639 show() 640 listView?.post { listView?.requestAccessibilityFocus() } 641 } 642 } 643 }) 644 } 645 646 private fun createControlsSpaceFrame() { 647 val inflater = LayoutInflater.from(activityContext) 648 inflater.inflate(R.layout.controls_with_favorites, parent, true) 649 650 parent.requireViewById<ImageView>(R.id.controls_close).apply { 651 setOnClickListener { _: View -> onDismiss.run() } 652 visibility = View.VISIBLE 653 } 654 } 655 656 private fun createListView(selected: SelectionItem) { 657 if (selectedItem !is SelectedItem.StructureItem) return 658 val selectedStructure = (selectedItem as SelectedItem.StructureItem).structure 659 val inflater = LayoutInflater.from(activityContext) 660 661 val maxColumns = ControlAdapter.findMaxColumns(activityContext.resources) 662 663 val listView = parent.requireViewById(R.id.global_actions_controls_list) as ViewGroup 664 listView.removeAllViews() 665 var lastRow: ViewGroup = createRow(inflater, listView) 666 selectedStructure.controls.forEach { 667 val key = ControlKey(selectedStructure.componentName, it.controlId) 668 controlsById.get(key)?.let { 669 if (lastRow.getChildCount() == maxColumns) { 670 lastRow = createRow(inflater, listView) 671 } 672 val baseLayout = inflater.inflate( 673 R.layout.controls_base_item, lastRow, false) as ViewGroup 674 lastRow.addView(baseLayout) 675 676 // Use ConstraintLayout in the future... for now, manually adjust margins 677 if (lastRow.getChildCount() == 1) { 678 val lp = baseLayout.getLayoutParams() as ViewGroup.MarginLayoutParams 679 lp.setMarginStart(0) 680 } 681 val cvh = ControlViewHolder( 682 baseLayout, 683 controlsController.get(), 684 uiExecutor, 685 bgExecutor, 686 controlActionCoordinator, 687 controlsMetricsLogger, 688 selected.uid 689 ) 690 cvh.bindData(it, false /* isLocked, will be ignored on initial load */) 691 controlViewsById.put(key, cvh) 692 } 693 } 694 695 // add spacers if necessary to keep control size consistent 696 val mod = selectedStructure.controls.size % maxColumns 697 var spacersToAdd = if (mod == 0) 0 else maxColumns - mod 698 val margin = context.resources.getDimensionPixelSize(R.dimen.control_spacing) 699 while (spacersToAdd > 0) { 700 val lp = LinearLayout.LayoutParams(0, 0, 1f).apply { 701 setMarginStart(margin) 702 } 703 lastRow.addView(Space(context), lp) 704 spacersToAdd-- 705 } 706 } 707 708 override fun getPreferredSelectedItem(structures: List<StructureInfo>): SelectedItem { 709 val preferredPanel = selectedComponentRepository.getSelectedComponent() 710 val component = preferredPanel?.componentName ?: EMPTY_COMPONENT 711 return if (preferredPanel?.isPanel == true) { 712 SelectedItem.PanelItem(preferredPanel.name, component) 713 } else { 714 if (structures.isEmpty()) return SelectedItem.EMPTY_SELECTION 715 SelectedItem.StructureItem(structures.firstOrNull { 716 component == it.componentName && preferredPanel?.name == it.structure 717 } ?: structures[0]) 718 } 719 } 720 721 private fun updatePreferences(selectedItem: SelectedItem) { 722 selectedComponentRepository.setSelectedComponent( 723 SelectedComponentRepository.SelectedComponent(selectedItem) 724 ) 725 } 726 727 private fun maybeUpdateSelectedItem(item: SelectionItem): Boolean { 728 val newSelection = if (item.isPanel) { 729 SelectedItem.PanelItem(item.appName, item.componentName) 730 } else { 731 SelectedItem.StructureItem(allStructures.firstOrNull { 732 it.structure == item.structure && it.componentName == item.componentName 733 } ?: EMPTY_STRUCTURE) 734 } 735 return if (newSelection != selectedItem ) { 736 selectedItem = newSelection 737 updatePreferences(selectedItem) 738 true 739 } else { 740 false 741 } 742 } 743 744 private fun switchAppOrStructure(item: SelectionItem) { 745 if (maybeUpdateSelectedItem(item)) { 746 reload(parent) 747 } 748 } 749 750 override fun closeDialogs(immediately: Boolean) { 751 if (immediately) { 752 popup?.dismissImmediate() 753 } else { 754 popup?.dismiss() 755 } 756 popup = null 757 758 controlViewsById.forEach { 759 it.value.dismiss() 760 } 761 controlActionCoordinator.closeDialogs() 762 removeAppDialog?.cancel() 763 } 764 765 override fun hide(parent: ViewGroup) { 766 // We need to check for the parent because it's possible that we have started showing in a 767 // different activity. In that case, make sure to only clear things associated with the 768 // passed parent 769 if (parent == this.parent) { 770 Log.d(ControlsUiController.TAG, "hide()") 771 hidden = true 772 773 closeDialogs(true) 774 controlsController.get().unsubscribe() 775 taskViewController?.dismiss() 776 taskViewController = null 777 778 controlsById.clear() 779 controlViewsById.clear() 780 781 controlsListingController.get().removeCallback(listingCallback) 782 783 if (!retainCache) RenderInfo.clearCache() 784 } 785 parent.removeAllViews() 786 } 787 788 override fun onRefreshState(componentName: ComponentName, controls: List<Control>) { 789 val isLocked = !keyguardStateController.isUnlocked() 790 controls.forEach { c -> 791 controlsById.get(ControlKey(componentName, c.getControlId()))?.let { 792 Log.d(ControlsUiController.TAG, "onRefreshState() for id: " + c.getControlId()) 793 iconCache.store(componentName, c.controlId, c.customIcon) 794 val cws = ControlWithState(componentName, it.ci, c) 795 val key = ControlKey(componentName, c.getControlId()) 796 controlsById.put(key, cws) 797 798 controlViewsById.get(key)?.let { 799 uiExecutor.execute { it.bindData(cws, isLocked) } 800 } 801 } 802 } 803 } 804 805 override fun onActionResponse(componentName: ComponentName, controlId: String, response: Int) { 806 val key = ControlKey(componentName, controlId) 807 uiExecutor.execute { 808 controlViewsById.get(key)?.actionResponse(response) 809 } 810 } 811 812 override fun onSizeChange() { 813 selectionItem?.let { 814 when (selectedItem) { 815 is SelectedItem.StructureItem -> createListView(it) 816 is SelectedItem.PanelItem -> taskViewController?.refreshBounds() ?: reload(parent) 817 } 818 } ?: reload(parent) 819 } 820 821 private fun createRow(inflater: LayoutInflater, listView: ViewGroup): ViewGroup { 822 val row = inflater.inflate(R.layout.controls_row, listView, false) as ViewGroup 823 listView.addView(row) 824 return row 825 } 826 827 private fun findSelectionItem(si: SelectedItem, items: List<SelectionItem>): SelectionItem? = 828 items.firstOrNull { it.matches(si) } 829 830 override fun dump(pw: PrintWriter, args: Array<out String>) { 831 pw.println("ControlsUiControllerImpl:") 832 pw.asIndenting().indentIfPossible { 833 println("hidden: $hidden") 834 println("selectedItem: $selectedItem") 835 println("lastSelections: $lastSelections") 836 println("setting: ${controlsSettingsRepository 837 .allowActionOnTrivialControlsInLockscreen.value}") 838 } 839 } 840 } 841 842 @VisibleForTesting 843 internal data class SelectionItem( 844 val appName: CharSequence, 845 val structure: CharSequence, 846 val icon: Drawable, 847 val componentName: ComponentName, 848 val uid: Int, 849 val panelComponentName: ComponentName? 850 ) { getTitlenull851 fun getTitle() = if (structure.isEmpty()) { appName } else { structure } 852 853 val isPanel: Boolean = panelComponentName != null 854 matchesnull855 fun matches(selectedItem: SelectedItem): Boolean { 856 if (componentName != selectedItem.componentName) { 857 // Not the same component so they are not the same. 858 return false 859 } 860 if (isPanel || selectedItem is SelectedItem.PanelItem) { 861 // As they have the same component, if [this.isPanel] then we may be migrating from 862 // device controls API into panel. Want this to match, even if the selectedItem is not 863 // a panel. We don't want to match on app name because that can change with locale. 864 return true 865 } 866 // Return true if we find a structure with the correct name 867 return structure == (selectedItem as SelectedItem.StructureItem).structure.structure 868 } 869 } 870 871 private class ItemAdapter( 872 val parentContext: Context, 873 val resource: Int 874 ) : ArrayAdapter<SelectionItem>(parentContext, resource) { 875 876 val layoutInflater = LayoutInflater.from(context) 877 getViewnull878 override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { 879 val item = getItem(position) 880 val view = convertView ?: layoutInflater.inflate(resource, parent, false) 881 view.requireViewById<TextView>(R.id.controls_spinner_item).apply { 882 setText(item.getTitle()) 883 } 884 view.requireViewById<ImageView>(R.id.app_icon).apply { 885 setImageDrawable(item.icon) 886 } 887 return view 888 } 889 } 890