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.app.ActivityOptions
20 import android.content.ComponentName
21 import android.content.Context
22 import android.content.Intent
23 import android.os.Bundle
24 import android.util.Log
25 import android.view.View
26 import android.view.ViewGroup
27 import android.view.ViewStub
28 import android.widget.Button
29 import android.widget.TextView
30 import android.widget.Toast
31 import android.window.OnBackInvokedCallback
32 import android.window.OnBackInvokedDispatcher
33 import androidx.activity.ComponentActivity
34 import androidx.recyclerview.widget.GridLayoutManager
35 import androidx.recyclerview.widget.ItemTouchHelper
36 import androidx.recyclerview.widget.RecyclerView
37 import com.android.systemui.res.R
38 import com.android.systemui.controls.CustomIconCache
39 import com.android.systemui.controls.controller.ControlsControllerImpl
40 import com.android.systemui.controls.controller.StructureInfo
41 import com.android.systemui.controls.ui.ControlsActivity
42 import com.android.systemui.dagger.qualifiers.Main
43 import com.android.systemui.settings.UserTracker
44 import java.util.concurrent.Executor
45 import javax.inject.Inject
46 
47 /**
48  * Activity for rearranging and removing controls for a given structure
49  */
50 open class ControlsEditingActivity @Inject constructor(
51     @Main private val mainExecutor: Executor,
52     private val controller: ControlsControllerImpl,
53     private val userTracker: UserTracker,
54     private val customIconCache: CustomIconCache,
55 ) : ComponentActivity() {
56 
57     companion object {
58         private const val DEBUG = false
59         private const val TAG = "ControlsEditingActivity"
60         const val EXTRA_STRUCTURE = ControlsFavoritingActivity.EXTRA_STRUCTURE
61         const val EXTRA_APP = ControlsFavoritingActivity.EXTRA_APP
62         const val EXTRA_FROM_FAVORITING = "extra_from_favoriting"
63         private val SUBTITLE_ID = R.string.controls_favorite_rearrange
64         private val EMPTY_TEXT_ID = R.string.controls_favorite_removed
65     }
66 
67     private lateinit var component: ComponentName
68     private lateinit var structure: CharSequence
69     private lateinit var model: FavoritesModel
70     private lateinit var subtitle: TextView
71     private lateinit var saveButton: View
72     private lateinit var addControls: View
73 
74     private var isFromFavoriting: Boolean = false
75 
76     private val userTrackerCallback: UserTracker.Callback = object : UserTracker.Callback {
77         private val startingUser = controller.currentUserId
78 
onUserChangednull79         override fun onUserChanged(newUser: Int, userContext: Context) {
80             if (newUser != startingUser) {
81                 userTracker.removeCallback(this)
82                 finish()
83             }
84         }
85     }
86 
<lambda>null87     private val mOnBackInvokedCallback = OnBackInvokedCallback {
88         if (DEBUG) {
89             Log.d(TAG, "Predictive Back dispatcher called mOnBackInvokedCallback")
90         }
91         onBackPressed()
92     }
93 
onCreatenull94     override fun onCreate(savedInstanceState: Bundle?) {
95         super.onCreate(savedInstanceState)
96 
97         intent.getParcelableExtra<ComponentName>(Intent.EXTRA_COMPONENT_NAME)?.let {
98             component = it
99         } ?: run(this::finish)
100         isFromFavoriting = intent.getBooleanExtra(EXTRA_FROM_FAVORITING, false)
101         intent.getCharSequenceExtra(EXTRA_STRUCTURE)?.let {
102             structure = it
103         } ?: run(this::finish)
104 
105         bindViews()
106 
107         bindButtons()
108     }
109 
onStartnull110     override fun onStart() {
111         super.onStart()
112         setUpList()
113 
114         userTracker.addCallback(userTrackerCallback, mainExecutor)
115 
116         if (DEBUG) {
117             Log.d(TAG, "Registered onBackInvokedCallback")
118         }
119         onBackInvokedDispatcher.registerOnBackInvokedCallback(
120                 OnBackInvokedDispatcher.PRIORITY_DEFAULT, mOnBackInvokedCallback)
121     }
122 
onStopnull123     override fun onStop() {
124         super.onStop()
125         userTracker.removeCallback(userTrackerCallback)
126 
127         if (DEBUG) {
128             Log.d(TAG, "Unregistered onBackInvokedCallback")
129         }
130         onBackInvokedDispatcher.unregisterOnBackInvokedCallback(mOnBackInvokedCallback)
131     }
132 
onBackPressednull133     override fun onBackPressed() {
134         animateExitAndFinish()
135     }
136 
animateExitAndFinishnull137     private fun animateExitAndFinish() {
138         val rootView = requireViewById<ViewGroup>(R.id.controls_management_root)
139         ControlsAnimations.exitAnimation(
140                 rootView,
141                 object : Runnable {
142                     override fun run() {
143                         finish()
144                     }
145                 }
146         ).start()
147     }
148 
bindViewsnull149     private fun bindViews() {
150         setContentView(R.layout.controls_management)
151 
152         lifecycle.addObserver(
153             ControlsAnimations.observerForAnimations(
154                 requireViewById<ViewGroup>(R.id.controls_management_root),
155                 window,
156                 intent
157             )
158         )
159 
160         requireViewById<ViewStub>(R.id.stub).apply {
161             layoutResource = R.layout.controls_management_editing
162             inflate()
163         }
164         requireViewById<TextView>(R.id.title).text = structure
165         setTitle(structure)
166         subtitle = requireViewById<TextView>(R.id.subtitle).apply {
167             setText(SUBTITLE_ID)
168         }
169     }
170 
bindButtonsnull171     private fun bindButtons() {
172         addControls = requireViewById<Button>(R.id.addControls).apply {
173             isEnabled = true
174             visibility = View.VISIBLE
175             setOnClickListener {
176                 if (saveButton.isEnabled) {
177                     // The user has made changes
178                     Toast.makeText(
179                         applicationContext,
180                         R.string.controls_favorite_toast_no_changes,
181                         Toast.LENGTH_SHORT
182                     ).show()
183                 }
184                 if (isFromFavoriting) {
185                     animateExitAndFinish()
186                 } else {
187                     startActivity(Intent(context, ControlsFavoritingActivity::class.java).also {
188                         it.putExtra(ControlsFavoritingActivity.EXTRA_STRUCTURE, structure)
189                         it.putExtra(Intent.EXTRA_COMPONENT_NAME, component)
190                         it.putExtra(
191                             ControlsFavoritingActivity.EXTRA_APP,
192                             intent.getCharSequenceExtra(EXTRA_APP),
193                         )
194                         it.putExtra(
195                             ControlsFavoritingActivity.EXTRA_SOURCE,
196                             ControlsFavoritingActivity.EXTRA_SOURCE_VALUE_FROM_EDITING,
197                         )
198                     },
199                                   ActivityOptions.makeSceneTransitionAnimation(
200                                       this@ControlsEditingActivity
201                                   ).toBundle(),
202                     )
203                 }
204             }
205         }
206         saveButton = requireViewById<Button>(R.id.done).apply {
207             isEnabled = isFromFavoriting
208             setText(R.string.save)
209             setOnClickListener {
210                 saveFavorites()
211                 startActivity(
212                     Intent(applicationContext, ControlsActivity::class.java),
213                     ActivityOptions
214                         .makeSceneTransitionAnimation(this@ControlsEditingActivity).toBundle()
215                 )
216                 animateExitAndFinish()
217             }
218         }
219     }
220 
saveFavoritesnull221     private fun saveFavorites() {
222         controller.replaceFavoritesForStructure(
223                 StructureInfo(component, structure, model.favorites))
224     }
225 
226     private val favoritesModelCallback = object : FavoritesModel.FavoritesModelCallback {
onNoneChangednull227         override fun onNoneChanged(showNoFavorites: Boolean) {
228             if (showNoFavorites) {
229                 subtitle.setText(EMPTY_TEXT_ID)
230             } else {
231                 subtitle.setText(SUBTITLE_ID)
232             }
233         }
234 
onChangenull235         override fun onChange() = Unit
236 
237         override fun onFirstChange() {
238             saveButton.isEnabled = true
239         }
240     }
241 
setUpListnull242     private fun setUpList() {
243         val controls = controller.getFavoritesForStructure(component, structure)
244         model = FavoritesModel(customIconCache, component, controls, favoritesModelCallback)
245         val elevation = resources.getFloat(R.dimen.control_card_elevation)
246         val recyclerView = requireViewById<RecyclerView>(R.id.list)
247         recyclerView.alpha = 0.0f
248         val adapter = ControlAdapter(elevation, userTracker.userId).apply {
249             registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
250                 var hasAnimated = false
251                 override fun onChanged() {
252                     if (!hasAnimated) {
253                         hasAnimated = true
254                         ControlsAnimations.enterAnimation(recyclerView).start()
255                     }
256                 }
257             })
258         }
259 
260         val margin = resources
261                 .getDimensionPixelSize(R.dimen.controls_card_margin)
262         val itemDecorator = MarginItemDecorator(margin, margin)
263         val spanCount = ControlAdapter.findMaxColumns(resources)
264 
265         recyclerView.apply {
266             this.adapter = adapter
267             layoutManager = object : GridLayoutManager(recyclerView.context, spanCount) {
268 
269                 // This will remove from the announcement the row corresponding to the divider,
270                 // as it's not something that should be announced.
271                 override fun getRowCountForAccessibility(
272                     recycler: RecyclerView.Recycler,
273                     state: RecyclerView.State
274                 ): Int {
275                     val initial = super.getRowCountForAccessibility(recycler, state)
276                     return if (initial > 0) initial - 1 else initial
277                 }
278             }.apply {
279                 spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
280                     override fun getSpanSize(position: Int): Int {
281                         return if (adapter?.getItemViewType(position)
282                                 != ControlAdapter.TYPE_CONTROL) spanCount else 1
283                     }
284                 }
285             }
286             addItemDecoration(itemDecorator)
287         }
288         adapter.changeModel(model)
289         model.attachAdapter(adapter)
290         ItemTouchHelper(model.itemTouchHelperCallback).attachToRecyclerView(recyclerView)
291     }
292 
onDestroynull293     override fun onDestroy() {
294         userTracker.removeCallback(userTrackerCallback)
295         super.onDestroy()
296     }
297 }
298