• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * 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 
17 package com.android.systemui.controls.controller
18 
19 import android.app.ActivityManager
20 import android.app.PendingIntent
21 import android.app.backup.BackupManager
22 import android.content.BroadcastReceiver
23 import android.content.ComponentName
24 import android.content.ContentResolver
25 import android.content.Context
26 import android.content.Intent
27 import android.content.IntentFilter
28 import android.database.ContentObserver
29 import android.net.Uri
30 import android.os.Environment
31 import android.os.UserHandle
32 import android.provider.Settings
33 import android.service.controls.Control
34 import android.service.controls.actions.ControlAction
35 import android.util.ArrayMap
36 import android.util.Log
37 import com.android.internal.annotations.VisibleForTesting
38 import com.android.systemui.Dumpable
39 import com.android.systemui.backup.BackupHelper
40 import com.android.systemui.broadcast.BroadcastDispatcher
41 import com.android.systemui.controls.ControlStatus
42 import com.android.systemui.controls.ControlsServiceInfo
43 import com.android.systemui.controls.management.ControlsListingController
44 import com.android.systemui.controls.ui.ControlsUiController
45 import com.android.systemui.dagger.qualifiers.Background
46 import com.android.systemui.dump.DumpManager
47 import com.android.systemui.globalactions.GlobalActionsDialog
48 import com.android.systemui.util.concurrency.DelayableExecutor
49 import java.io.FileDescriptor
50 import java.io.PrintWriter
51 import java.util.Optional
52 import java.util.concurrent.TimeUnit
53 import java.util.function.Consumer
54 import javax.inject.Inject
55 import javax.inject.Singleton
56 
57 @Singleton
58 class ControlsControllerImpl @Inject constructor (
59     private val context: Context,
60     @Background private val executor: DelayableExecutor,
61     private val uiController: ControlsUiController,
62     private val bindingController: ControlsBindingController,
63     private val listingController: ControlsListingController,
64     private val broadcastDispatcher: BroadcastDispatcher,
65     optionalWrapper: Optional<ControlsFavoritePersistenceWrapper>,
66     dumpManager: DumpManager
67 ) : Dumpable, ControlsController {
68 
69     companion object {
70         private const val TAG = "ControlsControllerImpl"
71         internal const val CONTROLS_AVAILABLE = Settings.Secure.CONTROLS_ENABLED
72         internal val URI = Settings.Secure.getUriFor(CONTROLS_AVAILABLE)
73         private const val USER_CHANGE_RETRY_DELAY = 500L // ms
74         private const val DEFAULT_ENABLED = 1
75         private const val PERMISSION_SELF = "com.android.systemui.permission.SELF"
76         const val SUGGESTED_CONTROLS_PER_STRUCTURE = 6
77 
78         private fun isAvailable(userId: Int, cr: ContentResolver) = Settings.Secure.getIntForUser(
79             cr, CONTROLS_AVAILABLE, DEFAULT_ENABLED, userId) != 0
80     }
81 
82     private var userChanging: Boolean = true
83     private var userStructure: UserStructure
84 
85     private var seedingInProgress = false
86     private val seedingCallbacks = mutableListOf<Consumer<Boolean>>()
87 
88     private var currentUser = UserHandle.of(ActivityManager.getCurrentUser())
89     override val currentUserId
90         get() = currentUser.identifier
91 
92     private val contentResolver: ContentResolver
93         get() = context.contentResolver
94     override var available = isAvailable(currentUserId, contentResolver)
95         private set
96 
97     private val persistenceWrapper: ControlsFavoritePersistenceWrapper
98     @VisibleForTesting
99     internal var auxiliaryPersistenceWrapper: AuxiliaryPersistenceWrapper
100 
101     init {
102         userStructure = UserStructure(context, currentUser)
103 
104         persistenceWrapper = optionalWrapper.orElseGet {
105             ControlsFavoritePersistenceWrapper(
106                     userStructure.file,
107                     executor,
108                     BackupManager(userStructure.userContext)
109             )
110         }
111 
112         auxiliaryPersistenceWrapper = AuxiliaryPersistenceWrapper(
113                 userStructure.auxiliaryFile,
114                 executor
115         )
116     }
117 
118     private fun setValuesForUser(newUser: UserHandle) {
119         Log.d(TAG, "Changing to user: $newUser")
120         currentUser = newUser
121         userStructure = UserStructure(context, currentUser)
122         persistenceWrapper.changeFileAndBackupManager(
123                 userStructure.file,
124                 BackupManager(userStructure.userContext)
125         )
126         auxiliaryPersistenceWrapper.changeFile(userStructure.auxiliaryFile)
127         available = isAvailable(newUser.identifier, contentResolver)
128         resetFavorites(available)
129         bindingController.changeUser(newUser)
130         listingController.changeUser(newUser)
131         userChanging = false
132     }
133 
134     private val userSwitchReceiver = object : BroadcastReceiver() {
135         override fun onReceive(context: Context, intent: Intent) {
136             if (intent.action == Intent.ACTION_USER_SWITCHED) {
137                 userChanging = true
138                 val newUser =
139                         UserHandle.of(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, sendingUserId))
140                 if (currentUser == newUser) {
141                     userChanging = false
142                     return
143                 }
144                 setValuesForUser(newUser)
145             }
146         }
147     }
148 
149     @VisibleForTesting
150     internal val restoreFinishedReceiver = object : BroadcastReceiver() {
151         override fun onReceive(context: Context, intent: Intent) {
152             val user = intent.getIntExtra(Intent.EXTRA_USER_ID, UserHandle.USER_NULL)
153             if (user == currentUserId) {
154                 executor.execute {
155                     Log.d(TAG, "Restore finished, storing auxiliary favorites")
156                     auxiliaryPersistenceWrapper.initialize()
157                     persistenceWrapper.storeFavorites(auxiliaryPersistenceWrapper.favorites)
158                     resetFavorites(available)
159                 }
160             }
161         }
162     }
163 
164     @VisibleForTesting
165     internal val settingObserver = object : ContentObserver(null) {
166         override fun onChange(
167             selfChange: Boolean,
168             uris: Collection<Uri>,
169             flags: Int,
170             userId: Int
171         ) {
172             // Do not listen to changes in the middle of user change, those will be read by the
173             // user-switch receiver.
174             if (userChanging || userId != currentUserId) {
175                 return
176             }
177             available = isAvailable(currentUserId, contentResolver)
178             resetFavorites(available)
179         }
180     }
181 
182     // 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 {
190         override fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>) {
191             executor.execute {
192                 val serviceInfoSet = serviceInfos.map(ControlsServiceInfo::componentName).toSet()
193                 val favoriteComponentSet = Favorites.getAllStructures().map {
194                     it.componentName
195                 }.toSet()
196 
197                 // When a component is uninstalled, allow seeding to happen again if the user
198                 // reinstalls the app
199                 val prefs = userStructure.userContext.getSharedPreferences(
200                     GlobalActionsDialog.PREFS_CONTROLS_FILE, Context.MODE_PRIVATE)
201                 val completedSeedingPackageSet = prefs.getStringSet(
202                     GlobalActionsDialog.PREFS_CONTROLS_SEEDING_COMPLETED, mutableSetOf<String>())
203                 val servicePackageSet = serviceInfoSet.map { it.packageName }
204                 prefs.edit().putStringSet(GlobalActionsDialog.PREFS_CONTROLS_SEEDING_COMPLETED,
205                     completedSeedingPackageSet.intersect(servicePackageSet)).apply()
206 
207                 var changed = false
208                 favoriteComponentSet.subtract(serviceInfoSet).forEach {
209                     changed = true
210                     Favorites.removeStructures(it)
211                     bindingController.onComponentRemoved(it)
212                 }
213 
214                 if (auxiliaryPersistenceWrapper.favorites.isNotEmpty()) {
215                     serviceInfoSet.subtract(favoriteComponentSet).forEach {
216                         val toAdd = auxiliaryPersistenceWrapper.getCachedFavoritesAndRemoveFor(it)
217                         if (toAdd.isNotEmpty()) {
218                             changed = true
219                             toAdd.forEach {
220                                 Favorites.replaceControls(it)
221                             }
222                         }
223                     }
224                     // Need to clear the ones that were restored immediately. This will delete
225                     // them from the auxiliary file if they were not deleted. Should only do any
226                     // work the first time after a restore.
227                     serviceInfoSet.intersect(favoriteComponentSet).forEach {
228                         auxiliaryPersistenceWrapper.getCachedFavoritesAndRemoveFor(it)
229                     }
230                 }
231 
232                 // Check if something has been added or removed, if so, store the new list
233                 if (changed) {
234                     Log.d(TAG, "Detected change in available services, storing updated favorites")
235                     persistenceWrapper.storeFavorites(Favorites.getAllStructures())
236                 }
237             }
238         }
239     }
240 
241     init {
242         dumpManager.registerDumpable(javaClass.name, this)
243         resetFavorites(available)
244         userChanging = false
245         broadcastDispatcher.registerReceiver(
246                 userSwitchReceiver,
247                 IntentFilter(Intent.ACTION_USER_SWITCHED),
248                 executor,
249                 UserHandle.ALL
250         )
251         context.registerReceiver(
252             restoreFinishedReceiver,
253             IntentFilter(BackupHelper.ACTION_RESTORE_FINISHED),
254             PERMISSION_SELF,
255             null
256         )
257         contentResolver.registerContentObserver(URI, false, settingObserver, UserHandle.USER_ALL)
258         listingController.addCallback(listingCallback)
259     }
260 
261     fun destroy() {
262         broadcastDispatcher.unregisterReceiver(userSwitchReceiver)
263         context.unregisterReceiver(restoreFinishedReceiver)
264         contentResolver.unregisterContentObserver(settingObserver)
265         listingController.removeCallback(listingCallback)
266     }
267 
268     private fun resetFavorites(shouldLoad: Boolean) {
269         Favorites.clear()
270 
271         if (shouldLoad) {
272             Favorites.load(persistenceWrapper.readFavorites())
273         }
274     }
275 
276     private fun confirmAvailability(): Boolean {
277         if (userChanging) {
278             Log.w(TAG, "Controls not available while user is changing")
279             return false
280         }
281         if (!available) {
282             Log.d(TAG, "Controls not available")
283             return false
284         }
285         return true
286     }
287 
288     override fun loadForComponent(
289         componentName: ComponentName,
290         dataCallback: Consumer<ControlsController.LoadData>,
291         cancelWrapper: Consumer<Runnable>
292     ) {
293         if (!confirmAvailability()) {
294             if (userChanging) {
295                 // Try again later, userChanging should not last forever. If so, we have bigger
296                 // problems. This will return a runnable that allows to cancel the delayed version,
297                 // it will not be able to cancel the load if
298                 executor.executeDelayed(
299                     { loadForComponent(componentName, dataCallback, cancelWrapper) },
300                     USER_CHANGE_RETRY_DELAY,
301                     TimeUnit.MILLISECONDS
302                 )
303             }
304 
305             dataCallback.accept(createLoadDataObject(emptyList(), emptyList(), true))
306         }
307 
308         cancelWrapper.accept(
309             bindingController.bindAndLoad(
310                 componentName,
311                 object : ControlsBindingController.LoadCallback {
312                     override fun accept(controls: List<Control>) {
313                         executor.execute {
314                             val favoritesForComponentKeys = Favorites
315                                 .getControlsForComponent(componentName).map { it.controlId }
316 
317                             val changed = Favorites.updateControls(componentName, controls)
318                             if (changed) {
319                                 persistenceWrapper.storeFavorites(Favorites.getAllStructures())
320                             }
321                             val removed = findRemoved(favoritesForComponentKeys.toSet(), controls)
322                             val controlsWithFavorite = controls.map {
323                                 ControlStatus(
324                                     it,
325                                     componentName,
326                                     it.controlId in favoritesForComponentKeys
327                                 )
328                             }
329                             val removedControls = mutableListOf<ControlStatus>()
330                             Favorites.getStructuresForComponent(componentName).forEach { st ->
331                                 st.controls.forEach {
332                                     if (it.controlId in removed) {
333                                         val r = createRemovedStatus(componentName, it, st.structure)
334                                         removedControls.add(r)
335                                     }
336                                 }
337                             }
338                             val loadData = createLoadDataObject(
339                                 removedControls +
340                                 controlsWithFavorite,
341                                 favoritesForComponentKeys
342                             )
343                             dataCallback.accept(loadData)
344                         }
345                     }
346 
347                     override fun error(message: String) {
348                         executor.execute {
349                             val controls = Favorites.getStructuresForComponent(componentName)
350                                     .flatMap { st ->
351                                         st.controls.map {
352                                             createRemovedStatus(componentName, it, st.structure,
353                                                     false)
354                                         }
355                                     }
356                             val keys = controls.map { it.control.controlId }
357                             val loadData = createLoadDataObject(controls, keys, true)
358                             dataCallback.accept(loadData)
359                         }
360                     }
361                 }
362             )
363         )
364     }
365 
366     override fun addSeedingFavoritesCallback(callback: Consumer<Boolean>): Boolean {
367         if (!seedingInProgress) return false
368         executor.execute {
369             // status may have changed by this point, so check again and inform the
370             // caller if necessary
371             if (seedingInProgress) seedingCallbacks.add(callback)
372             else callback.accept(false)
373         }
374         return true
375     }
376 
377     override fun seedFavoritesForComponents(
378         componentNames: List<ComponentName>,
379         callback: Consumer<SeedResponse>
380     ) {
381         if (seedingInProgress || componentNames.isEmpty()) return
382 
383         if (!confirmAvailability()) {
384             if (userChanging) {
385                 // Try again later, userChanging should not last forever. If so, we have bigger
386                 // problems. This will return a runnable that allows to cancel the delayed version,
387                 // it will not be able to cancel the load if
388                 executor.executeDelayed(
389                     { seedFavoritesForComponents(componentNames, callback) },
390                     USER_CHANGE_RETRY_DELAY,
391                     TimeUnit.MILLISECONDS
392                 )
393             } else {
394                 componentNames.forEach {
395                     callback.accept(SeedResponse(it.packageName, false))
396                 }
397             }
398             return
399         }
400         seedingInProgress = true
401         startSeeding(componentNames, callback, false)
402     }
403 
404     private fun startSeeding(
405         remainingComponentNames: List<ComponentName>,
406         callback: Consumer<SeedResponse>,
407         didAnyFail: Boolean
408     ) {
409         if (remainingComponentNames.isEmpty()) {
410             endSeedingCall(!didAnyFail)
411             return
412         }
413 
414         val componentName = remainingComponentNames[0]
415         Log.d(TAG, "Beginning request to seed favorites for: $componentName")
416 
417         val remaining = remainingComponentNames.drop(1)
418         bindingController.bindAndLoadSuggested(
419             componentName,
420             object : ControlsBindingController.LoadCallback {
421                 override fun accept(controls: List<Control>) {
422                     executor.execute {
423                         val structureToControls =
424                             ArrayMap<CharSequence, MutableList<ControlInfo>>()
425 
426                         controls.forEach {
427                             val structure = it.structure ?: ""
428                             val list = structureToControls.get(structure)
429                                 ?: mutableListOf<ControlInfo>()
430                             if (list.size < SUGGESTED_CONTROLS_PER_STRUCTURE) {
431                                 list.add(
432                                     ControlInfo(it.controlId, it.title, it.subtitle, it.deviceType))
433                                 structureToControls.put(structure, list)
434                             }
435                         }
436 
437                         structureToControls.forEach {
438                             (s, cs) -> Favorites.replaceControls(
439                                 StructureInfo(componentName, s, cs))
440                         }
441 
442                         persistenceWrapper.storeFavorites(Favorites.getAllStructures())
443                         callback.accept(SeedResponse(componentName.packageName, true))
444                         startSeeding(remaining, callback, didAnyFail)
445                     }
446                 }
447 
448                 override fun error(message: String) {
449                     Log.e(TAG, "Unable to seed favorites: $message")
450                     executor.execute {
451                         callback.accept(SeedResponse(componentName.packageName, false))
452                         startSeeding(remaining, callback, true)
453                     }
454                 }
455             }
456         )
457     }
458 
459     private fun endSeedingCall(state: Boolean) {
460         seedingInProgress = false
461         seedingCallbacks.forEach {
462             it.accept(state)
463         }
464         seedingCallbacks.clear()
465     }
466 
467     private fun createRemovedStatus(
468         componentName: ComponentName,
469         controlInfo: ControlInfo,
470         structure: CharSequence,
471         setRemoved: Boolean = true
472     ): ControlStatus {
473         val intent = Intent(Intent.ACTION_MAIN).apply {
474             addCategory(Intent.CATEGORY_LAUNCHER)
475             this.`package` = componentName.packageName
476         }
477         val pendingIntent = PendingIntent.getActivity(context,
478                 componentName.hashCode(),
479                 intent,
480                 0)
481         val control = Control.StatelessBuilder(controlInfo.controlId, pendingIntent)
482                 .setTitle(controlInfo.controlTitle)
483                 .setSubtitle(controlInfo.controlSubtitle)
484                 .setStructure(structure)
485                 .setDeviceType(controlInfo.deviceType)
486                 .build()
487         return ControlStatus(control, componentName, true, setRemoved)
488     }
489 
490     private fun findRemoved(favoriteKeys: Set<String>, list: List<Control>): Set<String> {
491         val controlsKeys = list.map { it.controlId }
492         return favoriteKeys.minus(controlsKeys)
493     }
494 
495     override fun subscribeToFavorites(structureInfo: StructureInfo) {
496         if (!confirmAvailability()) return
497 
498         bindingController.subscribe(structureInfo)
499     }
500 
501     override fun unsubscribe() {
502         if (!confirmAvailability()) return
503         bindingController.unsubscribe()
504     }
505 
506     override fun addFavorite(
507         componentName: ComponentName,
508         structureName: CharSequence,
509         controlInfo: ControlInfo
510     ) {
511         if (!confirmAvailability()) return
512         executor.execute {
513             if (Favorites.addFavorite(componentName, structureName, controlInfo)) {
514                 persistenceWrapper.storeFavorites(Favorites.getAllStructures())
515             }
516         }
517     }
518 
519     override fun replaceFavoritesForStructure(structureInfo: StructureInfo) {
520         if (!confirmAvailability()) return
521         executor.execute {
522             Favorites.replaceControls(structureInfo)
523             persistenceWrapper.storeFavorites(Favorites.getAllStructures())
524         }
525     }
526 
527     override fun refreshStatus(componentName: ComponentName, control: Control) {
528         if (!confirmAvailability()) {
529             Log.d(TAG, "Controls not available")
530             return
531         }
532 
533         // Assume that non STATUS_OK responses may contain incomplete or invalid information about
534         // the control, and do not attempt to update it
535         if (control.getStatus() == Control.STATUS_OK) {
536             executor.execute {
537                 if (Favorites.updateControls(componentName, listOf(control))) {
538                     persistenceWrapper.storeFavorites(Favorites.getAllStructures())
539                 }
540             }
541         }
542         uiController.onRefreshState(componentName, listOf(control))
543     }
544 
545     override fun onActionResponse(componentName: ComponentName, controlId: String, response: Int) {
546         if (!confirmAvailability()) return
547         uiController.onActionResponse(componentName, controlId, response)
548     }
549 
550     override fun action(
551         componentName: ComponentName,
552         controlInfo: ControlInfo,
553         action: ControlAction
554     ) {
555         if (!confirmAvailability()) return
556         bindingController.action(componentName, controlInfo, action)
557     }
558 
559     override fun getFavorites(): List<StructureInfo> = Favorites.getAllStructures()
560 
561     override fun countFavoritesForComponent(componentName: ComponentName): Int =
562         Favorites.getControlsForComponent(componentName).size
563 
564     override fun getFavoritesForComponent(componentName: ComponentName): List<StructureInfo> =
565         Favorites.getStructuresForComponent(componentName)
566 
567     override fun getFavoritesForStructure(
568         componentName: ComponentName,
569         structureName: CharSequence
570     ): List<ControlInfo> {
571         return Favorites.getControlsForStructure(
572                 StructureInfo(componentName, structureName, emptyList())
573         )
574     }
575 
576     override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<out String>) {
577         pw.println("ControlsController state:")
578         pw.println("  Available: $available")
579         pw.println("  Changing users: $userChanging")
580         pw.println("  Current user: ${currentUser.identifier}")
581         pw.println("  Favorites:")
582         Favorites.getAllStructures().forEach { s ->
583             pw.println("    ${ s }")
584             s.controls.forEach { c ->
585                 pw.println("      ${ c }")
586             }
587         }
588         pw.println(bindingController.toString())
589     }
590 }
591 
592 class UserStructure(context: Context, user: UserHandle) {
593     val userContext = context.createContextAsUser(user, 0)
594 
595     val file = Environment.buildPath(
596             userContext.filesDir,
597             ControlsFavoritePersistenceWrapper.FILE_NAME
598     )
599 
600     val auxiliaryFile = Environment.buildPath(
601             userContext.filesDir,
602             AuxiliaryPersistenceWrapper.AUXILIARY_FILE_NAME
603     )
604 }
605 
606 /**
607  * Relies on immutable data for thread safety. When necessary to update favMap, use reassignment to
608  * replace it, which will not disrupt any ongoing map traversal.
609  *
610  * Update/replace calls should use thread isolation to avoid race conditions.
611  */
612 private object Favorites {
613     private var favMap = mapOf<ComponentName, List<StructureInfo>>()
614 
<lambda>null615     fun getAllStructures(): List<StructureInfo> = favMap.flatMap { it.value }
616 
getStructuresForComponentnull617     fun getStructuresForComponent(componentName: ComponentName): List<StructureInfo> =
618         favMap.get(componentName) ?: emptyList()
619 
620     fun getControlsForStructure(structure: StructureInfo): List<ControlInfo> =
621         getStructuresForComponent(structure.componentName)
622             .firstOrNull { it.structure == structure.structure }
623             ?.controls ?: emptyList()
624 
getControlsForComponentnull625     fun getControlsForComponent(componentName: ComponentName): List<ControlInfo> =
626         getStructuresForComponent(componentName).flatMap { it.controls }
627 
loadnull628     fun load(structures: List<StructureInfo>) {
629         favMap = structures.groupBy { it.componentName }
630     }
631 
updateControlsnull632     fun updateControls(componentName: ComponentName, controls: List<Control>): Boolean {
633         val controlsById = controls.associateBy { it.controlId }
634 
635         // utilize a new map to allow for changes to structure names
636         val structureToControls = mutableMapOf<CharSequence, MutableList<ControlInfo>>()
637 
638         // Must retain the current control order within each structure
639         var changed = false
640         getStructuresForComponent(componentName).forEach { s ->
641             s.controls.forEach { c ->
642                 val (sName, ci) = controlsById.get(c.controlId)?.let { updatedControl ->
643                     val controlInfo = if (updatedControl.title != c.controlTitle ||
644                         updatedControl.subtitle != c.controlSubtitle ||
645                         updatedControl.deviceType != c.deviceType) {
646                         changed = true
647                         c.copy(
648                             controlTitle = updatedControl.title,
649                             controlSubtitle = updatedControl.subtitle,
650                             deviceType = updatedControl.deviceType
651                         )
652                     } else { c }
653 
654                     val updatedStructure = updatedControl.structure ?: ""
655                     if (s.structure != updatedStructure) {
656                         changed = true
657                     }
658 
659                     Pair(updatedStructure, controlInfo)
660                 } ?: Pair(s.structure, c)
661 
662                 structureToControls.getOrPut(sName, { mutableListOf() }).add(ci)
663             }
664         }
665         if (!changed) return false
666 
667         val structures = structureToControls.map { (s, cs) -> StructureInfo(componentName, s, cs) }
668 
669         val newFavMap = favMap.toMutableMap()
670         newFavMap.put(componentName, structures)
671         favMap = newFavMap
672 
673         return true
674     }
675 
removeStructuresnull676     fun removeStructures(componentName: ComponentName) {
677         val newFavMap = favMap.toMutableMap()
678         newFavMap.remove(componentName)
679         favMap = newFavMap
680     }
681 
addFavoritenull682     fun addFavorite(
683         componentName: ComponentName,
684         structureName: CharSequence,
685         controlInfo: ControlInfo
686     ): Boolean {
687         // Check if control is in favorites
688         if (getControlsForComponent(componentName)
689                         .any { it.controlId == controlInfo.controlId }) {
690             return false
691         }
692         val structureInfo = favMap.get(componentName)
693                 ?.firstOrNull { it.structure == structureName }
694                 ?: StructureInfo(componentName, structureName, emptyList())
695         val newStructureInfo = structureInfo.copy(controls = structureInfo.controls + controlInfo)
696         replaceControls(newStructureInfo)
697         return true
698     }
699 
replaceControlsnull700     fun replaceControls(updatedStructure: StructureInfo) {
701         val newFavMap = favMap.toMutableMap()
702         val structures = mutableListOf<StructureInfo>()
703         val componentName = updatedStructure.componentName
704 
705         var replaced = false
706         getStructuresForComponent(componentName).forEach { s ->
707             val newStructure = if (s.structure == updatedStructure.structure) {
708                 replaced = true
709                 updatedStructure
710             } else { s }
711 
712             if (!newStructure.controls.isEmpty()) {
713                 structures.add(newStructure)
714             }
715         }
716 
717         if (!replaced && !updatedStructure.controls.isEmpty()) {
718             structures.add(updatedStructure)
719         }
720 
721         newFavMap.put(componentName, structures)
722         favMap = newFavMap
723     }
724 
clearnull725     fun clear() {
726         favMap = mapOf<ComponentName, List<StructureInfo>>()
727     }
728 }
729