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