• 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.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