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