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