• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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