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.service.controls.flags.Flags.homePanelDream 36 import android.util.Log 37 import android.view.ContextThemeWrapper 38 import android.view.Gravity 39 import android.view.LayoutInflater 40 import android.view.View 41 import android.view.ViewGroup 42 import android.view.animation.AccelerateInterpolator 43 import android.view.animation.DecelerateInterpolator 44 import android.widget.AdapterView 45 import android.widget.ArrayAdapter 46 import android.widget.BaseAdapter 47 import android.widget.FrameLayout 48 import android.widget.ImageView 49 import android.widget.LinearLayout 50 import android.widget.ListPopupWindow 51 import android.widget.Space 52 import android.widget.TextView 53 import androidx.annotation.VisibleForTesting 54 import com.android.systemui.Dumpable 55 import com.android.systemui.res.R 56 import com.android.systemui.controls.ControlsMetricsLogger 57 import com.android.systemui.controls.ControlsServiceInfo 58 import com.android.systemui.controls.CustomIconCache 59 import com.android.systemui.controls.controller.ControlsController 60 import com.android.systemui.controls.controller.StructureInfo 61 import com.android.systemui.controls.controller.StructureInfo.Companion.EMPTY_COMPONENT 62 import com.android.systemui.controls.controller.StructureInfo.Companion.EMPTY_STRUCTURE 63 import com.android.systemui.controls.management.ControlAdapter 64 import com.android.systemui.controls.management.ControlsEditingActivity 65 import com.android.systemui.controls.management.ControlsFavoritingActivity 66 import com.android.systemui.controls.management.ControlsListingController 67 import com.android.systemui.controls.management.ControlsProviderSelectorActivity 68 import com.android.systemui.controls.panels.AuthorizedPanelsRepository 69 import com.android.systemui.controls.panels.SelectedComponentRepository 70 import com.android.systemui.controls.settings.ControlsSettingsRepository 71 import com.android.systemui.dagger.SysUISingleton 72 import com.android.systemui.dagger.qualifiers.Background 73 import com.android.systemui.dagger.qualifiers.Main 74 import com.android.systemui.dump.DumpManager 75 import com.android.systemui.flags.FeatureFlags 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.withIncreasedIndent 82 import com.android.wm.shell.taskview.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(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?.removeTask() 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 activityStarter.dismissKeyguardThenExecute({ 332 showAppRemovalDialog(componentName, appName) 333 true 334 }, null, true) 335 } 336 337 private fun showAppRemovalDialog(componentName: ComponentName, appName: CharSequence) { 338 removeAppDialog?.cancel() 339 removeAppDialog = dialogsFactory.createRemoveAppDialog(context, appName) { shouldRemove -> 340 if (!shouldRemove || !controlsController.get().removeFavorites(componentName)) { 341 return@createRemoveAppDialog 342 } 343 344 if (selectedComponentRepository.getSelectedComponent()?.componentName == 345 componentName) { 346 selectedComponentRepository.removeSelectedComponent() 347 } 348 349 val selectedItem = getPreferredSelectedItem(controlsController.get().getFavorites()) 350 if (selectedItem == SelectedItem.EMPTY_SELECTION) { 351 // User removed the last panel. In this case we start app selection flow and don't 352 // want to auto-add it again 353 selectedComponentRepository.setShouldAddDefaultComponent(false) 354 } 355 reload(parent) 356 }.apply { show() } 357 } 358 359 private fun startTargetedActivity(si: StructureInfo, klazz: Class<*>) { 360 val i = Intent(activityContext, klazz) 361 putIntentExtras(i, si) 362 startActivity(i) 363 364 retainCache = true 365 } 366 367 private fun putIntentExtras(intent: Intent, si: StructureInfo) { 368 intent.apply { 369 putExtra(ControlsFavoritingActivity.EXTRA_APP, 370 controlsListingController.get().getAppLabel(si.componentName)) 371 putExtra(ControlsFavoritingActivity.EXTRA_STRUCTURE, si.structure) 372 putExtra(Intent.EXTRA_COMPONENT_NAME, si.componentName) 373 } 374 } 375 376 private fun startProviderSelectorActivity() { 377 val i = Intent(activityContext, ControlsProviderSelectorActivity::class.java) 378 i.putExtra(ControlsProviderSelectorActivity.BACK_SHOULD_EXIT, true) 379 startActivity(i) 380 } 381 382 private fun startActivity(intent: Intent, animateExtra: Boolean = true) { 383 // Force animations when transitioning from a dialog to an activity 384 if (animateExtra) { 385 intent.putExtra(ControlsUiController.EXTRA_ANIMATE, true) 386 } 387 388 if (keyguardStateController.isShowing()) { 389 activityStarter.postStartActivityDismissingKeyguard(intent, 0 /* delay */) 390 } else { 391 activityContext.startActivity( 392 intent, 393 ActivityOptions.makeSceneTransitionAnimation(activityContext as Activity).toBundle() 394 ) 395 } 396 } 397 398 private fun showControlsView(items: List<SelectionItem>) { 399 controlViewsById.clear() 400 401 val (panels, structures) = items.partition { it.isPanel } 402 val panelComponents = panels.map { it.componentName }.toSet() 403 404 val itemsByComponent = structures.associateBy { it.componentName } 405 .filterNot { it.key in panelComponents } 406 val panelsAndStructures = mutableListOf<SelectionItem>() 407 allStructures.mapNotNullTo(panelsAndStructures) { 408 itemsByComponent.get(it.componentName)?.copy(structure = it.structure) 409 } 410 panelsAndStructures.addAll(panels) 411 412 panelsAndStructures.sortWith(localeComparator) 413 414 lastSelections = panelsAndStructures 415 416 val selectionItem = findSelectionItem(selectedItem, panelsAndStructures) 417 ?: if (panels.isNotEmpty()) { 418 // If we couldn't find a good selected item, but there's at least one panel, 419 // show a panel. 420 panels[0] 421 } else { 422 items[0] 423 } 424 maybeUpdateSelectedItem(selectionItem) 425 426 createControlsSpaceFrame() 427 428 if (taskViewFactory.isPresent && selectionItem.isPanel) { 429 createPanelView(selectionItem.panelComponentName!!) 430 } else if (!selectionItem.isPanel) { 431 controlsMetricsLogger 432 .refreshBegin(selectionItem.uid, !keyguardStateController.isUnlocked()) 433 createListView(selectionItem) 434 } else { 435 Log.w(ControlsUiController.TAG, "Not TaskViewFactory to display panel $selectionItem") 436 } 437 this.selectionItem = selectionItem 438 439 bgExecutor.execute { 440 val intent = Intent(Intent.ACTION_MAIN) 441 .addCategory(Intent.CATEGORY_LAUNCHER) 442 .setPackage(selectionItem.componentName.packageName) 443 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or 444 Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) 445 val intents = packageManager 446 .queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(0L)) 447 intents.firstOrNull { it.activityInfo.exported }?.let { resolved -> 448 intent.setPackage(null) 449 intent.setComponent(resolved.activityInfo.componentName) 450 openAppIntent = intent 451 parent.post { 452 // This will call show on the PopupWindow in the same thread, so make sure this 453 // happens in the view thread. 454 overflowMenuAdapter?.notifyDataSetChanged() 455 } 456 } 457 } 458 createDropDown(panelsAndStructures, selectionItem) 459 460 val currentApps = panelsAndStructures.map { it.componentName }.toSet() 461 val allApps = controlsListingController.get() 462 .getCurrentServices().map { it.componentName }.toSet() 463 createMenu( 464 selectionItem = selectionItem, 465 extraApps = (allApps - currentApps).isNotEmpty(), 466 ) 467 } 468 469 private fun createPanelView(componentName: ComponentName) { 470 val setting = controlsSettingsRepository 471 .allowActionOnTrivialControlsInLockscreen.value 472 val pendingIntent = PendingIntent.getActivityAsUser( 473 context, 474 0, 475 Intent().apply { 476 component = componentName 477 putExtra( 478 ControlsProviderService.EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS, 479 setting 480 ) 481 if (homePanelDream()) { 482 putExtra(ControlsProviderService.EXTRA_CONTROLS_SURFACE, 483 ControlsProviderService.CONTROLS_SURFACE_ACTIVITY_PANEL) 484 } 485 }, 486 PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, 487 ActivityOptions.makeBasic() 488 .setPendingIntentCreatorBackgroundActivityStartMode( 489 ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED).toBundle(), 490 userTracker.userHandle 491 ) 492 493 parent.requireViewById<View>(R.id.controls_scroll_view).visibility = View.GONE 494 val container = parent.requireViewById<FrameLayout>(R.id.controls_panel) 495 container.visibility = View.VISIBLE 496 container.post { 497 taskViewFactory.get().create(activityContext, uiExecutor) { taskView -> 498 taskViewController = PanelTaskViewController( 499 activityContext, 500 uiExecutor, 501 pendingIntent, 502 taskView, 503 onDismiss::run 504 ).also { 505 container.addView(taskView) 506 it.launchTaskView() 507 } 508 } 509 } 510 } 511 512 private fun createMenu(selectionItem: SelectionItem, extraApps: Boolean) { 513 val isPanel = selectedItem is SelectedItem.PanelItem 514 val selectedStructure = (selectedItem as? SelectedItem.StructureItem)?.structure 515 ?: EMPTY_STRUCTURE 516 517 val items = buildList { 518 add(OverflowMenuAdapter.MenuItem( 519 context.getText(R.string.controls_open_app), 520 OPEN_APP_ID 521 )) 522 if (extraApps) { 523 add(OverflowMenuAdapter.MenuItem( 524 context.getText(R.string.controls_menu_add_another_app), 525 ADD_APP_ID 526 )) 527 } 528 add(OverflowMenuAdapter.MenuItem( 529 context.getText(R.string.controls_menu_remove), 530 REMOVE_APP_ID, 531 )) 532 if (!isPanel) { 533 add(OverflowMenuAdapter.MenuItem( 534 context.getText(R.string.controls_menu_edit), 535 EDIT_CONTROLS_ID 536 )) 537 } 538 } 539 540 val adapter = OverflowMenuAdapter(context, R.layout.controls_more_item, items) { position -> 541 getItemId(position) != OPEN_APP_ID || openAppIntent != null 542 } 543 544 val anchor = parent.requireViewById<ImageView>(R.id.controls_more) 545 anchor.setOnClickListener(object : View.OnClickListener { 546 override fun onClick(v: View) { 547 popup = ControlsPopupMenu(popupThemedContext).apply { 548 width = ViewGroup.LayoutParams.WRAP_CONTENT 549 anchorView = anchor 550 setDropDownGravity(Gravity.END) 551 setAdapter(adapter) 552 553 setOnItemClickListener(object : AdapterView.OnItemClickListener { 554 override fun onItemClick( 555 parent: AdapterView<*>, 556 view: View, 557 pos: Int, 558 id: Long 559 ) { 560 when (id) { 561 OPEN_APP_ID -> startDefaultActivity() 562 ADD_APP_ID -> startProviderSelectorActivity() 563 ADD_CONTROLS_ID -> startFavoritingActivity(selectedStructure) 564 EDIT_CONTROLS_ID -> startEditingActivity(selectedStructure) 565 REMOVE_APP_ID -> startRemovingApp( 566 selectionItem.componentName, selectionItem.appName 567 ) 568 } 569 dismiss() 570 } 571 }) 572 show() 573 listView?.post { listView?.requestAccessibilityFocus() } 574 } 575 } 576 }) 577 overflowMenuAdapter = adapter 578 } 579 580 private fun createDropDown(items: List<SelectionItem>, selected: SelectionItem) { 581 items.forEach { 582 RenderInfo.registerComponentIcon(it.componentName, it.icon) 583 } 584 585 val adapter = ItemAdapter(context, R.layout.controls_spinner_item).apply { 586 add(selected) 587 addAll(items 588 .filter { it !== selected } 589 .sortedBy { it.appName.toString() } 590 ) 591 } 592 593 val iconSize = context.resources 594 .getDimensionPixelSize(R.dimen.controls_header_app_icon_size) 595 596 /* 597 * Default spinner widget does not work with the window type required 598 * for this dialog. Use a textView with the ListPopupWindow to achieve 599 * a similar effect 600 */ 601 val spinner = parent.requireViewById<TextView>(R.id.app_or_structure_spinner).apply { 602 setText(selected.getTitle()) 603 // override the default color on the dropdown drawable 604 (getBackground() as LayerDrawable).getDrawable(0) 605 .setTint(context.resources.getColor(R.color.control_spinner_dropdown, null)) 606 selected.icon.setBounds(0, 0, iconSize, iconSize) 607 compoundDrawablePadding = (iconSize / 2.4f).toInt() 608 setCompoundDrawablesRelative(selected.icon, null, null, null) 609 } 610 611 val anchor = parent.requireViewById<View>(R.id.app_or_structure_spinner) 612 if (items.size == 1) { 613 spinner.setBackground(null) 614 anchor.setOnClickListener(null) 615 anchor.isClickable = false 616 return 617 } else { 618 spinner.background = parent.context.resources 619 .getDrawable(R.drawable.control_spinner_background) 620 } 621 622 anchor.setOnClickListener(object : View.OnClickListener { 623 override fun onClick(v: View) { 624 popup = ControlsPopupMenu(popupThemedContext).apply { 625 anchorView = anchor 626 width = ViewGroup.LayoutParams.MATCH_PARENT 627 setAdapter(adapter) 628 629 setOnItemClickListener(object : AdapterView.OnItemClickListener { 630 override fun onItemClick( 631 parent: AdapterView<*>, 632 view: View, 633 pos: Int, 634 id: Long 635 ) { 636 val listItem = parent.getItemAtPosition(pos) as SelectionItem 637 this@ControlsUiControllerImpl.switchAppOrStructure(listItem) 638 dismiss() 639 } 640 }) 641 show() 642 listView?.post { listView?.requestAccessibilityFocus() } 643 } 644 } 645 }) 646 } 647 648 private fun createControlsSpaceFrame() { 649 val inflater = LayoutInflater.from(activityContext) 650 inflater.inflate(R.layout.controls_with_favorites, parent, true) 651 652 parent.requireViewById<ImageView>(R.id.controls_close).apply { 653 setOnClickListener { _: View -> onDismiss.run() } 654 visibility = View.VISIBLE 655 } 656 } 657 658 private fun createListView(selected: SelectionItem) { 659 if (selectedItem !is SelectedItem.StructureItem) return 660 val selectedStructure = (selectedItem as SelectedItem.StructureItem).structure 661 val inflater = LayoutInflater.from(activityContext) 662 663 val maxColumns = ControlAdapter.findMaxColumns(activityContext.resources) 664 665 val listView = parent.requireViewById(R.id.controls_list) as ViewGroup 666 listView.removeAllViews() 667 var lastRow: ViewGroup = createRow(inflater, listView) 668 selectedStructure.controls.forEach { 669 val key = ControlKey(selectedStructure.componentName, it.controlId) 670 controlsById.get(key)?.let { 671 if (lastRow.getChildCount() == maxColumns) { 672 lastRow = createRow(inflater, listView) 673 } 674 val baseLayout = inflater.inflate( 675 R.layout.controls_base_item, lastRow, false) as ViewGroup 676 lastRow.addView(baseLayout) 677 678 // Use ConstraintLayout in the future... for now, manually adjust margins 679 if (lastRow.getChildCount() == 1) { 680 val lp = baseLayout.getLayoutParams() as ViewGroup.MarginLayoutParams 681 lp.setMarginStart(0) 682 } 683 val cvh = ControlViewHolder( 684 baseLayout, 685 controlsController.get(), 686 uiExecutor, 687 bgExecutor, 688 controlActionCoordinator, 689 controlsMetricsLogger, 690 selected.uid, 691 controlsController.get().currentUserId, 692 ) 693 cvh.bindData(it, false /* isLocked, will be ignored on initial load */) 694 controlViewsById.put(key, cvh) 695 } 696 } 697 698 // add spacers if necessary to keep control size consistent 699 val mod = selectedStructure.controls.size % maxColumns 700 var spacersToAdd = if (mod == 0) 0 else maxColumns - mod 701 val margin = context.resources.getDimensionPixelSize(R.dimen.control_spacing) 702 while (spacersToAdd > 0) { 703 val lp = LinearLayout.LayoutParams(0, 0, 1f).apply { 704 setMarginStart(margin) 705 } 706 lastRow.addView(Space(context), lp) 707 spacersToAdd-- 708 } 709 } 710 711 override fun getPreferredSelectedItem(structures: List<StructureInfo>): SelectedItem { 712 val preferredPanel = selectedComponentRepository.getSelectedComponent() 713 val component = preferredPanel?.componentName ?: EMPTY_COMPONENT 714 return if (preferredPanel?.isPanel == true) { 715 SelectedItem.PanelItem(preferredPanel.name, component) 716 } else { 717 if (structures.isEmpty()) return SelectedItem.EMPTY_SELECTION 718 SelectedItem.StructureItem(structures.firstOrNull { 719 component == it.componentName && preferredPanel?.name == it.structure 720 } ?: structures[0]) 721 } 722 } 723 724 private fun updatePreferences(selectedItem: SelectedItem) { 725 selectedComponentRepository.setSelectedComponent( 726 SelectedComponentRepository.SelectedComponent(selectedItem) 727 ) 728 } 729 730 private fun maybeUpdateSelectedItem(item: SelectionItem): Boolean { 731 val newSelection = if (item.isPanel) { 732 SelectedItem.PanelItem(item.appName, item.componentName) 733 } else { 734 SelectedItem.StructureItem(allStructures.firstOrNull { 735 it.structure == item.structure && it.componentName == item.componentName 736 } ?: EMPTY_STRUCTURE) 737 } 738 return if (newSelection != selectedItem ) { 739 selectedItem = newSelection 740 updatePreferences(selectedItem) 741 true 742 } else { 743 false 744 } 745 } 746 747 private fun switchAppOrStructure(item: SelectionItem) { 748 if (maybeUpdateSelectedItem(item)) { 749 reload(parent) 750 } 751 } 752 753 override fun closeDialogs(immediately: Boolean) { 754 if (immediately) { 755 popup?.dismissImmediate() 756 } else { 757 popup?.dismiss() 758 } 759 popup = null 760 761 controlViewsById.forEach { 762 it.value.dismiss() 763 } 764 controlActionCoordinator.closeDialogs() 765 removeAppDialog?.cancel() 766 } 767 768 override fun hide(parent: ViewGroup) { 769 // We need to check for the parent because it's possible that we have started showing in a 770 // different activity. In that case, make sure to only clear things associated with the 771 // passed parent 772 if (parent == this.parent) { 773 Log.d(ControlsUiController.TAG, "hide()") 774 hidden = true 775 776 closeDialogs(true) 777 controlsController.get().unsubscribe() 778 taskViewController?.removeTask() 779 taskViewController = null 780 781 controlsById.clear() 782 controlViewsById.clear() 783 784 controlsListingController.get().removeCallback(listingCallback) 785 786 if (!retainCache) RenderInfo.clearCache() 787 } 788 parent.removeAllViews() 789 } 790 791 override fun onRefreshState(componentName: ComponentName, controls: List<Control>) { 792 val isLocked = !keyguardStateController.isUnlocked() 793 controls.forEach { c -> 794 controlsById.get(ControlKey(componentName, c.getControlId()))?.let { 795 Log.d(ControlsUiController.TAG, "onRefreshState() for id: " + c.getControlId()) 796 iconCache.store(componentName, c.controlId, c.customIcon) 797 val cws = ControlWithState(componentName, it.ci, c) 798 val key = ControlKey(componentName, c.getControlId()) 799 controlsById.put(key, cws) 800 801 controlViewsById.get(key)?.let { 802 uiExecutor.execute { it.bindData(cws, isLocked) } 803 } 804 } 805 } 806 } 807 808 override fun onActionResponse(componentName: ComponentName, controlId: String, response: Int) { 809 val key = ControlKey(componentName, controlId) 810 uiExecutor.execute { 811 controlViewsById.get(key)?.actionResponse(response) 812 } 813 } 814 815 override fun onSizeChange() { 816 selectionItem?.let { 817 when (selectedItem) { 818 is SelectedItem.StructureItem -> createListView(it) 819 is SelectedItem.PanelItem -> taskViewController?.refreshBounds() ?: reload(parent) 820 } 821 } ?: reload(parent) 822 } 823 824 private fun createRow(inflater: LayoutInflater, listView: ViewGroup): ViewGroup { 825 val row = inflater.inflate(R.layout.controls_row, listView, false) as ViewGroup 826 listView.addView(row) 827 return row 828 } 829 830 private fun findSelectionItem(si: SelectedItem, items: List<SelectionItem>): SelectionItem? = 831 items.firstOrNull { it.matches(si) } 832 833 override fun dump(pw: PrintWriter, args: Array<out String>) = pw.asIndenting().run { 834 println("ControlsUiControllerImpl:") 835 withIncreasedIndent { 836 println("hidden: $hidden") 837 println("selectedItem: $selectedItem") 838 println("lastSelections: $lastSelections") 839 println("setting: ${controlsSettingsRepository 840 .allowActionOnTrivialControlsInLockscreen.value}") 841 } 842 } 843 } 844 845 @VisibleForTesting 846 internal data class SelectionItem( 847 val appName: CharSequence, 848 val structure: CharSequence, 849 val icon: Drawable, 850 val componentName: ComponentName, 851 val uid: Int, 852 val panelComponentName: ComponentName? 853 ) { getTitlenull854 fun getTitle() = if (structure.isEmpty()) { appName } else { structure } 855 856 val isPanel: Boolean = panelComponentName != null 857 matchesnull858 fun matches(selectedItem: SelectedItem): Boolean { 859 if (componentName != selectedItem.componentName) { 860 // Not the same component so they are not the same. 861 return false 862 } 863 if (isPanel || selectedItem is SelectedItem.PanelItem) { 864 // As they have the same component, if [this.isPanel] then we may be migrating from 865 // device controls API into panel. Want this to match, even if the selectedItem is not 866 // a panel. We don't want to match on app name because that can change with locale. 867 return true 868 } 869 // Return true if we find a structure with the correct name 870 return structure == (selectedItem as SelectedItem.StructureItem).structure.structure 871 } 872 } 873 874 private class ItemAdapter(parentContext: Context, val resource: Int) : 875 ArrayAdapter<SelectionItem>(parentContext, resource) { 876 877 private val layoutInflater = LayoutInflater.from(context)!! 878 getViewnull879 override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { 880 val item: SelectionItem = getItem(position)!! 881 val view = convertView ?: layoutInflater.inflate(resource, parent, false) 882 with(view.tag as? ViewHolder ?: ViewHolder(view).also { view.tag = it }) { 883 titleView.text = item.getTitle() 884 iconView.setImageDrawable(item.icon) 885 } 886 return view 887 } 888 889 private class ViewHolder(itemView: View) { 890 891 val titleView: TextView = itemView.requireViewById(R.id.controls_spinner_item) 892 val iconView: ImageView = itemView.requireViewById(R.id.app_icon) 893 } 894 } 895