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