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