blob: 6d34009169d50fd0d5562bdb76fd8e66525de93d [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
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -050044import com.android.systemui.controls.ui.ControlsUiController
45import com.android.systemui.dagger.qualifiers.Background
Ned Burnsaaeb44b2020-02-12 23:48:26 -050046import com.android.systemui.dump.DumpManager
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -050047import com.android.systemui.util.concurrency.DelayableExecutor
48import java.io.FileDescriptor
49import java.io.PrintWriter
50import java.util.Optional
Fabian Kozynski7988bd42020-01-30 12:21:52 -050051import java.util.concurrent.TimeUnit
Fabian Kozynski9c459e52020-02-12 09:08:15 -050052import java.util.function.Consumer
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -050053import javax.inject.Inject
54import javax.inject.Singleton
55
56@Singleton
57class ControlsControllerImpl @Inject constructor (
58 private val context: Context,
59 @Background private val executor: DelayableExecutor,
60 private val uiController: ControlsUiController,
61 private val bindingController: ControlsBindingController,
Fabian Kozynski7988bd42020-01-30 12:21:52 -050062 private val listingController: ControlsListingController,
Fabian Kozynski8b540452020-02-04 15:16:30 -050063 private val broadcastDispatcher: BroadcastDispatcher,
Fabian Kozynski7988bd42020-01-30 12:21:52 -050064 optionalWrapper: Optional<ControlsFavoritePersistenceWrapper>,
Ned Burnsaaeb44b2020-02-12 23:48:26 -050065 dumpManager: DumpManager
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -050066) : Dumpable, ControlsController {
67
68 companion object {
69 private const val TAG = "ControlsControllerImpl"
Fabian Kozynskibcaf0ef2020-03-23 15:09:37 -040070 internal const val CONTROLS_AVAILABLE = Settings.Secure.CONTROLS_ENABLED
Fabian Kozynski8b540452020-02-04 15:16:30 -050071 internal val URI = Settings.Secure.getUriFor(CONTROLS_AVAILABLE)
72 private const val USER_CHANGE_RETRY_DELAY = 500L // ms
Fabian Kozynski0424ab12020-02-21 12:09:17 -050073 private const val DEFAULT_ENABLED = 1
Fabian Kozynski66e97542020-03-11 15:49:19 -040074 private const val PERMISSION_SELF = "com.android.systemui.permission.SELF"
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -050075 }
76
Fabian Kozynski8b540452020-02-04 15:16:30 -050077 private var userChanging: Boolean = true
78
Fabian Kozynski1a6a7942020-03-10 11:19:04 -040079 private var loadCanceller: Runnable? = null
80
Matt Pietal61266442020-03-17 12:53:44 -040081 private var seedingInProgress = false
82 private val seedingCallbacks = mutableListOf<Consumer<Boolean>>()
83
Fabian Kozynskia43c4b22020-02-24 15:43:42 -050084 private var currentUser = UserHandle.of(ActivityManager.getCurrentUser())
Fabian Kozynski7988bd42020-01-30 12:21:52 -050085 override val currentUserId
86 get() = currentUser.identifier
87
Fabian Kozynskia43c4b22020-02-24 15:43:42 -050088 private val contentResolver: ContentResolver
89 get() = context.contentResolver
90 override var available = Settings.Secure.getIntForUser(
91 contentResolver, CONTROLS_AVAILABLE, DEFAULT_ENABLED, currentUserId) != 0
92 private set
93
Fabian Kozynski66e97542020-03-11 15:49:19 -040094 private var file = Environment.buildPath(
95 context.filesDir,
96 ControlsFavoritePersistenceWrapper.FILE_NAME
97 )
98 private var auxiliaryFile = Environment.buildPath(
99 context.filesDir,
100 AuxiliaryPersistenceWrapper.AUXILIARY_FILE_NAME
101 )
Fabian Kozynski7988bd42020-01-30 12:21:52 -0500102 private val persistenceWrapper = optionalWrapper.orElseGet {
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500103 ControlsFavoritePersistenceWrapper(
Fabian Kozynski66e97542020-03-11 15:49:19 -0400104 file,
105 executor,
106 BackupManager(context)
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500107 )
108 }
109
Fabian Kozynski66e97542020-03-11 15:49:19 -0400110 @VisibleForTesting
111 internal var auxiliaryPersistenceWrapper = AuxiliaryPersistenceWrapper(auxiliaryFile, executor)
112
Fabian Kozynski7988bd42020-01-30 12:21:52 -0500113 private fun setValuesForUser(newUser: UserHandle) {
114 Log.d(TAG, "Changing to user: $newUser")
115 currentUser = newUser
116 val userContext = context.createContextAsUser(currentUser, 0)
Fabian Kozynski66e97542020-03-11 15:49:19 -0400117 file = Environment.buildPath(
Fabian Kozynski7988bd42020-01-30 12:21:52 -0500118 userContext.filesDir, ControlsFavoritePersistenceWrapper.FILE_NAME)
Fabian Kozynski66e97542020-03-11 15:49:19 -0400119 auxiliaryFile = Environment.buildPath(
120 userContext.filesDir, AuxiliaryPersistenceWrapper.AUXILIARY_FILE_NAME)
121 persistenceWrapper.changeFileAndBackupManager(file, BackupManager(userContext))
122 auxiliaryPersistenceWrapper.changeFile(auxiliaryFile)
Fabian Kozynski8b540452020-02-04 15:16:30 -0500123 available = Settings.Secure.getIntForUser(contentResolver, CONTROLS_AVAILABLE,
Fabian Kozynskia43c4b22020-02-24 15:43:42 -0500124 DEFAULT_ENABLED, newUser.identifier) != 0
Matt Pietal313f37d2020-02-24 11:27:22 -0500125 resetFavorites(available)
Fabian Kozynski7988bd42020-01-30 12:21:52 -0500126 bindingController.changeUser(newUser)
127 listingController.changeUser(newUser)
128 userChanging = false
129 }
130
131 private val userSwitchReceiver = object : BroadcastReceiver() {
132 override fun onReceive(context: Context, intent: Intent) {
133 if (intent.action == Intent.ACTION_USER_SWITCHED) {
134 userChanging = true
Fabian Kozynski84371de2020-02-27 10:58:25 -0500135 listingController.removeCallback(listingCallback)
Fabian Kozynski7988bd42020-01-30 12:21:52 -0500136 val newUser =
137 UserHandle.of(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, sendingUserId))
138 if (currentUser == newUser) {
139 userChanging = false
140 return
141 }
142 setValuesForUser(newUser)
143 }
144 }
145 }
146
Fabian Kozynski8b540452020-02-04 15:16:30 -0500147 @VisibleForTesting
Fabian Kozynski66e97542020-03-11 15:49:19 -0400148 internal val restoreFinishedReceiver = object : BroadcastReceiver() {
149 override fun onReceive(context: Context, intent: Intent) {
150 val user = intent.getIntExtra(Intent.EXTRA_USER_ID, UserHandle.USER_NULL)
151 if (user == currentUserId) {
152 executor.execute {
153 auxiliaryPersistenceWrapper.initialize()
154 listingController.removeCallback(listingCallback)
155 persistenceWrapper.storeFavorites(auxiliaryPersistenceWrapper.favorites)
156 resetFavorites(available)
157 }
158 }
159 }
160 }
161
162 @VisibleForTesting
Fabian Kozynski8b540452020-02-04 15:16:30 -0500163 internal val settingObserver = object : ContentObserver(null) {
Fabian Kozynskicaf76d22020-03-06 18:07:43 -0500164 override fun onChange(
165 selfChange: Boolean,
Jeff Sharkey8b0cff72020-03-09 15:49:01 -0600166 uris: Collection<Uri>,
Fabian Kozynskicaf76d22020-03-06 18:07:43 -0500167 flags: Int,
168 userId: Int
169 ) {
Fabian Kozynski8b540452020-02-04 15:16:30 -0500170 // Do not listen to changes in the middle of user change, those will be read by the
171 // user-switch receiver.
172 if (userChanging || userId != currentUserId) {
173 return
174 }
175 available = Settings.Secure.getIntForUser(contentResolver, CONTROLS_AVAILABLE,
Fabian Kozynskicaf76d22020-03-06 18:07:43 -0500176 DEFAULT_ENABLED, currentUserId) != 0
Matt Pietal313f37d2020-02-24 11:27:22 -0500177 resetFavorites(available)
Fabian Kozynski8b540452020-02-04 15:16:30 -0500178 }
179 }
180
Fabian Kozynski84371de2020-02-27 10:58:25 -0500181 // Handling of removed components
182
183 /**
184 * Check if any component has been removed and if so, remove all its favorites.
185 *
186 * If some component has been removed, the new set of favorites will also be saved.
187 */
188 private val listingCallback = object : ControlsListingController.ControlsListingCallback {
Matt Pietal638253a2020-03-02 09:10:43 -0500189 override fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>) {
Fabian Kozynski84371de2020-02-27 10:58:25 -0500190 executor.execute {
Matt Pietal638253a2020-03-02 09:10:43 -0500191 val serviceInfoSet = serviceInfos.map(ControlsServiceInfo::componentName).toSet()
192 val favoriteComponentSet = Favorites.getAllStructures().map {
193 it.componentName
194 }.toSet()
195
196 var changed = false
197 favoriteComponentSet.subtract(serviceInfoSet).forEach {
198 changed = true
199 Favorites.removeStructures(it)
200 bindingController.onComponentRemoved(it)
201 }
202
Fabian Kozynski66e97542020-03-11 15:49:19 -0400203 if (auxiliaryPersistenceWrapper.favorites.isNotEmpty()) {
204 serviceInfoSet.subtract(favoriteComponentSet).forEach {
205 val toAdd = auxiliaryPersistenceWrapper.getCachedFavoritesAndRemoveFor(it)
206 if (toAdd.isNotEmpty()) {
207 changed = true
208 toAdd.forEach {
209 Favorites.replaceControls(it)
210 }
211 }
212 }
213 // Need to clear the ones that were restored immediately. This will delete
214 // them from the auxiliary file if they were not deleted. Should only do any
215 // work the first time after a restore.
216 serviceInfoSet.intersect(favoriteComponentSet).forEach {
217 auxiliaryPersistenceWrapper.getCachedFavoritesAndRemoveFor(it)
218 }
219 }
220
221 // Check if something has been added or removed, if so, store the new list
Matt Pietal638253a2020-03-02 09:10:43 -0500222 if (changed) {
223 persistenceWrapper.storeFavorites(Favorites.getAllStructures())
Fabian Kozynski84371de2020-02-27 10:58:25 -0500224 }
225 }
226 }
227 }
228
Fabian Kozynski7988bd42020-01-30 12:21:52 -0500229 init {
Ned Burnsaaeb44b2020-02-12 23:48:26 -0500230 dumpManager.registerDumpable(javaClass.name, this)
Matt Pietal313f37d2020-02-24 11:27:22 -0500231 resetFavorites(available)
Fabian Kozynski7988bd42020-01-30 12:21:52 -0500232 userChanging = false
233 broadcastDispatcher.registerReceiver(
234 userSwitchReceiver,
235 IntentFilter(Intent.ACTION_USER_SWITCHED),
236 executor,
237 UserHandle.ALL
238 )
Fabian Kozynski66e97542020-03-11 15:49:19 -0400239 context.registerReceiver(
240 restoreFinishedReceiver,
241 IntentFilter(BackupHelper.ACTION_RESTORE_FINISHED),
242 PERMISSION_SELF,
243 null
244 )
Fabian Kozynski8b540452020-02-04 15:16:30 -0500245 contentResolver.registerContentObserver(URI, false, settingObserver, UserHandle.USER_ALL)
Fabian Kozynski7988bd42020-01-30 12:21:52 -0500246 }
247
Fabian Kozynski66e97542020-03-11 15:49:19 -0400248 fun destroy() {
249 broadcastDispatcher.unregisterReceiver(userSwitchReceiver)
250 context.unregisterReceiver(restoreFinishedReceiver)
251 contentResolver.unregisterContentObserver(settingObserver)
252 listingController.removeCallback(listingCallback)
253 }
254
Matt Pietal313f37d2020-02-24 11:27:22 -0500255 private fun resetFavorites(shouldLoad: Boolean) {
256 Favorites.clear()
257
258 if (shouldLoad) {
259 Favorites.load(persistenceWrapper.readFavorites())
Matt Pietal638253a2020-03-02 09:10:43 -0500260 listingController.addCallback(listingCallback)
Matt Pietal313f37d2020-02-24 11:27:22 -0500261 }
262 }
263
Fabian Kozynski7988bd42020-01-30 12:21:52 -0500264 private fun confirmAvailability(): Boolean {
265 if (userChanging) {
266 Log.w(TAG, "Controls not available while user is changing")
267 return false
268 }
269 if (!available) {
270 Log.d(TAG, "Controls not available")
271 return false
272 }
273 return true
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500274 }
275
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500276 override fun loadForComponent(
277 componentName: ComponentName,
Fabian Kozynski9c459e52020-02-12 09:08:15 -0500278 dataCallback: Consumer<ControlsController.LoadData>
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500279 ) {
Fabian Kozynski7988bd42020-01-30 12:21:52 -0500280 if (!confirmAvailability()) {
281 if (userChanging) {
282 // Try again later, userChanging should not last forever. If so, we have bigger
Fabian Kozynski1a6a7942020-03-10 11:19:04 -0400283 // problems. This will return a runnable that allows to cancel the delayed version,
284 // it will not be able to cancel the load if
285 loadCanceller = executor.executeDelayed(
Fabian Kozynski9c459e52020-02-12 09:08:15 -0500286 { loadForComponent(componentName, dataCallback) },
Fabian Kozynski7988bd42020-01-30 12:21:52 -0500287 USER_CHANGE_RETRY_DELAY,
288 TimeUnit.MILLISECONDS
289 )
290 } else {
Fabian Kozynski9c459e52020-02-12 09:08:15 -0500291 dataCallback.accept(createLoadDataObject(emptyList(), emptyList(), true))
Fabian Kozynski7988bd42020-01-30 12:21:52 -0500292 }
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500293 return
294 }
Fabian Kozynski1a6a7942020-03-10 11:19:04 -0400295 loadCanceller = bindingController.bindAndLoad(
Fabian Kozynski9c459e52020-02-12 09:08:15 -0500296 componentName,
297 object : ControlsBindingController.LoadCallback {
298 override fun accept(controls: List<Control>) {
Fabian Kozynski1a6a7942020-03-10 11:19:04 -0400299 loadCanceller = null
Matt Pietal638253a2020-03-02 09:10:43 -0500300 executor.execute {
301 val favoritesForComponentKeys = Favorites
302 .getControlsForComponent(componentName).map { it.controlId }
Matt Pietal313f37d2020-02-24 11:27:22 -0500303
Matt Pietal638253a2020-03-02 09:10:43 -0500304 val changed = Favorites.updateControls(componentName, controls)
305 if (changed) {
306 persistenceWrapper.storeFavorites(Favorites.getAllStructures())
307 }
308 val removed = findRemoved(favoritesForComponentKeys.toSet(), controls)
309 val controlsWithFavorite = controls.map {
Matt Pietal53a8bbd2020-03-05 16:10:34 -0500310 ControlStatus(
311 it,
312 componentName,
313 it.controlId in favoritesForComponentKeys
314 )
Matt Pietal638253a2020-03-02 09:10:43 -0500315 }
Fabian Kozynskia9803042020-03-26 12:07:21 -0400316 val removedControls = mutableListOf<ControlStatus>()
317 Favorites.getStructuresForComponent(componentName).forEach { st ->
318 st.controls.forEach {
319 if (it.controlId in removed) {
320 val r = createRemovedStatus(componentName, it, st.structure)
321 removedControls.add(r)
322 }
323 }
324 }
Matt Pietal638253a2020-03-02 09:10:43 -0500325 val loadData = createLoadDataObject(
Fabian Kozynskia9803042020-03-26 12:07:21 -0400326 removedControls +
Matt Pietal638253a2020-03-02 09:10:43 -0500327 controlsWithFavorite,
328 favoritesForComponentKeys
329 )
Matt Pietal638253a2020-03-02 09:10:43 -0500330 dataCallback.accept(loadData)
331 }
Fabian Kozynski9c459e52020-02-12 09:08:15 -0500332 }
333
334 override fun error(message: String) {
Fabian Kozynski1a6a7942020-03-10 11:19:04 -0400335 loadCanceller = null
Fabian Kozynski713b7272020-03-03 18:35:52 -0500336 executor.execute {
Fabian Kozynskia9803042020-03-26 12:07:21 -0400337 val controls = Favorites.getStructuresForComponent(componentName)
338 .flatMap { st ->
339 st.controls.map {
340 createRemovedStatus(componentName, it, st.structure,
341 false)
342 }
343 }
344 val keys = controls.map { it.control.controlId }
345 val loadData = createLoadDataObject(controls, keys, true)
Fabian Kozynski713b7272020-03-03 18:35:52 -0500346 dataCallback.accept(loadData)
347 }
Fabian Kozynski9c459e52020-02-12 09:08:15 -0500348 }
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500349 }
Fabian Kozynski9c459e52020-02-12 09:08:15 -0500350 )
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500351 }
352
Matt Pietal61266442020-03-17 12:53:44 -0400353 override fun addSeedingFavoritesCallback(callback: Consumer<Boolean>): Boolean {
354 if (!seedingInProgress) return false
355 executor.execute {
356 // status may have changed by this point, so check again and inform the
357 // caller if necessary
358 if (seedingInProgress) seedingCallbacks.add(callback)
359 else callback.accept(false)
360 }
361 return true
362 }
363
364 override fun seedFavoritesForComponent(
365 componentName: ComponentName,
366 callback: Consumer<Boolean>
367 ) {
Matt Pietalcd757c82020-04-08 10:20:48 -0400368 if (seedingInProgress) return
369
Matt Pietal61266442020-03-17 12:53:44 -0400370 Log.i(TAG, "Beginning request to seed favorites for: $componentName")
371 if (!confirmAvailability()) {
372 if (userChanging) {
373 // Try again later, userChanging should not last forever. If so, we have bigger
374 // problems. This will return a runnable that allows to cancel the delayed version,
375 // it will not be able to cancel the load if
376 executor.executeDelayed(
377 { seedFavoritesForComponent(componentName, callback) },
378 USER_CHANGE_RETRY_DELAY,
379 TimeUnit.MILLISECONDS
380 )
381 } else {
382 callback.accept(false)
383 }
384 return
385 }
386 seedingInProgress = true
387 bindingController.bindAndLoadSuggested(
388 componentName,
389 object : ControlsBindingController.LoadCallback {
390 override fun accept(controls: List<Control>) {
391 executor.execute {
392 val structureToControls =
393 ArrayMap<CharSequence, MutableList<ControlInfo>>()
394
395 controls.forEach {
396 val structure = it.structure ?: ""
397 val list = structureToControls.get(structure)
Matt Pietal85878262020-03-18 15:34:46 -0400398 ?: mutableListOf<ControlInfo>()
399 list.add(
400 ControlInfo(it.controlId, it.title, it.subtitle, it.deviceType))
Matt Pietal61266442020-03-17 12:53:44 -0400401 structureToControls.put(structure, list)
402 }
403
404 structureToControls.forEach {
405 (s, cs) -> Favorites.replaceControls(
406 StructureInfo(componentName, s, cs))
407 }
408
409 persistenceWrapper.storeFavorites(Favorites.getAllStructures())
410 callback.accept(true)
411 endSeedingCall(true)
412 }
413 }
414
415 override fun error(message: String) {
416 Log.e(TAG, "Unable to seed favorites: $message")
417 executor.execute {
418 callback.accept(false)
419 endSeedingCall(false)
420 }
421 }
422 }
423 )
424 }
425
426 private fun endSeedingCall(state: Boolean) {
427 seedingInProgress = false
428 seedingCallbacks.forEach {
429 it.accept(state)
430 }
431 seedingCallbacks.clear()
432 }
433
Fabian Kozynski1a6a7942020-03-10 11:19:04 -0400434 override fun cancelLoad() {
435 loadCanceller?.let {
436 executor.execute(it)
437 }
438 }
439
Fabian Kozynski9c459e52020-02-12 09:08:15 -0500440 private fun createRemovedStatus(
Matt Pietal313f37d2020-02-24 11:27:22 -0500441 componentName: ComponentName,
Fabian Kozynski9c459e52020-02-12 09:08:15 -0500442 controlInfo: ControlInfo,
Fabian Kozynskia9803042020-03-26 12:07:21 -0400443 structure: CharSequence,
Fabian Kozynski9c459e52020-02-12 09:08:15 -0500444 setRemoved: Boolean = true
445 ): ControlStatus {
446 val intent = Intent(Intent.ACTION_MAIN).apply {
447 addCategory(Intent.CATEGORY_LAUNCHER)
Matt Pietal313f37d2020-02-24 11:27:22 -0500448 this.`package` = componentName.packageName
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500449 }
450 val pendingIntent = PendingIntent.getActivity(context,
Matt Pietal313f37d2020-02-24 11:27:22 -0500451 componentName.hashCode(),
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500452 intent,
453 0)
454 val control = Control.StatelessBuilder(controlInfo.controlId, pendingIntent)
455 .setTitle(controlInfo.controlTitle)
Fabian Kozynskia9803042020-03-26 12:07:21 -0400456 .setSubtitle(controlInfo.controlSubtitle)
457 .setStructure(structure)
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500458 .setDeviceType(controlInfo.deviceType)
459 .build()
Matt Pietal53a8bbd2020-03-05 16:10:34 -0500460 return ControlStatus(control, componentName, true, setRemoved)
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500461 }
462
Matt Pietal638253a2020-03-02 09:10:43 -0500463 private fun findRemoved(favoriteKeys: Set<String>, list: List<Control>): Set<String> {
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500464 val controlsKeys = list.map { it.controlId }
465 return favoriteKeys.minus(controlsKeys)
466 }
467
Matt Pietal313f37d2020-02-24 11:27:22 -0500468 override fun subscribeToFavorites(structureInfo: StructureInfo) {
Fabian Kozynski7988bd42020-01-30 12:21:52 -0500469 if (!confirmAvailability()) return
Matt Pietal313f37d2020-02-24 11:27:22 -0500470
471 bindingController.subscribe(structureInfo)
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500472 }
473
474 override fun unsubscribe() {
Fabian Kozynski7988bd42020-01-30 12:21:52 -0500475 if (!confirmAvailability()) return
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500476 bindingController.unsubscribe()
477 }
478
Fabian Kozynski04e7bde2020-02-13 13:02:33 -0500479 override fun addFavorite(
480 componentName: ComponentName,
481 structureName: CharSequence,
482 controlInfo: ControlInfo
483 ) {
484 if (!confirmAvailability()) return
485 executor.execute {
486 if (Favorites.addFavorite(componentName, structureName, controlInfo)) {
487 persistenceWrapper.storeFavorites(Favorites.getAllStructures())
488 }
489 }
490 }
491
Matt Pietal313f37d2020-02-24 11:27:22 -0500492 override fun replaceFavoritesForStructure(structureInfo: StructureInfo) {
Fabian Kozynski7988bd42020-01-30 12:21:52 -0500493 if (!confirmAvailability()) return
Matt Pietal638253a2020-03-02 09:10:43 -0500494 executor.execute {
495 Favorites.replaceControls(structureInfo)
496 persistenceWrapper.storeFavorites(Favorites.getAllStructures())
497 }
Fabian Kozynski9aa23af2020-02-05 17:47:47 -0500498 }
499
Matt Pietalcd757c82020-04-08 10:20:48 -0400500 override fun resetFavorites() {
501 executor.execute {
502 Favorites.clear()
503 persistenceWrapper.storeFavorites(Favorites.getAllStructures())
504 }
505 }
506
Fabian Kozynski1bb26b52020-01-08 18:20:36 -0500507 override fun refreshStatus(componentName: ComponentName, control: Control) {
Fabian Kozynski7988bd42020-01-30 12:21:52 -0500508 if (!confirmAvailability()) {
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500509 Log.d(TAG, "Controls not available")
510 return
511 }
Matt Pietal1a209db2020-03-27 11:58:24 -0400512
513 // Assume that non STATUS_OK responses may contain incomplete or invalid information about
514 // the control, and do not attempt to update it
515 if (control.getStatus() == Control.STATUS_OK) {
516 executor.execute {
517 if (Favorites.updateControls(componentName, listOf(control))) {
518 persistenceWrapper.storeFavorites(Favorites.getAllStructures())
519 }
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500520 }
521 }
Fabian Kozynski1bb26b52020-01-08 18:20:36 -0500522 uiController.onRefreshState(componentName, listOf(control))
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500523 }
524
525 override fun onActionResponse(componentName: ComponentName, controlId: String, response: Int) {
Fabian Kozynski7988bd42020-01-30 12:21:52 -0500526 if (!confirmAvailability()) return
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500527 uiController.onActionResponse(componentName, controlId, response)
528 }
529
Matt Pietal313f37d2020-02-24 11:27:22 -0500530 override fun action(
531 componentName: ComponentName,
532 controlInfo: ControlInfo,
533 action: ControlAction
534 ) {
Fabian Kozynski7988bd42020-01-30 12:21:52 -0500535 if (!confirmAvailability()) return
Matt Pietal313f37d2020-02-24 11:27:22 -0500536 bindingController.action(componentName, controlInfo, action)
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500537 }
538
Matt Pietal313f37d2020-02-24 11:27:22 -0500539 override fun getFavorites(): List<StructureInfo> = Favorites.getAllStructures()
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500540
Matt Pietal313f37d2020-02-24 11:27:22 -0500541 override fun countFavoritesForComponent(componentName: ComponentName): Int =
542 Favorites.getControlsForComponent(componentName).size
Fabian Kozynski5fc5f6b2020-02-03 15:21:14 -0500543
Matt Pietal313f37d2020-02-24 11:27:22 -0500544 override fun getFavoritesForComponent(componentName: ComponentName): List<StructureInfo> =
545 Favorites.getStructuresForComponent(componentName)
Fabian Kozynski9aa23af2020-02-05 17:47:47 -0500546
Fabian Kozynski8765d352020-04-06 21:16:02 -0400547 override fun getFavoritesForStructure(
548 componentName: ComponentName,
549 structureName: CharSequence
550 ): List<ControlInfo> {
551 return Favorites.getControlsForStructure(
552 StructureInfo(componentName, structureName, emptyList())
553 )
554 }
555
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500556 override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<out String>) {
557 pw.println("ControlsController state:")
Fabian Kozynski7988bd42020-01-30 12:21:52 -0500558 pw.println(" Available: $available")
559 pw.println(" Changing users: $userChanging")
560 pw.println(" Current user: ${currentUser.identifier}")
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500561 pw.println(" Favorites:")
Matt Pietal313f37d2020-02-24 11:27:22 -0500562 Favorites.getAllStructures().forEach { s ->
563 pw.println(" ${ s }")
564 s.controls.forEach { c ->
565 pw.println(" ${ c }")
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500566 }
567 }
Fabian Kozynski7988bd42020-01-30 12:21:52 -0500568 pw.println(bindingController.toString())
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500569 }
Matt Pietal313f37d2020-02-24 11:27:22 -0500570}
571
572/**
573 * Relies on immutable data for thread safety. When necessary to update favMap, use reassignment to
574 * replace it, which will not disrupt any ongoing map traversal.
Matt Pietal638253a2020-03-02 09:10:43 -0500575 *
576 * Update/replace calls should use thread isolation to avoid race conditions.
Matt Pietal313f37d2020-02-24 11:27:22 -0500577 */
578private object Favorites {
579 private var favMap = mapOf<ComponentName, List<StructureInfo>>()
580
581 fun getAllStructures(): List<StructureInfo> = favMap.flatMap { it.value }
582
583 fun getStructuresForComponent(componentName: ComponentName): List<StructureInfo> =
584 favMap.get(componentName) ?: emptyList()
585
586 fun getControlsForStructure(structure: StructureInfo): List<ControlInfo> =
587 getStructuresForComponent(structure.componentName)
588 .firstOrNull { it.structure == structure.structure }
589 ?.controls ?: emptyList()
590
591 fun getControlsForComponent(componentName: ComponentName): List<ControlInfo> =
592 getStructuresForComponent(componentName).flatMap { it.controls }
593
594 fun load(structures: List<StructureInfo>) {
595 favMap = structures.groupBy { it.componentName }
596 }
597
598 fun updateControls(componentName: ComponentName, controls: List<Control>): Boolean {
599 val controlsById = controls.associateBy { it.controlId }
600
601 // utilize a new map to allow for changes to structure names
602 val structureToControls = mutableMapOf<CharSequence, MutableList<ControlInfo>>()
603
604 // Must retain the current control order within each structure
605 var changed = false
606 getStructuresForComponent(componentName).forEach { s ->
607 s.controls.forEach { c ->
608 val (sName, ci) = controlsById.get(c.controlId)?.let { updatedControl ->
609 val controlInfo = if (updatedControl.title != c.controlTitle ||
Matt Pietal85878262020-03-18 15:34:46 -0400610 updatedControl.subtitle != c.controlSubtitle ||
Matt Pietal313f37d2020-02-24 11:27:22 -0500611 updatedControl.deviceType != c.deviceType) {
612 changed = true
613 c.copy(
614 controlTitle = updatedControl.title,
Matt Pietal85878262020-03-18 15:34:46 -0400615 controlSubtitle = updatedControl.subtitle,
Matt Pietal313f37d2020-02-24 11:27:22 -0500616 deviceType = updatedControl.deviceType
617 )
618 } else { c }
619
620 val updatedStructure = updatedControl.structure ?: ""
621 if (s.structure != updatedStructure) {
622 changed = true
623 }
624
625 Pair(updatedStructure, controlInfo)
626 } ?: Pair(s.structure, c)
627
628 structureToControls.getOrPut(sName, { mutableListOf() }).add(ci)
629 }
630 }
631 if (!changed) return false
632
633 val structures = structureToControls.map { (s, cs) -> StructureInfo(componentName, s, cs) }
634
635 val newFavMap = favMap.toMutableMap()
636 newFavMap.put(componentName, structures)
Matt Pietal638253a2020-03-02 09:10:43 -0500637 favMap = newFavMap
Matt Pietal313f37d2020-02-24 11:27:22 -0500638
639 return true
640 }
641
Matt Pietal638253a2020-03-02 09:10:43 -0500642 fun removeStructures(componentName: ComponentName) {
643 val newFavMap = favMap.toMutableMap()
644 newFavMap.remove(componentName)
645 favMap = newFavMap
646 }
647
Fabian Kozynski04e7bde2020-02-13 13:02:33 -0500648 fun addFavorite(
649 componentName: ComponentName,
650 structureName: CharSequence,
651 controlInfo: ControlInfo
652 ): Boolean {
653 // Check if control is in favorites
654 if (getControlsForComponent(componentName)
655 .any { it.controlId == controlInfo.controlId }) {
656 return false
657 }
658 val structureInfo = favMap.get(componentName)
659 ?.firstOrNull { it.structure == structureName }
660 ?: StructureInfo(componentName, structureName, emptyList())
661 val newStructureInfo = structureInfo.copy(controls = structureInfo.controls + controlInfo)
662 replaceControls(newStructureInfo)
663 return true
664 }
665
Matt Pietal313f37d2020-02-24 11:27:22 -0500666 fun replaceControls(updatedStructure: StructureInfo) {
667 val newFavMap = favMap.toMutableMap()
668 val structures = mutableListOf<StructureInfo>()
669 val componentName = updatedStructure.componentName
670
671 var replaced = false
672 getStructuresForComponent(componentName).forEach { s ->
673 val newStructure = if (s.structure == updatedStructure.structure) {
674 replaced = true
675 updatedStructure
676 } else { s }
677
Matt Pietal567b0f62020-03-10 08:44:33 -0400678 if (!newStructure.controls.isEmpty()) {
679 structures.add(newStructure)
680 }
Matt Pietal313f37d2020-02-24 11:27:22 -0500681 }
682
Matt Pietal567b0f62020-03-10 08:44:33 -0400683 if (!replaced && !updatedStructure.controls.isEmpty()) {
Matt Pietal313f37d2020-02-24 11:27:22 -0500684 structures.add(updatedStructure)
685 }
686
Fabian Kozynski04e7bde2020-02-13 13:02:33 -0500687 newFavMap.put(componentName, structures)
688 favMap = newFavMap
Matt Pietal313f37d2020-02-24 11:27:22 -0500689 }
690
691 fun clear() {
692 favMap = mapOf<ComponentName, List<StructureInfo>>()
693 }
694}