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