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