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