blob: 5626a5de2e3cbaa690724a8ec9f6315804a7177f [file] [log] [blame]
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -05001/*
2 * Copyright (C) 2019 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
17package com.android.systemui.controls.controller
18
Fabian Kozynskia43c4b22020-02-24 15:43:42 -050019import android.app.ActivityManager
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -050020import android.app.PendingIntent
Fabian Kozynski66e97542020-03-11 15:49:19 -040021import android.app.backup.BackupManager
Fabian Kozynski7988bd42020-01-30 12:21:52 -050022import android.content.BroadcastReceiver
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -050023import android.content.ComponentName
Fabian Kozynski8b540452020-02-04 15:16:30 -050024import android.content.ContentResolver
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -050025import android.content.Context
26import android.content.Intent
Fabian Kozynski7988bd42020-01-30 12:21:52 -050027import android.content.IntentFilter
Fabian Kozynski8b540452020-02-04 15:16:30 -050028import android.database.ContentObserver
29import android.net.Uri
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -050030import android.os.Environment
Fabian Kozynski7988bd42020-01-30 12:21:52 -050031import android.os.UserHandle
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -050032import android.provider.Settings
33import android.service.controls.Control
34import android.service.controls.actions.ControlAction
Matt Pietal61266442020-03-17 12:53:44 -040035import android.util.ArrayMap
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -050036import android.util.Log
Fabian Kozynski8b540452020-02-04 15:16:30 -050037import com.android.internal.annotations.VisibleForTesting
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -050038import com.android.systemui.Dumpable
Fabian Kozynski66e97542020-03-11 15:49:19 -040039import com.android.systemui.backup.BackupHelper
Fabian Kozynski7988bd42020-01-30 12:21:52 -050040import com.android.systemui.broadcast.BroadcastDispatcher
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -050041import com.android.systemui.controls.ControlStatus
Fabian Kozynski84371de2020-02-27 10:58:25 -050042import com.android.systemui.controls.ControlsServiceInfo
Fabian Kozynski7988bd42020-01-30 12:21:52 -050043import com.android.systemui.controls.management.ControlsListingController
Lucas Dupind60b3322020-04-15 18:06:47 -070044import com.android.systemui.controls.ui.ControlWithState
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -050045import com.android.systemui.controls.ui.ControlsUiController
46import com.android.systemui.dagger.qualifiers.Background
Ned Burnsaaeb44b2020-02-12 23:48:26 -050047import com.android.systemui.dump.DumpManager
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -050048import com.android.systemui.util.concurrency.DelayableExecutor
49import java.io.FileDescriptor
50import java.io.PrintWriter
51import java.util.Optional
Fabian Kozynski7988bd42020-01-30 12:21:52 -050052import java.util.concurrent.TimeUnit
Fabian Kozynski9c459e52020-02-12 09:08:15 -050053import java.util.function.Consumer
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -050054import javax.inject.Inject
55import javax.inject.Singleton
56
57@Singleton
58class ControlsControllerImpl @Inject constructor (
59 private val context: Context,
60 @Background private val executor: DelayableExecutor,
61 private val uiController: ControlsUiController,
62 private val bindingController: ControlsBindingController,
Fabian Kozynski7988bd42020-01-30 12:21:52 -050063 private val listingController: ControlsListingController,
Fabian Kozynski8b540452020-02-04 15:16:30 -050064 private val broadcastDispatcher: BroadcastDispatcher,
Fabian Kozynski7988bd42020-01-30 12:21:52 -050065 optionalWrapper: Optional<ControlsFavoritePersistenceWrapper>,
Ned Burnsaaeb44b2020-02-12 23:48:26 -050066 dumpManager: DumpManager
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -050067) : Dumpable, ControlsController {
68
69 companion object {
70 private const val TAG = "ControlsControllerImpl"
Fabian Kozynskibcaf0ef2020-03-23 15:09:37 -040071 internal const val CONTROLS_AVAILABLE = Settings.Secure.CONTROLS_ENABLED
Fabian Kozynski8b540452020-02-04 15:16:30 -050072 internal val URI = Settings.Secure.getUriFor(CONTROLS_AVAILABLE)
73 private const val USER_CHANGE_RETRY_DELAY = 500L // ms
Fabian Kozynski0424ab12020-02-21 12:09:17 -050074 private const val DEFAULT_ENABLED = 1
Fabian Kozynski66e97542020-03-11 15:49:19 -040075 private const val PERMISSION_SELF = "com.android.systemui.permission.SELF"
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -050076 }
77
Fabian Kozynski8b540452020-02-04 15:16:30 -050078 private var userChanging: Boolean = true
79
Fabian Kozynski1a6a7942020-03-10 11:19:04 -040080 private var loadCanceller: Runnable? = null
81
Matt Pietal61266442020-03-17 12:53:44 -040082 private var seedingInProgress = false
83 private val seedingCallbacks = mutableListOf<Consumer<Boolean>>()
84
Fabian Kozynskia43c4b22020-02-24 15:43:42 -050085 private var currentUser = UserHandle.of(ActivityManager.getCurrentUser())
Fabian Kozynski7988bd42020-01-30 12:21:52 -050086 override val currentUserId
87 get() = currentUser.identifier
88
Fabian Kozynskia43c4b22020-02-24 15:43:42 -050089 private val contentResolver: ContentResolver
90 get() = context.contentResolver
91 override var available = Settings.Secure.getIntForUser(
92 contentResolver, CONTROLS_AVAILABLE, DEFAULT_ENABLED, currentUserId) != 0
93 private set
94
Fabian Kozynski66e97542020-03-11 15:49:19 -040095 private var file = Environment.buildPath(
96 context.filesDir,
97 ControlsFavoritePersistenceWrapper.FILE_NAME
98 )
99 private var auxiliaryFile = Environment.buildPath(
100 context.filesDir,
101 AuxiliaryPersistenceWrapper.AUXILIARY_FILE_NAME
102 )
Fabian Kozynski7988bd42020-01-30 12:21:52 -0500103 private val persistenceWrapper = optionalWrapper.orElseGet {
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500104 ControlsFavoritePersistenceWrapper(
Fabian Kozynski66e97542020-03-11 15:49:19 -0400105 file,
106 executor,
107 BackupManager(context)
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500108 )
109 }
110
Fabian Kozynski66e97542020-03-11 15:49:19 -0400111 @VisibleForTesting
112 internal var auxiliaryPersistenceWrapper = AuxiliaryPersistenceWrapper(auxiliaryFile, executor)
113
Fabian Kozynski7988bd42020-01-30 12:21:52 -0500114 private fun setValuesForUser(newUser: UserHandle) {
115 Log.d(TAG, "Changing to user: $newUser")
116 currentUser = newUser
117 val userContext = context.createContextAsUser(currentUser, 0)
Fabian Kozynski66e97542020-03-11 15:49:19 -0400118 file = Environment.buildPath(
Fabian Kozynski7988bd42020-01-30 12:21:52 -0500119 userContext.filesDir, ControlsFavoritePersistenceWrapper.FILE_NAME)
Fabian Kozynski66e97542020-03-11 15:49:19 -0400120 auxiliaryFile = Environment.buildPath(
121 userContext.filesDir, AuxiliaryPersistenceWrapper.AUXILIARY_FILE_NAME)
122 persistenceWrapper.changeFileAndBackupManager(file, BackupManager(userContext))
123 auxiliaryPersistenceWrapper.changeFile(auxiliaryFile)
Fabian Kozynski8b540452020-02-04 15:16:30 -0500124 available = Settings.Secure.getIntForUser(contentResolver, CONTROLS_AVAILABLE,
Fabian Kozynskia43c4b22020-02-24 15:43:42 -0500125 DEFAULT_ENABLED, newUser.identifier) != 0
Matt Pietal313f37d2020-02-24 11:27:22 -0500126 resetFavorites(available)
Fabian Kozynski7988bd42020-01-30 12:21:52 -0500127 bindingController.changeUser(newUser)
128 listingController.changeUser(newUser)
129 userChanging = false
130 }
131
132 private val userSwitchReceiver = object : BroadcastReceiver() {
133 override fun onReceive(context: Context, intent: Intent) {
134 if (intent.action == Intent.ACTION_USER_SWITCHED) {
135 userChanging = true
Fabian Kozynski84371de2020-02-27 10:58:25 -0500136 listingController.removeCallback(listingCallback)
Fabian Kozynski7988bd42020-01-30 12:21:52 -0500137 val newUser =
138 UserHandle.of(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, sendingUserId))
139 if (currentUser == newUser) {
140 userChanging = false
141 return
142 }
143 setValuesForUser(newUser)
144 }
145 }
146 }
147
Fabian Kozynski8b540452020-02-04 15:16:30 -0500148 @VisibleForTesting
Fabian Kozynski66e97542020-03-11 15:49:19 -0400149 internal val restoreFinishedReceiver = object : BroadcastReceiver() {
150 override fun onReceive(context: Context, intent: Intent) {
151 val user = intent.getIntExtra(Intent.EXTRA_USER_ID, UserHandle.USER_NULL)
152 if (user == currentUserId) {
153 executor.execute {
154 auxiliaryPersistenceWrapper.initialize()
155 listingController.removeCallback(listingCallback)
156 persistenceWrapper.storeFavorites(auxiliaryPersistenceWrapper.favorites)
157 resetFavorites(available)
158 }
159 }
160 }
161 }
162
163 @VisibleForTesting
Fabian Kozynski8b540452020-02-04 15:16:30 -0500164 internal val settingObserver = object : ContentObserver(null) {
Fabian Kozynskicaf76d22020-03-06 18:07:43 -0500165 override fun onChange(
166 selfChange: Boolean,
Jeff Sharkey8b0cff72020-03-09 15:49:01 -0600167 uris: Collection<Uri>,
Fabian Kozynskicaf76d22020-03-06 18:07:43 -0500168 flags: Int,
169 userId: Int
170 ) {
Fabian Kozynski8b540452020-02-04 15:16:30 -0500171 // Do not listen to changes in the middle of user change, those will be read by the
172 // user-switch receiver.
173 if (userChanging || userId != currentUserId) {
174 return
175 }
176 available = Settings.Secure.getIntForUser(contentResolver, CONTROLS_AVAILABLE,
Fabian Kozynskicaf76d22020-03-06 18:07:43 -0500177 DEFAULT_ENABLED, currentUserId) != 0
Matt Pietal313f37d2020-02-24 11:27:22 -0500178 resetFavorites(available)
Fabian Kozynski8b540452020-02-04 15:16:30 -0500179 }
180 }
181
Fabian Kozynski84371de2020-02-27 10:58:25 -0500182 // Handling of removed components
183
184 /**
185 * Check if any component has been removed and if so, remove all its favorites.
186 *
187 * If some component has been removed, the new set of favorites will also be saved.
188 */
189 private val listingCallback = object : ControlsListingController.ControlsListingCallback {
Matt Pietal638253a2020-03-02 09:10:43 -0500190 override fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>) {
Fabian Kozynski84371de2020-02-27 10:58:25 -0500191 executor.execute {
Matt Pietal638253a2020-03-02 09:10:43 -0500192 val serviceInfoSet = serviceInfos.map(ControlsServiceInfo::componentName).toSet()
193 val favoriteComponentSet = Favorites.getAllStructures().map {
194 it.componentName
195 }.toSet()
196
197 var changed = false
198 favoriteComponentSet.subtract(serviceInfoSet).forEach {
199 changed = true
200 Favorites.removeStructures(it)
201 bindingController.onComponentRemoved(it)
202 }
203
Fabian Kozynski66e97542020-03-11 15:49:19 -0400204 if (auxiliaryPersistenceWrapper.favorites.isNotEmpty()) {
205 serviceInfoSet.subtract(favoriteComponentSet).forEach {
206 val toAdd = auxiliaryPersistenceWrapper.getCachedFavoritesAndRemoveFor(it)
207 if (toAdd.isNotEmpty()) {
208 changed = true
209 toAdd.forEach {
210 Favorites.replaceControls(it)
211 }
212 }
213 }
214 // Need to clear the ones that were restored immediately. This will delete
215 // them from the auxiliary file if they were not deleted. Should only do any
216 // work the first time after a restore.
217 serviceInfoSet.intersect(favoriteComponentSet).forEach {
218 auxiliaryPersistenceWrapper.getCachedFavoritesAndRemoveFor(it)
219 }
220 }
221
222 // Check if something has been added or removed, if so, store the new list
Matt Pietal638253a2020-03-02 09:10:43 -0500223 if (changed) {
224 persistenceWrapper.storeFavorites(Favorites.getAllStructures())
Fabian Kozynski84371de2020-02-27 10:58:25 -0500225 }
226 }
227 }
228 }
229
Fabian Kozynski7988bd42020-01-30 12:21:52 -0500230 init {
Ned Burnsaaeb44b2020-02-12 23:48:26 -0500231 dumpManager.registerDumpable(javaClass.name, this)
Matt Pietal313f37d2020-02-24 11:27:22 -0500232 resetFavorites(available)
Fabian Kozynski7988bd42020-01-30 12:21:52 -0500233 userChanging = false
234 broadcastDispatcher.registerReceiver(
235 userSwitchReceiver,
236 IntentFilter(Intent.ACTION_USER_SWITCHED),
237 executor,
238 UserHandle.ALL
239 )
Fabian Kozynski66e97542020-03-11 15:49:19 -0400240 context.registerReceiver(
241 restoreFinishedReceiver,
242 IntentFilter(BackupHelper.ACTION_RESTORE_FINISHED),
243 PERMISSION_SELF,
244 null
245 )
Fabian Kozynski8b540452020-02-04 15:16:30 -0500246 contentResolver.registerContentObserver(URI, false, settingObserver, UserHandle.USER_ALL)
Fabian Kozynski7988bd42020-01-30 12:21:52 -0500247 }
248
Fabian Kozynski66e97542020-03-11 15:49:19 -0400249 fun destroy() {
250 broadcastDispatcher.unregisterReceiver(userSwitchReceiver)
251 context.unregisterReceiver(restoreFinishedReceiver)
252 contentResolver.unregisterContentObserver(settingObserver)
253 listingController.removeCallback(listingCallback)
254 }
255
Matt Pietal313f37d2020-02-24 11:27:22 -0500256 private fun resetFavorites(shouldLoad: Boolean) {
257 Favorites.clear()
258
259 if (shouldLoad) {
260 Favorites.load(persistenceWrapper.readFavorites())
Matt Pietal638253a2020-03-02 09:10:43 -0500261 listingController.addCallback(listingCallback)
Matt Pietal313f37d2020-02-24 11:27:22 -0500262 }
263 }
264
Fabian Kozynski7988bd42020-01-30 12:21:52 -0500265 private fun confirmAvailability(): Boolean {
266 if (userChanging) {
267 Log.w(TAG, "Controls not available while user is changing")
268 return false
269 }
270 if (!available) {
271 Log.d(TAG, "Controls not available")
272 return false
273 }
274 return true
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500275 }
276
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500277 override fun loadForComponent(
278 componentName: ComponentName,
Fabian Kozynski9c459e52020-02-12 09:08:15 -0500279 dataCallback: Consumer<ControlsController.LoadData>
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500280 ) {
Fabian Kozynski7988bd42020-01-30 12:21:52 -0500281 if (!confirmAvailability()) {
282 if (userChanging) {
283 // Try again later, userChanging should not last forever. If so, we have bigger
Fabian Kozynski1a6a7942020-03-10 11:19:04 -0400284 // problems. This will return a runnable that allows to cancel the delayed version,
285 // it will not be able to cancel the load if
286 loadCanceller = executor.executeDelayed(
Fabian Kozynski9c459e52020-02-12 09:08:15 -0500287 { loadForComponent(componentName, dataCallback) },
Fabian Kozynski7988bd42020-01-30 12:21:52 -0500288 USER_CHANGE_RETRY_DELAY,
289 TimeUnit.MILLISECONDS
290 )
291 } else {
Fabian Kozynski9c459e52020-02-12 09:08:15 -0500292 dataCallback.accept(createLoadDataObject(emptyList(), emptyList(), true))
Fabian Kozynski7988bd42020-01-30 12:21:52 -0500293 }
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500294 return
295 }
Fabian Kozynski1a6a7942020-03-10 11:19:04 -0400296 loadCanceller = bindingController.bindAndLoad(
Fabian Kozynski9c459e52020-02-12 09:08:15 -0500297 componentName,
298 object : ControlsBindingController.LoadCallback {
299 override fun accept(controls: List<Control>) {
Fabian Kozynski1a6a7942020-03-10 11:19:04 -0400300 loadCanceller = null
Matt Pietal638253a2020-03-02 09:10:43 -0500301 executor.execute {
302 val favoritesForComponentKeys = Favorites
303 .getControlsForComponent(componentName).map { it.controlId }
Matt Pietal313f37d2020-02-24 11:27:22 -0500304
Matt Pietal638253a2020-03-02 09:10:43 -0500305 val changed = Favorites.updateControls(componentName, controls)
306 if (changed) {
307 persistenceWrapper.storeFavorites(Favorites.getAllStructures())
308 }
309 val removed = findRemoved(favoritesForComponentKeys.toSet(), controls)
310 val controlsWithFavorite = controls.map {
Matt Pietal53a8bbd2020-03-05 16:10:34 -0500311 ControlStatus(
312 it,
313 componentName,
314 it.controlId in favoritesForComponentKeys
315 )
Matt Pietal638253a2020-03-02 09:10:43 -0500316 }
Fabian Kozynskia9803042020-03-26 12:07:21 -0400317 val removedControls = mutableListOf<ControlStatus>()
318 Favorites.getStructuresForComponent(componentName).forEach { st ->
319 st.controls.forEach {
320 if (it.controlId in removed) {
321 val r = createRemovedStatus(componentName, it, st.structure)
322 removedControls.add(r)
323 }
324 }
325 }
Matt Pietal638253a2020-03-02 09:10:43 -0500326 val loadData = createLoadDataObject(
Fabian Kozynskia9803042020-03-26 12:07:21 -0400327 removedControls +
Matt Pietal638253a2020-03-02 09:10:43 -0500328 controlsWithFavorite,
329 favoritesForComponentKeys
330 )
Matt Pietal638253a2020-03-02 09:10:43 -0500331 dataCallback.accept(loadData)
332 }
Fabian Kozynski9c459e52020-02-12 09:08:15 -0500333 }
334
335 override fun error(message: String) {
Fabian Kozynski1a6a7942020-03-10 11:19:04 -0400336 loadCanceller = null
Fabian Kozynski713b7272020-03-03 18:35:52 -0500337 executor.execute {
Fabian Kozynskia9803042020-03-26 12:07:21 -0400338 val controls = Favorites.getStructuresForComponent(componentName)
339 .flatMap { st ->
340 st.controls.map {
341 createRemovedStatus(componentName, it, st.structure,
342 false)
343 }
344 }
345 val keys = controls.map { it.control.controlId }
346 val loadData = createLoadDataObject(controls, keys, true)
Fabian Kozynski713b7272020-03-03 18:35:52 -0500347 dataCallback.accept(loadData)
348 }
Fabian Kozynski9c459e52020-02-12 09:08:15 -0500349 }
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500350 }
Fabian Kozynski9c459e52020-02-12 09:08:15 -0500351 )
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500352 }
353
Matt Pietal61266442020-03-17 12:53:44 -0400354 override fun addSeedingFavoritesCallback(callback: Consumer<Boolean>): Boolean {
355 if (!seedingInProgress) return false
356 executor.execute {
357 // status may have changed by this point, so check again and inform the
358 // caller if necessary
359 if (seedingInProgress) seedingCallbacks.add(callback)
360 else callback.accept(false)
361 }
362 return true
363 }
364
365 override fun seedFavoritesForComponent(
366 componentName: ComponentName,
367 callback: Consumer<Boolean>
368 ) {
Matt Pietalcd757c82020-04-08 10:20:48 -0400369 if (seedingInProgress) return
370
Matt Pietal61266442020-03-17 12:53:44 -0400371 Log.i(TAG, "Beginning request to seed favorites for: $componentName")
372 if (!confirmAvailability()) {
373 if (userChanging) {
374 // Try again later, userChanging should not last forever. If so, we have bigger
375 // problems. This will return a runnable that allows to cancel the delayed version,
376 // it will not be able to cancel the load if
377 executor.executeDelayed(
378 { seedFavoritesForComponent(componentName, callback) },
379 USER_CHANGE_RETRY_DELAY,
380 TimeUnit.MILLISECONDS
381 )
382 } else {
383 callback.accept(false)
384 }
385 return
386 }
387 seedingInProgress = true
388 bindingController.bindAndLoadSuggested(
389 componentName,
390 object : ControlsBindingController.LoadCallback {
391 override fun accept(controls: List<Control>) {
392 executor.execute {
393 val structureToControls =
394 ArrayMap<CharSequence, MutableList<ControlInfo>>()
395
396 controls.forEach {
397 val structure = it.structure ?: ""
398 val list = structureToControls.get(structure)
Matt Pietal85878262020-03-18 15:34:46 -0400399 ?: mutableListOf<ControlInfo>()
400 list.add(
401 ControlInfo(it.controlId, it.title, it.subtitle, it.deviceType))
Matt Pietal61266442020-03-17 12:53:44 -0400402 structureToControls.put(structure, list)
403 }
404
405 structureToControls.forEach {
406 (s, cs) -> Favorites.replaceControls(
407 StructureInfo(componentName, s, cs))
408 }
409
410 persistenceWrapper.storeFavorites(Favorites.getAllStructures())
411 callback.accept(true)
412 endSeedingCall(true)
413 }
414 }
415
416 override fun error(message: String) {
417 Log.e(TAG, "Unable to seed favorites: $message")
418 executor.execute {
419 callback.accept(false)
420 endSeedingCall(false)
421 }
422 }
423 }
424 )
425 }
426
427 private fun endSeedingCall(state: Boolean) {
428 seedingInProgress = false
429 seedingCallbacks.forEach {
430 it.accept(state)
431 }
432 seedingCallbacks.clear()
433 }
434
Fabian Kozynski1a6a7942020-03-10 11:19:04 -0400435 override fun cancelLoad() {
436 loadCanceller?.let {
437 executor.execute(it)
438 }
439 }
440
Fabian Kozynski9c459e52020-02-12 09:08:15 -0500441 private fun createRemovedStatus(
Matt Pietal313f37d2020-02-24 11:27:22 -0500442 componentName: ComponentName,
Fabian Kozynski9c459e52020-02-12 09:08:15 -0500443 controlInfo: ControlInfo,
Fabian Kozynskia9803042020-03-26 12:07:21 -0400444 structure: CharSequence,
Fabian Kozynski9c459e52020-02-12 09:08:15 -0500445 setRemoved: Boolean = true
446 ): ControlStatus {
447 val intent = Intent(Intent.ACTION_MAIN).apply {
448 addCategory(Intent.CATEGORY_LAUNCHER)
Matt Pietal313f37d2020-02-24 11:27:22 -0500449 this.`package` = componentName.packageName
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500450 }
451 val pendingIntent = PendingIntent.getActivity(context,
Matt Pietal313f37d2020-02-24 11:27:22 -0500452 componentName.hashCode(),
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500453 intent,
454 0)
455 val control = Control.StatelessBuilder(controlInfo.controlId, pendingIntent)
456 .setTitle(controlInfo.controlTitle)
Fabian Kozynskia9803042020-03-26 12:07:21 -0400457 .setSubtitle(controlInfo.controlSubtitle)
458 .setStructure(structure)
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500459 .setDeviceType(controlInfo.deviceType)
460 .build()
Matt Pietal53a8bbd2020-03-05 16:10:34 -0500461 return ControlStatus(control, componentName, true, setRemoved)
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500462 }
463
Matt Pietal638253a2020-03-02 09:10:43 -0500464 private fun findRemoved(favoriteKeys: Set<String>, list: List<Control>): Set<String> {
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500465 val controlsKeys = list.map { it.controlId }
466 return favoriteKeys.minus(controlsKeys)
467 }
468
Matt Pietal313f37d2020-02-24 11:27:22 -0500469 override fun subscribeToFavorites(structureInfo: StructureInfo) {
Fabian Kozynski7988bd42020-01-30 12:21:52 -0500470 if (!confirmAvailability()) return
Matt Pietal313f37d2020-02-24 11:27:22 -0500471
472 bindingController.subscribe(structureInfo)
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500473 }
474
475 override fun unsubscribe() {
Fabian Kozynski7988bd42020-01-30 12:21:52 -0500476 if (!confirmAvailability()) return
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500477 bindingController.unsubscribe()
478 }
479
Fabian Kozynski04e7bde2020-02-13 13:02:33 -0500480 override fun addFavorite(
481 componentName: ComponentName,
482 structureName: CharSequence,
483 controlInfo: ControlInfo
484 ) {
485 if (!confirmAvailability()) return
486 executor.execute {
487 if (Favorites.addFavorite(componentName, structureName, controlInfo)) {
488 persistenceWrapper.storeFavorites(Favorites.getAllStructures())
489 }
490 }
491 }
492
Matt Pietal313f37d2020-02-24 11:27:22 -0500493 override fun replaceFavoritesForStructure(structureInfo: StructureInfo) {
Fabian Kozynski7988bd42020-01-30 12:21:52 -0500494 if (!confirmAvailability()) return
Matt Pietal638253a2020-03-02 09:10:43 -0500495 executor.execute {
496 Favorites.replaceControls(structureInfo)
497 persistenceWrapper.storeFavorites(Favorites.getAllStructures())
498 }
Fabian Kozynski9aa23af2020-02-05 17:47:47 -0500499 }
500
Matt Pietalcd757c82020-04-08 10:20:48 -0400501 override fun resetFavorites() {
502 executor.execute {
503 Favorites.clear()
504 persistenceWrapper.storeFavorites(Favorites.getAllStructures())
505 }
506 }
507
Lucas Dupind60b3322020-04-15 18:06:47 -0700508 override fun onFocusChanged(cws: ControlWithState?) {
509 uiController.onFocusChanged(cws)
510 }
511
Fabian Kozynski1bb26b52020-01-08 18:20:36 -0500512 override fun refreshStatus(componentName: ComponentName, control: Control) {
Fabian Kozynski7988bd42020-01-30 12:21:52 -0500513 if (!confirmAvailability()) {
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500514 Log.d(TAG, "Controls not available")
515 return
516 }
Matt Pietal1a209db2020-03-27 11:58:24 -0400517
518 // Assume that non STATUS_OK responses may contain incomplete or invalid information about
519 // the control, and do not attempt to update it
520 if (control.getStatus() == Control.STATUS_OK) {
521 executor.execute {
522 if (Favorites.updateControls(componentName, listOf(control))) {
523 persistenceWrapper.storeFavorites(Favorites.getAllStructures())
524 }
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500525 }
526 }
Fabian Kozynski1bb26b52020-01-08 18:20:36 -0500527 uiController.onRefreshState(componentName, listOf(control))
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500528 }
529
530 override fun onActionResponse(componentName: ComponentName, controlId: String, response: Int) {
Fabian Kozynski7988bd42020-01-30 12:21:52 -0500531 if (!confirmAvailability()) return
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500532 uiController.onActionResponse(componentName, controlId, response)
533 }
534
Matt Pietal313f37d2020-02-24 11:27:22 -0500535 override fun action(
536 componentName: ComponentName,
537 controlInfo: ControlInfo,
538 action: ControlAction
539 ) {
Fabian Kozynski7988bd42020-01-30 12:21:52 -0500540 if (!confirmAvailability()) return
Matt Pietal313f37d2020-02-24 11:27:22 -0500541 bindingController.action(componentName, controlInfo, action)
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500542 }
543
Matt Pietal313f37d2020-02-24 11:27:22 -0500544 override fun getFavorites(): List<StructureInfo> = Favorites.getAllStructures()
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500545
Matt Pietal313f37d2020-02-24 11:27:22 -0500546 override fun countFavoritesForComponent(componentName: ComponentName): Int =
547 Favorites.getControlsForComponent(componentName).size
Fabian Kozynski5fc5f6b2020-02-03 15:21:14 -0500548
Matt Pietal313f37d2020-02-24 11:27:22 -0500549 override fun getFavoritesForComponent(componentName: ComponentName): List<StructureInfo> =
550 Favorites.getStructuresForComponent(componentName)
Fabian Kozynski9aa23af2020-02-05 17:47:47 -0500551
Fabian Kozynski8765d352020-04-06 21:16:02 -0400552 override fun getFavoritesForStructure(
553 componentName: ComponentName,
554 structureName: CharSequence
555 ): List<ControlInfo> {
556 return Favorites.getControlsForStructure(
557 StructureInfo(componentName, structureName, emptyList())
558 )
559 }
560
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500561 override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<out String>) {
562 pw.println("ControlsController state:")
Fabian Kozynski7988bd42020-01-30 12:21:52 -0500563 pw.println(" Available: $available")
564 pw.println(" Changing users: $userChanging")
565 pw.println(" Current user: ${currentUser.identifier}")
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500566 pw.println(" Favorites:")
Matt Pietal313f37d2020-02-24 11:27:22 -0500567 Favorites.getAllStructures().forEach { s ->
568 pw.println(" ${ s }")
569 s.controls.forEach { c ->
570 pw.println(" ${ c }")
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500571 }
572 }
Fabian Kozynski7988bd42020-01-30 12:21:52 -0500573 pw.println(bindingController.toString())
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500574 }
Matt Pietal313f37d2020-02-24 11:27:22 -0500575}
576
577/**
578 * Relies on immutable data for thread safety. When necessary to update favMap, use reassignment to
579 * replace it, which will not disrupt any ongoing map traversal.
Matt Pietal638253a2020-03-02 09:10:43 -0500580 *
581 * Update/replace calls should use thread isolation to avoid race conditions.
Matt Pietal313f37d2020-02-24 11:27:22 -0500582 */
583private object Favorites {
584 private var favMap = mapOf<ComponentName, List<StructureInfo>>()
585
586 fun getAllStructures(): List<StructureInfo> = favMap.flatMap { it.value }
587
588 fun getStructuresForComponent(componentName: ComponentName): List<StructureInfo> =
589 favMap.get(componentName) ?: emptyList()
590
591 fun getControlsForStructure(structure: StructureInfo): List<ControlInfo> =
592 getStructuresForComponent(structure.componentName)
593 .firstOrNull { it.structure == structure.structure }
594 ?.controls ?: emptyList()
595
596 fun getControlsForComponent(componentName: ComponentName): List<ControlInfo> =
597 getStructuresForComponent(componentName).flatMap { it.controls }
598
599 fun load(structures: List<StructureInfo>) {
600 favMap = structures.groupBy { it.componentName }
601 }
602
603 fun updateControls(componentName: ComponentName, controls: List<Control>): Boolean {
604 val controlsById = controls.associateBy { it.controlId }
605
606 // utilize a new map to allow for changes to structure names
607 val structureToControls = mutableMapOf<CharSequence, MutableList<ControlInfo>>()
608
609 // Must retain the current control order within each structure
610 var changed = false
611 getStructuresForComponent(componentName).forEach { s ->
612 s.controls.forEach { c ->
613 val (sName, ci) = controlsById.get(c.controlId)?.let { updatedControl ->
614 val controlInfo = if (updatedControl.title != c.controlTitle ||
Matt Pietal85878262020-03-18 15:34:46 -0400615 updatedControl.subtitle != c.controlSubtitle ||
Matt Pietal313f37d2020-02-24 11:27:22 -0500616 updatedControl.deviceType != c.deviceType) {
617 changed = true
618 c.copy(
619 controlTitle = updatedControl.title,
Matt Pietal85878262020-03-18 15:34:46 -0400620 controlSubtitle = updatedControl.subtitle,
Matt Pietal313f37d2020-02-24 11:27:22 -0500621 deviceType = updatedControl.deviceType
622 )
623 } else { c }
624
625 val updatedStructure = updatedControl.structure ?: ""
626 if (s.structure != updatedStructure) {
627 changed = true
628 }
629
630 Pair(updatedStructure, controlInfo)
631 } ?: Pair(s.structure, c)
632
633 structureToControls.getOrPut(sName, { mutableListOf() }).add(ci)
634 }
635 }
636 if (!changed) return false
637
638 val structures = structureToControls.map { (s, cs) -> StructureInfo(componentName, s, cs) }
639
640 val newFavMap = favMap.toMutableMap()
641 newFavMap.put(componentName, structures)
Matt Pietal638253a2020-03-02 09:10:43 -0500642 favMap = newFavMap
Matt Pietal313f37d2020-02-24 11:27:22 -0500643
644 return true
645 }
646
Matt Pietal638253a2020-03-02 09:10:43 -0500647 fun removeStructures(componentName: ComponentName) {
648 val newFavMap = favMap.toMutableMap()
649 newFavMap.remove(componentName)
650 favMap = newFavMap
651 }
652
Fabian Kozynski04e7bde2020-02-13 13:02:33 -0500653 fun addFavorite(
654 componentName: ComponentName,
655 structureName: CharSequence,
656 controlInfo: ControlInfo
657 ): Boolean {
658 // Check if control is in favorites
659 if (getControlsForComponent(componentName)
660 .any { it.controlId == controlInfo.controlId }) {
661 return false
662 }
663 val structureInfo = favMap.get(componentName)
664 ?.firstOrNull { it.structure == structureName }
665 ?: StructureInfo(componentName, structureName, emptyList())
666 val newStructureInfo = structureInfo.copy(controls = structureInfo.controls + controlInfo)
667 replaceControls(newStructureInfo)
668 return true
669 }
670
Matt Pietal313f37d2020-02-24 11:27:22 -0500671 fun replaceControls(updatedStructure: StructureInfo) {
672 val newFavMap = favMap.toMutableMap()
673 val structures = mutableListOf<StructureInfo>()
674 val componentName = updatedStructure.componentName
675
676 var replaced = false
677 getStructuresForComponent(componentName).forEach { s ->
678 val newStructure = if (s.structure == updatedStructure.structure) {
679 replaced = true
680 updatedStructure
681 } else { s }
682
Matt Pietal567b0f62020-03-10 08:44:33 -0400683 if (!newStructure.controls.isEmpty()) {
684 structures.add(newStructure)
685 }
Matt Pietal313f37d2020-02-24 11:27:22 -0500686 }
687
Matt Pietal567b0f62020-03-10 08:44:33 -0400688 if (!replaced && !updatedStructure.controls.isEmpty()) {
Matt Pietal313f37d2020-02-24 11:27:22 -0500689 structures.add(updatedStructure)
690 }
691
Fabian Kozynski04e7bde2020-02-13 13:02:33 -0500692 newFavMap.put(componentName, structures)
693 favMap = newFavMap
Matt Pietal313f37d2020-02-24 11:27:22 -0500694 }
695
696 fun clear() {
697 favMap = mapOf<ComponentName, List<StructureInfo>>()
698 }
699}