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