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