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.management 18 19 import android.animation.Animator 20 import android.animation.AnimatorListenerAdapter 21 import android.app.Activity 22 import android.app.ActivityOptions 23 import android.content.ComponentName 24 import android.content.Context 25 import android.content.Intent 26 import android.content.res.Configuration 27 import android.os.Bundle 28 import android.os.Process.INVALID_UID 29 import android.text.TextUtils 30 import android.util.Log 31 import android.view.Gravity 32 import android.view.View 33 import android.view.ViewGroup 34 import android.view.ViewStub 35 import android.widget.Button 36 import android.widget.FrameLayout 37 import android.widget.TextView 38 import android.window.OnBackInvokedCallback 39 import android.window.OnBackInvokedDispatcher 40 import androidx.activity.ComponentActivity 41 import androidx.annotation.VisibleForTesting 42 import androidx.viewpager2.widget.ViewPager2 43 import com.android.systemui.Prefs 44 import com.android.systemui.controls.TooltipManager 45 import com.android.systemui.controls.controller.ControlsControllerImpl 46 import com.android.systemui.controls.controller.StructureInfo 47 import com.android.systemui.controls.ui.ControlsActivity 48 import com.android.systemui.dagger.qualifiers.Main 49 import com.android.systemui.res.R 50 import com.android.systemui.settings.UserTracker 51 import com.android.systemui.utils.SafeIconLoader 52 import java.text.Collator 53 import java.util.concurrent.Executor 54 import javax.inject.Inject 55 56 open class ControlsFavoritingActivity 57 @Inject 58 constructor( 59 @Main private val executor: Executor, 60 private val controller: ControlsControllerImpl, 61 private val userTracker: UserTracker, 62 private val safeIconLoaderFactory: SafeIconLoader.Factory, 63 private val controlsListingController: ControlsListingController, 64 ) : ComponentActivity(), ControlsManagementActivity { 65 66 companion object { 67 private const val DEBUG = false 68 private const val TAG = "ControlsFavoritingActivity" 69 70 // If provided and no structure is available, use as the title 71 const val EXTRA_APP = "extra_app_label" 72 73 // If provided, show this structure page first 74 const val EXTRA_STRUCTURE = "extra_structure" 75 const val EXTRA_SINGLE_STRUCTURE = "extra_single_structure" 76 const val EXTRA_SOURCE = "extra_source" 77 const val EXTRA_SOURCE_UNDEFINED: Byte = 0 78 const val EXTRA_SOURCE_VALUE_FROM_PROVIDER_SELECTOR: Byte = 1 79 const val EXTRA_SOURCE_VALUE_FROM_EDITING: Byte = 2 80 private const val TOOLTIP_PREFS_KEY = Prefs.Key.CONTROLS_STRUCTURE_SWIPE_TOOLTIP_COUNT 81 private const val TOOLTIP_MAX_SHOWN = 2 82 } 83 84 override val activity: Activity 85 get() = this 86 87 private var component: ComponentName? = null 88 private var appName: CharSequence? = null 89 private var structureExtra: CharSequence? = null 90 private var openSource = EXTRA_SOURCE_UNDEFINED 91 92 private lateinit var structurePager: ViewPager2 93 private lateinit var statusText: TextView 94 private lateinit var titleView: TextView 95 private lateinit var subtitleView: TextView 96 private lateinit var pageIndicator: ManagementPageIndicator 97 private var mTooltipManager: TooltipManager? = null 98 private lateinit var doneButton: View 99 private lateinit var rearrangeButton: Button 100 private var listOfStructures = emptyList<StructureContainer>() 101 102 private lateinit var comparator: Comparator<StructureContainer> 103 private var cancelLoadRunnable: Runnable? = null 104 private var isPagerLoaded = false 105 106 private val fromProviderSelector: Boolean 107 get() = openSource == EXTRA_SOURCE_VALUE_FROM_PROVIDER_SELECTOR 108 109 private val fromEditing: Boolean 110 get() = openSource == EXTRA_SOURCE_VALUE_FROM_EDITING 111 112 private val userTrackerCallback: UserTracker.Callback = 113 object : UserTracker.Callback { 114 private val startingUser = controller.currentUserId 115 116 override fun onUserChanged(newUser: Int, userContext: Context) { 117 if (newUser != startingUser) { 118 userTracker.removeCallback(this) 119 finish() 120 } 121 } 122 } 123 124 private val mOnBackInvokedCallback = OnBackInvokedCallback { 125 if (DEBUG) { 126 Log.d(TAG, "Predictive Back dispatcher called mOnBackInvokedCallback") 127 } 128 onBackPressed() 129 } 130 131 override fun onBackPressed() { 132 if (fromEditing) { 133 animateExitAndFinish() 134 } 135 if (!fromProviderSelector) { 136 openControlsOrigin() 137 } 138 animateExitAndFinish() 139 } 140 141 override fun onCreate(savedInstanceState: Bundle?) { 142 super.onCreate(savedInstanceState) 143 144 val collator = Collator.getInstance(resources.configuration.locales[0]) 145 comparator = compareBy(collator) { it.structureName } 146 appName = intent.getCharSequenceExtra(EXTRA_APP) 147 structureExtra = intent.getCharSequenceExtra(EXTRA_STRUCTURE) 148 component = intent.getParcelableExtra<ComponentName>(Intent.EXTRA_COMPONENT_NAME) 149 openSource = intent.getByteExtra(EXTRA_SOURCE, EXTRA_SOURCE_UNDEFINED) 150 151 bindViews() 152 } 153 154 private val controlsModelCallback = 155 object : ControlsModel.ControlsModelCallback { 156 override fun onFirstChange() { 157 doneButton.isEnabled = true 158 } 159 160 override fun onChange() { 161 val structure: StructureContainer = listOfStructures[structurePager.currentItem] 162 rearrangeButton.isEnabled = structure.model.favorites.isNotEmpty() 163 } 164 } 165 166 private fun loadControls() { 167 component?.let { componentName -> 168 statusText.text = resources.getText(com.android.internal.R.string.loading) 169 val emptyZoneString = resources.getText(R.string.controls_favorite_other_zone_header) 170 controller.loadForComponent( 171 componentName, 172 { data -> 173 val allControls = data.allControls 174 val favoriteKeys = data.favoritesIds 175 val error = data.errorOnLoad 176 val controlsByStructure = allControls.groupBy { it.control.structure ?: "" } 177 listOfStructures = 178 controlsByStructure 179 .map { 180 StructureContainer( 181 it.key, 182 AllModel( 183 it.value, 184 favoriteKeys, 185 emptyZoneString, 186 controlsModelCallback, 187 ), 188 ) 189 } 190 .sortedWith(comparator) 191 192 val structureIndex = 193 listOfStructures 194 .indexOfFirst { sc -> sc.structureName == structureExtra } 195 .let { if (it == -1) 0 else it } 196 197 // If we were requested to show a single structure, set the list to just that 198 // one 199 if (intent.getBooleanExtra(EXTRA_SINGLE_STRUCTURE, false)) { 200 listOfStructures = listOf(listOfStructures[structureIndex]) 201 } 202 203 val uid = 204 controlsListingController 205 .getCurrentServices() 206 .firstOrNull { it.componentName == componentName } 207 ?.serviceInfo 208 ?.applicationInfo 209 ?.uid ?: INVALID_UID 210 val packageName = componentName.packageName 211 val safeIconLoader = 212 safeIconLoaderFactory.create(uid, packageName, userTracker.userId) 213 214 executor.execute { 215 structurePager.adapter = 216 StructureAdapter(listOfStructures, userTracker.userId, safeIconLoader) 217 structurePager.setCurrentItem(structureIndex) 218 if (error) { 219 statusText.text = 220 resources.getString( 221 R.string.controls_favorite_load_error, 222 appName ?: "", 223 ) 224 subtitleView.visibility = View.GONE 225 } else if (listOfStructures.isEmpty()) { 226 statusText.text = 227 resources.getString(R.string.controls_favorite_load_none) 228 subtitleView.visibility = View.GONE 229 } else { 230 statusText.visibility = View.GONE 231 232 pageIndicator.setNumPages(listOfStructures.size) 233 pageIndicator.setLocation(0f) 234 pageIndicator.visibility = 235 if (listOfStructures.size > 1) View.VISIBLE else View.INVISIBLE 236 237 ControlsAnimations.enterAnimation(pageIndicator) 238 .apply { 239 addListener( 240 object : AnimatorListenerAdapter() { 241 override fun onAnimationEnd(animation: Animator) { 242 // Position the tooltip if necessary after 243 // animations are complete 244 // so we can get the position on screen. The tooltip 245 // is not 246 // rooted in the layout root. 247 if ( 248 pageIndicator.visibility == View.VISIBLE && 249 mTooltipManager != null 250 ) { 251 val p = IntArray(2) 252 pageIndicator.getLocationOnScreen(p) 253 val x = p[0] + pageIndicator.width / 2 254 val y = p[1] + pageIndicator.height 255 mTooltipManager?.show( 256 R.string.controls_structure_tooltip, 257 x, 258 y, 259 ) 260 } 261 } 262 } 263 ) 264 } 265 .start() 266 ControlsAnimations.enterAnimation(structurePager).start() 267 } 268 } 269 }, 270 { runnable -> cancelLoadRunnable = runnable }, 271 ) 272 } 273 } 274 275 private fun setUpPager() { 276 structurePager.alpha = 0.0f 277 pageIndicator.alpha = 0.0f 278 val uid = 279 controlsListingController 280 .getCurrentServices() 281 .firstOrNull { it.componentName == component } 282 ?.serviceInfo 283 ?.applicationInfo 284 ?.uid ?: INVALID_UID 285 val packageName = componentName?.packageName ?: "" 286 val safeIconLoader = safeIconLoaderFactory.create(uid, packageName, userTracker.userId) 287 288 structurePager.apply { 289 adapter = StructureAdapter(emptyList(), userTracker.userId, safeIconLoader) 290 registerOnPageChangeCallback( 291 object : ViewPager2.OnPageChangeCallback() { 292 override fun onPageSelected(position: Int) { 293 super.onPageSelected(position) 294 val name = listOfStructures[position].structureName 295 val title = if (!TextUtils.isEmpty(name)) name else appName 296 titleView.text = title 297 titleView.requestFocus() 298 } 299 300 override fun onPageScrolled( 301 position: Int, 302 positionOffset: Float, 303 positionOffsetPixels: Int, 304 ) { 305 super.onPageScrolled(position, positionOffset, positionOffsetPixels) 306 pageIndicator.setLocation(position + positionOffset) 307 } 308 } 309 ) 310 } 311 } 312 313 private fun bindViews() { 314 setContentView(R.layout.controls_management) 315 316 applyInsets(R.id.controls_management_root) 317 318 lifecycle.addObserver( 319 ControlsAnimations.observerForAnimations( 320 requireViewById<ViewGroup>(R.id.controls_management_root), 321 window, 322 intent, 323 ) 324 ) 325 326 requireViewById<ViewStub>(R.id.stub).apply { 327 layoutResource = R.layout.controls_management_favorites 328 inflate() 329 } 330 331 statusText = requireViewById(R.id.status_message) 332 if (shouldShowTooltip()) { 333 mTooltipManager = 334 TooltipManager(statusText.context, TOOLTIP_PREFS_KEY, TOOLTIP_MAX_SHOWN) 335 addContentView( 336 mTooltipManager?.layout, 337 FrameLayout.LayoutParams( 338 ViewGroup.LayoutParams.WRAP_CONTENT, 339 ViewGroup.LayoutParams.WRAP_CONTENT, 340 Gravity.TOP or Gravity.LEFT, 341 ), 342 ) 343 } 344 pageIndicator = 345 requireViewById<ManagementPageIndicator>(R.id.structure_page_indicator).apply { 346 visibilityListener = { 347 if (it != View.VISIBLE) { 348 mTooltipManager?.hide(true) 349 } 350 } 351 } 352 353 val title = 354 structureExtra 355 ?: (appName ?: resources.getText(R.string.controls_favorite_default_title)) 356 titleView = requireViewById<TextView>(R.id.title).apply { text = title } 357 subtitleView = 358 requireViewById<TextView>(R.id.subtitle).apply { 359 text = resources.getText(R.string.controls_favorite_subtitle) 360 } 361 structurePager = requireViewById<ViewPager2>(R.id.structure_pager) 362 structurePager.registerOnPageChangeCallback( 363 object : ViewPager2.OnPageChangeCallback() { 364 override fun onPageSelected(position: Int) { 365 super.onPageSelected(position) 366 mTooltipManager?.hide(true) 367 } 368 } 369 ) 370 bindButtons() 371 } 372 373 @VisibleForTesting 374 internal open fun animateExitAndFinish() { 375 val rootView = requireViewById<ViewGroup>(R.id.controls_management_root) 376 ControlsAnimations.exitAnimation( 377 rootView, 378 object : Runnable { 379 override fun run() { 380 finish() 381 } 382 }, 383 ) 384 .start() 385 } 386 387 private fun bindButtons() { 388 rearrangeButton = 389 requireViewById<Button>(R.id.rearrange).apply { 390 text = 391 if (fromEditing) { 392 getString(R.string.controls_favorite_back_to_editing) 393 } else { 394 getString(R.string.controls_favorite_rearrange_button) 395 } 396 isEnabled = false 397 visibility = View.VISIBLE 398 setOnClickListener { 399 if (component == null) return@setOnClickListener 400 saveFavorites() 401 startActivity( 402 Intent(context, ControlsEditingActivity::class.java).also { 403 it.putExtra(Intent.EXTRA_COMPONENT_NAME, component) 404 it.putExtra(ControlsEditingActivity.EXTRA_APP, appName) 405 it.putExtra(ControlsEditingActivity.EXTRA_FROM_FAVORITING, true) 406 it.putExtra( 407 ControlsEditingActivity.EXTRA_STRUCTURE, 408 listOfStructures[structurePager.currentItem].structureName, 409 ) 410 }, 411 ActivityOptions.makeSceneTransitionAnimation( 412 this@ControlsFavoritingActivity 413 ) 414 .toBundle(), 415 ) 416 } 417 } 418 419 doneButton = 420 requireViewById<Button>(R.id.done).apply { 421 isEnabled = false 422 setOnClickListener { 423 if (component == null) return@setOnClickListener 424 saveFavorites() 425 animateExitAndFinish() 426 openControlsOrigin() 427 } 428 } 429 } 430 431 private fun saveFavorites() { 432 listOfStructures.forEach { 433 val favoritesForStorage = it.model.favorites 434 controller.replaceFavoritesForStructure( 435 StructureInfo(component!!, it.structureName, favoritesForStorage) 436 ) 437 } 438 } 439 440 private fun openControlsOrigin() { 441 startActivity( 442 Intent(applicationContext, ControlsActivity::class.java), 443 ActivityOptions.makeSceneTransitionAnimation(this).toBundle(), 444 ) 445 } 446 447 override fun onPause() { 448 super.onPause() 449 mTooltipManager?.hide(false) 450 } 451 452 override fun onStart() { 453 super.onStart() 454 455 userTracker.addCallback(userTrackerCallback, executor) 456 457 if (DEBUG) { 458 Log.d(TAG, "Registered onBackInvokedCallback") 459 } 460 onBackInvokedDispatcher.registerOnBackInvokedCallback( 461 OnBackInvokedDispatcher.PRIORITY_DEFAULT, 462 mOnBackInvokedCallback, 463 ) 464 } 465 466 override fun onResume() { 467 super.onResume() 468 469 // only do once, to make sure that any user changes do not get replaces if resume is called 470 // more than once 471 if (!isPagerLoaded) { 472 setUpPager() 473 loadControls() 474 isPagerLoaded = true 475 } 476 } 477 478 override fun onStop() { 479 super.onStop() 480 481 userTracker.removeCallback(userTrackerCallback) 482 483 if (DEBUG) { 484 Log.d(TAG, "Unregistered onBackInvokedCallback") 485 } 486 onBackInvokedDispatcher.unregisterOnBackInvokedCallback(mOnBackInvokedCallback) 487 } 488 489 override fun onConfigurationChanged(newConfig: Configuration) { 490 super.onConfigurationChanged(newConfig) 491 mTooltipManager?.hide(false) 492 } 493 494 override fun onDestroy() { 495 cancelLoadRunnable?.run() 496 super.onDestroy() 497 } 498 499 private fun shouldShowTooltip(): Boolean { 500 return Prefs.getInt(applicationContext, TOOLTIP_PREFS_KEY, 0) < TOOLTIP_MAX_SHOWN 501 } 502 } 503 504 data class StructureContainer(val structureName: CharSequence, val model: ControlsModel) 505