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