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.content.ComponentName 25 import android.content.Context 26 import android.content.Intent 27 import android.content.SharedPreferences 28 import android.content.res.Configuration 29 import android.graphics.drawable.Drawable 30 import android.graphics.drawable.LayerDrawable 31 import android.service.controls.Control 32 import android.util.Log 33 import android.util.TypedValue 34 import android.view.ContextThemeWrapper 35 import android.view.LayoutInflater 36 import android.view.View 37 import android.view.ViewGroup 38 import android.view.animation.AccelerateInterpolator 39 import android.view.animation.DecelerateInterpolator 40 import android.widget.AdapterView 41 import android.widget.ArrayAdapter 42 import android.widget.ImageView 43 import android.widget.LinearLayout 44 import android.widget.ListPopupWindow 45 import android.widget.Space 46 import android.widget.TextView 47 import com.android.systemui.R 48 import com.android.systemui.controls.ControlsMetricsLogger 49 import com.android.systemui.controls.ControlsServiceInfo 50 import com.android.systemui.controls.CustomIconCache 51 import com.android.systemui.controls.controller.ControlInfo 52 import com.android.systemui.controls.controller.ControlsController 53 import com.android.systemui.controls.controller.StructureInfo 54 import com.android.systemui.controls.management.ControlsEditingActivity 55 import com.android.systemui.controls.management.ControlsFavoritingActivity 56 import com.android.systemui.controls.management.ControlsListingController 57 import com.android.systemui.controls.management.ControlsProviderSelectorActivity 58 import com.android.systemui.dagger.SysUISingleton 59 import com.android.systemui.dagger.qualifiers.Background 60 import com.android.systemui.dagger.qualifiers.Main 61 import com.android.systemui.globalactions.GlobalActionsPopupMenu 62 import com.android.systemui.plugins.ActivityStarter 63 import com.android.systemui.statusbar.phone.ShadeController 64 import com.android.systemui.statusbar.policy.KeyguardStateController 65 import com.android.systemui.util.concurrency.DelayableExecutor 66 import dagger.Lazy 67 import java.text.Collator 68 import java.util.function.Consumer 69 import javax.inject.Inject 70 71 private data class ControlKey(val componentName: ComponentName, val controlId: String) 72 73 @SysUISingleton 74 class ControlsUiControllerImpl @Inject constructor ( 75 val controlsController: Lazy<ControlsController>, 76 val context: Context, 77 @Main val uiExecutor: DelayableExecutor, 78 @Background val bgExecutor: DelayableExecutor, 79 val controlsListingController: Lazy<ControlsListingController>, 80 @Main val sharedPreferences: SharedPreferences, 81 val controlActionCoordinator: ControlActionCoordinator, 82 private val activityStarter: ActivityStarter, 83 private val shadeController: ShadeController, 84 private val iconCache: CustomIconCache, 85 private val controlsMetricsLogger: ControlsMetricsLogger, 86 private val keyguardStateController: KeyguardStateController 87 ) : ControlsUiController { 88 89 companion object { 90 private const val PREF_COMPONENT = "controls_component" 91 private const val PREF_STRUCTURE = "controls_structure" 92 93 private const val FADE_IN_MILLIS = 200L 94 95 private val EMPTY_COMPONENT = ComponentName("", "") 96 private val EMPTY_STRUCTURE = StructureInfo( 97 EMPTY_COMPONENT, 98 "", 99 mutableListOf<ControlInfo>() 100 ) 101 } 102 103 private var selectedStructure: StructureInfo = EMPTY_STRUCTURE 104 private lateinit var allStructures: List<StructureInfo> 105 private val controlsById = mutableMapOf<ControlKey, ControlWithState>() 106 private val controlViewsById = mutableMapOf<ControlKey, ControlViewHolder>() 107 private lateinit var parent: ViewGroup 108 private lateinit var lastItems: List<SelectionItem> 109 private var popup: ListPopupWindow? = null 110 private var hidden = true 111 private lateinit var onDismiss: Runnable 112 private val popupThemedContext = ContextThemeWrapper(context, R.style.Control_ListPopupWindow) 113 private var retainCache = false 114 115 private val collator = Collator.getInstance(context.resources.configuration.locales[0]) 116 private val localeComparator = compareBy<SelectionItem, CharSequence>(collator) { 117 it.getTitle() 118 } 119 120 private val onSeedingComplete = Consumer<Boolean> { 121 accepted -> 122 if (accepted) { 123 selectedStructure = controlsController.get().getFavorites().maxByOrNull { 124 it.controls.size 125 } ?: EMPTY_STRUCTURE 126 updatePreferences(selectedStructure) 127 } 128 reload(parent) 129 } 130 131 private lateinit var activityContext: Context 132 private lateinit var listingCallback: ControlsListingController.ControlsListingCallback 133 134 private fun createCallback( 135 onResult: (List<SelectionItem>) -> Unit 136 ): ControlsListingController.ControlsListingCallback { 137 return object : ControlsListingController.ControlsListingCallback { 138 override fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>) { 139 val lastItems = serviceInfos.map { 140 val uid = it.serviceInfo.applicationInfo.uid 141 SelectionItem(it.loadLabel(), "", it.loadIcon(), it.componentName, uid) 142 } 143 uiExecutor.execute { 144 parent.removeAllViews() 145 if (lastItems.size > 0) { 146 onResult(lastItems) 147 } 148 } 149 } 150 } 151 } 152 153 override fun show( 154 parent: ViewGroup, 155 onDismiss: Runnable, 156 activityContext: Context 157 ) { 158 Log.d(ControlsUiController.TAG, "show()") 159 this.parent = parent 160 this.onDismiss = onDismiss 161 this.activityContext = activityContext 162 hidden = false 163 retainCache = false 164 165 controlActionCoordinator.activityContext = activityContext 166 167 allStructures = controlsController.get().getFavorites() 168 selectedStructure = getPreferredStructure(allStructures) 169 170 if (controlsController.get().addSeedingFavoritesCallback(onSeedingComplete)) { 171 listingCallback = createCallback(::showSeedingView) 172 } else if (selectedStructure.controls.isEmpty() && allStructures.size <= 1) { 173 // only show initial view if there are really no favorites across any structure 174 listingCallback = createCallback(::showInitialSetupView) 175 } else { 176 selectedStructure.controls.map { 177 ControlWithState(selectedStructure.componentName, it, null) 178 }.associateByTo(controlsById) { 179 ControlKey(selectedStructure.componentName, it.ci.controlId) 180 } 181 listingCallback = createCallback(::showControlsView) 182 controlsController.get().subscribeToFavorites(selectedStructure) 183 } 184 185 controlsListingController.get().addCallback(listingCallback) 186 } 187 188 private fun reload(parent: ViewGroup) { 189 if (hidden) return 190 191 controlsListingController.get().removeCallback(listingCallback) 192 controlsController.get().unsubscribe() 193 194 val fadeAnim = ObjectAnimator.ofFloat(parent, "alpha", 1.0f, 0.0f) 195 fadeAnim.setInterpolator(AccelerateInterpolator(1.0f)) 196 fadeAnim.setDuration(FADE_IN_MILLIS) 197 fadeAnim.addListener(object : AnimatorListenerAdapter() { 198 override fun onAnimationEnd(animation: Animator) { 199 controlViewsById.clear() 200 controlsById.clear() 201 202 show(parent, onDismiss, activityContext) 203 val showAnim = ObjectAnimator.ofFloat(parent, "alpha", 0.0f, 1.0f) 204 showAnim.setInterpolator(DecelerateInterpolator(1.0f)) 205 showAnim.setDuration(FADE_IN_MILLIS) 206 showAnim.start() 207 } 208 }) 209 fadeAnim.start() 210 } 211 212 private fun showSeedingView(items: List<SelectionItem>) { 213 val inflater = LayoutInflater.from(context) 214 inflater.inflate(R.layout.controls_no_favorites, parent, true) 215 val subtitle = parent.requireViewById<TextView>(R.id.controls_subtitle) 216 subtitle.setText(context.resources.getString(R.string.controls_seeding_in_progress)) 217 } 218 219 private fun showInitialSetupView(items: List<SelectionItem>) { 220 startProviderSelectorActivity() 221 onDismiss.run() 222 } 223 224 private fun startFavoritingActivity(si: StructureInfo) { 225 startTargetedActivity(si, ControlsFavoritingActivity::class.java) 226 } 227 228 private fun startEditingActivity(si: StructureInfo) { 229 startTargetedActivity(si, ControlsEditingActivity::class.java) 230 } 231 232 private fun startTargetedActivity(si: StructureInfo, klazz: Class<*>) { 233 val i = Intent(activityContext, klazz) 234 putIntentExtras(i, si) 235 startActivity(i) 236 237 retainCache = true 238 } 239 240 private fun putIntentExtras(intent: Intent, si: StructureInfo) { 241 intent.apply { 242 putExtra(ControlsFavoritingActivity.EXTRA_APP, 243 controlsListingController.get().getAppLabel(si.componentName)) 244 putExtra(ControlsFavoritingActivity.EXTRA_STRUCTURE, si.structure) 245 putExtra(Intent.EXTRA_COMPONENT_NAME, si.componentName) 246 } 247 } 248 249 private fun startProviderSelectorActivity() { 250 val i = Intent(activityContext, ControlsProviderSelectorActivity::class.java) 251 i.putExtra(ControlsProviderSelectorActivity.BACK_SHOULD_EXIT, true) 252 startActivity(i) 253 } 254 255 private fun startActivity(intent: Intent) { 256 // Force animations when transitioning from a dialog to an activity 257 intent.putExtra(ControlsUiController.EXTRA_ANIMATE, true) 258 259 if (keyguardStateController.isShowing()) { 260 activityStarter.postStartActivityDismissingKeyguard(intent, 0 /* delay */) 261 } else { 262 activityContext.startActivity( 263 intent, 264 ActivityOptions.makeSceneTransitionAnimation(activityContext as Activity).toBundle() 265 ) 266 } 267 } 268 269 private fun showControlsView(items: List<SelectionItem>) { 270 controlViewsById.clear() 271 272 val itemsByComponent = items.associateBy { it.componentName } 273 val itemsWithStructure = mutableListOf<SelectionItem>() 274 allStructures.mapNotNullTo(itemsWithStructure) { 275 itemsByComponent.get(it.componentName)?.copy(structure = it.structure) 276 } 277 itemsWithStructure.sortWith(localeComparator) 278 279 val selectionItem = findSelectionItem(selectedStructure, itemsWithStructure) ?: items[0] 280 281 controlsMetricsLogger.refreshBegin(selectionItem.uid, !keyguardStateController.isUnlocked()) 282 283 createListView(selectionItem) 284 createDropDown(itemsWithStructure, selectionItem) 285 createMenu() 286 } 287 288 private fun createMenu() { 289 val items = arrayOf( 290 context.resources.getString(R.string.controls_menu_add), 291 context.resources.getString(R.string.controls_menu_edit) 292 ) 293 var adapter = ArrayAdapter<String>(context, R.layout.controls_more_item, items) 294 295 val anchor = parent.requireViewById<ImageView>(R.id.controls_more) 296 anchor.setOnClickListener(object : View.OnClickListener { 297 override fun onClick(v: View) { 298 popup = GlobalActionsPopupMenu( 299 popupThemedContext, 300 false /* isDropDownMode */ 301 ).apply { 302 setAnchorView(anchor) 303 setAdapter(adapter) 304 setOnItemClickListener(object : AdapterView.OnItemClickListener { 305 override fun onItemClick( 306 parent: AdapterView<*>, 307 view: View, 308 pos: Int, 309 id: Long 310 ) { 311 when (pos) { 312 // 0: Add Control 313 0 -> startFavoritingActivity(selectedStructure) 314 // 1: Edit controls 315 1 -> startEditingActivity(selectedStructure) 316 } 317 dismiss() 318 } 319 }) 320 show() 321 } 322 } 323 }) 324 } 325 326 private fun createDropDown(items: List<SelectionItem>, selected: SelectionItem) { 327 items.forEach { 328 RenderInfo.registerComponentIcon(it.componentName, it.icon) 329 } 330 331 var adapter = ItemAdapter(context, R.layout.controls_spinner_item).apply { 332 addAll(items) 333 } 334 335 /* 336 * Default spinner widget does not work with the window type required 337 * for this dialog. Use a textView with the ListPopupWindow to achieve 338 * a similar effect 339 */ 340 val spinner = parent.requireViewById<TextView>(R.id.app_or_structure_spinner).apply { 341 setText(selected.getTitle()) 342 // override the default color on the dropdown drawable 343 (getBackground() as LayerDrawable).getDrawable(0) 344 .setTint(context.resources.getColor(R.color.control_spinner_dropdown, null)) 345 } 346 347 if (items.size == 1) { 348 spinner.setBackground(null) 349 return 350 } 351 352 val anchor = parent.requireViewById<ViewGroup>(R.id.controls_header) 353 anchor.setOnClickListener(object : View.OnClickListener { 354 override fun onClick(v: View) { 355 popup = GlobalActionsPopupMenu( 356 popupThemedContext, 357 true /* isDropDownMode */ 358 ).apply { 359 setAnchorView(anchor) 360 setAdapter(adapter) 361 362 setOnItemClickListener(object : AdapterView.OnItemClickListener { 363 override fun onItemClick( 364 parent: AdapterView<*>, 365 view: View, 366 pos: Int, 367 id: Long 368 ) { 369 val listItem = parent.getItemAtPosition(pos) as SelectionItem 370 this@ControlsUiControllerImpl.switchAppOrStructure(listItem) 371 dismiss() 372 } 373 }) 374 show() 375 } 376 } 377 }) 378 } 379 380 private fun createListView(selected: SelectionItem) { 381 val inflater = LayoutInflater.from(context) 382 inflater.inflate(R.layout.controls_with_favorites, parent, true) 383 384 parent.requireViewById<ImageView>(R.id.controls_close).apply { 385 setOnClickListener { _: View -> onDismiss.run() } 386 visibility = View.VISIBLE 387 } 388 389 val maxColumns = findMaxColumns() 390 391 val listView = parent.requireViewById(R.id.global_actions_controls_list) as ViewGroup 392 var lastRow: ViewGroup = createRow(inflater, listView) 393 selectedStructure.controls.forEach { 394 val key = ControlKey(selectedStructure.componentName, it.controlId) 395 controlsById.get(key)?.let { 396 if (lastRow.getChildCount() == maxColumns) { 397 lastRow = createRow(inflater, listView) 398 } 399 val baseLayout = inflater.inflate( 400 R.layout.controls_base_item, lastRow, false) as ViewGroup 401 lastRow.addView(baseLayout) 402 403 // Use ConstraintLayout in the future... for now, manually adjust margins 404 if (lastRow.getChildCount() == 1) { 405 val lp = baseLayout.getLayoutParams() as ViewGroup.MarginLayoutParams 406 lp.setMarginStart(0) 407 } 408 val cvh = ControlViewHolder( 409 baseLayout, 410 controlsController.get(), 411 uiExecutor, 412 bgExecutor, 413 controlActionCoordinator, 414 controlsMetricsLogger, 415 selected.uid 416 ) 417 cvh.bindData(it, false /* isLocked, will be ignored on initial load */) 418 controlViewsById.put(key, cvh) 419 } 420 } 421 422 // add spacers if necessary to keep control size consistent 423 val mod = selectedStructure.controls.size % maxColumns 424 var spacersToAdd = if (mod == 0) 0 else maxColumns - mod 425 val margin = context.resources.getDimensionPixelSize(R.dimen.control_spacing) 426 while (spacersToAdd > 0) { 427 val lp = LinearLayout.LayoutParams(0, 0, 1f).apply { 428 setMarginStart(margin) 429 } 430 lastRow.addView(Space(context), lp) 431 spacersToAdd-- 432 } 433 } 434 435 /** 436 * For low-dp width screens that also employ an increased font scale, adjust the 437 * number of columns. This helps prevent text truncation on these devices. 438 */ 439 private fun findMaxColumns(): Int { 440 val res = context.resources 441 var maxColumns = res.getInteger(R.integer.controls_max_columns) 442 val maxColumnsAdjustWidth = 443 res.getInteger(R.integer.controls_max_columns_adjust_below_width_dp) 444 445 val outValue = TypedValue() 446 res.getValue(R.dimen.controls_max_columns_adjust_above_font_scale, outValue, true) 447 val maxColumnsAdjustFontScale = outValue.getFloat() 448 449 val config = res.configuration 450 val isPortrait = config.orientation == Configuration.ORIENTATION_PORTRAIT 451 if (isPortrait && 452 config.screenWidthDp != Configuration.SCREEN_WIDTH_DP_UNDEFINED && 453 config.screenWidthDp <= maxColumnsAdjustWidth && 454 config.fontScale >= maxColumnsAdjustFontScale) { 455 maxColumns-- 456 } 457 458 return maxColumns 459 } 460 461 override fun getPreferredStructure(structures: List<StructureInfo>): StructureInfo { 462 if (structures.isEmpty()) return EMPTY_STRUCTURE 463 464 val component = sharedPreferences.getString(PREF_COMPONENT, null)?.let { 465 ComponentName.unflattenFromString(it) 466 } ?: EMPTY_COMPONENT 467 val structure = sharedPreferences.getString(PREF_STRUCTURE, "") 468 469 return structures.firstOrNull { 470 component == it.componentName && structure == it.structure 471 } ?: structures.get(0) 472 } 473 474 private fun updatePreferences(si: StructureInfo) { 475 if (si == EMPTY_STRUCTURE) return 476 sharedPreferences.edit() 477 .putString(PREF_COMPONENT, si.componentName.flattenToString()) 478 .putString(PREF_STRUCTURE, si.structure.toString()) 479 .commit() 480 } 481 482 private fun switchAppOrStructure(item: SelectionItem) { 483 val newSelection = allStructures.first { 484 it.structure == item.structure && it.componentName == item.componentName 485 } 486 487 if (newSelection != selectedStructure) { 488 selectedStructure = newSelection 489 updatePreferences(selectedStructure) 490 reload(parent) 491 } 492 } 493 494 override fun closeDialogs(immediately: Boolean) { 495 if (immediately) { 496 popup?.dismissImmediate() 497 } else { 498 popup?.dismiss() 499 } 500 popup = null 501 502 controlViewsById.forEach { 503 it.value.dismiss() 504 } 505 controlActionCoordinator.closeDialogs() 506 } 507 508 override fun hide() { 509 hidden = true 510 511 closeDialogs(true) 512 controlsController.get().unsubscribe() 513 514 parent.removeAllViews() 515 controlsById.clear() 516 controlViewsById.clear() 517 518 controlsListingController.get().removeCallback(listingCallback) 519 520 if (!retainCache) RenderInfo.clearCache() 521 } 522 523 override fun onRefreshState(componentName: ComponentName, controls: List<Control>) { 524 val isLocked = !keyguardStateController.isUnlocked() 525 controls.forEach { c -> 526 controlsById.get(ControlKey(componentName, c.getControlId()))?.let { 527 Log.d(ControlsUiController.TAG, "onRefreshState() for id: " + c.getControlId()) 528 iconCache.store(componentName, c.controlId, c.customIcon) 529 val cws = ControlWithState(componentName, it.ci, c) 530 val key = ControlKey(componentName, c.getControlId()) 531 controlsById.put(key, cws) 532 533 controlViewsById.get(key)?.let { 534 uiExecutor.execute { it.bindData(cws, isLocked) } 535 } 536 } 537 } 538 } 539 540 override fun onActionResponse(componentName: ComponentName, controlId: String, response: Int) { 541 val key = ControlKey(componentName, controlId) 542 uiExecutor.execute { 543 controlViewsById.get(key)?.actionResponse(response) 544 } 545 } 546 547 private fun createRow(inflater: LayoutInflater, listView: ViewGroup): ViewGroup { 548 val row = inflater.inflate(R.layout.controls_row, listView, false) as ViewGroup 549 listView.addView(row) 550 return row 551 } 552 553 private fun findSelectionItem(si: StructureInfo, items: List<SelectionItem>): SelectionItem? = 554 items.firstOrNull { 555 it.componentName == si.componentName && it.structure == si.structure 556 } 557 } 558 559 private data class SelectionItem( 560 val appName: CharSequence, 561 val structure: CharSequence, 562 val icon: Drawable, 563 val componentName: ComponentName, 564 val uid: Int 565 ) { 566 fun getTitle() = if (structure.isEmpty()) { appName } else { structure } 567 } 568 569 private class ItemAdapter( 570 val parentContext: Context, 571 val resource: Int 572 ) : ArrayAdapter<SelectionItem>(parentContext, resource) { 573 574 val layoutInflater = LayoutInflater.from(context) 575 576 override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { 577 val item = getItem(position) 578 val view = convertView ?: layoutInflater.inflate(resource, parent, false) 579 view.requireViewById<TextView>(R.id.controls_spinner_item).apply { 580 setText(item.getTitle()) 581 } 582 view.requireViewById<ImageView>(R.id.app_icon).apply { 583 setImageDrawable(item.icon) 584 } 585 return view 586 } 587 } 588