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