1 /* <lambda>null2 * Copyright (C) 2024 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 package com.android.launcher3.model 17 18 import android.content.Context 19 import android.database.sqlite.SQLiteDatabase 20 import android.graphics.Point 21 import android.util.Log 22 import androidx.annotation.VisibleForTesting 23 import com.android.launcher3.Flags 24 import com.android.launcher3.LauncherPrefs 25 import com.android.launcher3.LauncherPrefs.Companion.get 26 import com.android.launcher3.LauncherPrefs.Companion.getPrefs 27 import com.android.launcher3.LauncherSettings 28 import com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME 29 import com.android.launcher3.LauncherSettings.Favorites.TMP_TABLE 30 import com.android.launcher3.Utilities 31 import com.android.launcher3.config.FeatureFlags 32 import com.android.launcher3.logging.FileLog 33 import com.android.launcher3.logging.StatsLogManager 34 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ROW_SHIFT_GRID_MIGRATION 35 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ROW_SHIFT_ONE_GRID_MIGRATION 36 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_STANDARD_GRID_MIGRATION 37 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_STANDARD_ONE_GRID_MIGRATION 38 import com.android.launcher3.model.GridSizeMigrationDBController.DbReader 39 import com.android.launcher3.model.GridSizeMigrationDBController.isOneGridMigration 40 import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction 41 import com.android.launcher3.provider.LauncherDbUtils.copyTable 42 import com.android.launcher3.provider.LauncherDbUtils.dropTable 43 import com.android.launcher3.provider.LauncherDbUtils.shiftWorkspaceByXCells 44 import com.android.launcher3.util.CellAndSpan 45 import com.android.launcher3.util.GridOccupancy 46 import com.android.launcher3.util.IntArray 47 48 class GridSizeMigrationLogic { 49 /** 50 * Migrates the grid size from srcDeviceState to destDeviceState and make those changes in the 51 * target DB, using the source DB to determine what to add/remove/move/resize in the destination 52 * DB. 53 */ 54 fun migrateGrid( 55 context: Context, 56 srcDeviceState: DeviceGridState, 57 destDeviceState: DeviceGridState, 58 target: DatabaseHelper, 59 source: SQLiteDatabase, 60 isDestNewDb: Boolean, 61 modelDelegate: ModelDelegate, 62 ) { 63 if (!GridSizeMigrationDBController.needsToMigrate(srcDeviceState, destDeviceState)) { 64 return 65 } 66 67 val statsLogManager: StatsLogManager = StatsLogManager.newInstance(context) 68 69 val isAfterRestore = get(context).get(LauncherPrefs.IS_FIRST_LOAD_AFTER_RESTORE) 70 FileLog.d( 71 TAG, 72 "Begin grid migration. isAfterRestore: $isAfterRestore\nsrcDeviceState: " + 73 "$srcDeviceState\ndestDeviceState: $destDeviceState\nisDestNewDb: $isDestNewDb", 74 ) 75 76 val shouldMigrateToStrtictlyTallerGrid = 77 shouldMigrateToStrictlyTallerGrid(isDestNewDb, srcDeviceState, destDeviceState) 78 if (shouldMigrateToStrtictlyTallerGrid) { 79 copyTable(source, TABLE_NAME, target.writableDatabase, TABLE_NAME, context) 80 } else { 81 copyTable(source, TABLE_NAME, target.writableDatabase, TMP_TABLE, context) 82 } 83 84 val migrationStartTime = System.currentTimeMillis() 85 try { 86 SQLiteTransaction(target.writableDatabase).use { t -> 87 // We want to add the extra row(s) to the top of the screen, so we shift the grid 88 // down. 89 if (shouldMigrateToStrtictlyTallerGrid) { 90 Log.d(TAG, "Migrating to strictly taller grid") 91 if (Flags.oneGridSpecs()) { 92 shiftWorkspaceByXCells( 93 target.writableDatabase, 94 (destDeviceState.rows - srcDeviceState.rows), 95 TABLE_NAME, 96 ) 97 } 98 // Save current configuration, so that the migration does not run again. 99 destDeviceState.writeToPrefs(context) 100 t.commit() 101 102 if (isOneGridMigration(srcDeviceState, destDeviceState)) { 103 statsLogManager.logger().log(LAUNCHER_ROW_SHIFT_ONE_GRID_MIGRATION) 104 } 105 statsLogManager.logger().log(LAUNCHER_ROW_SHIFT_GRID_MIGRATION) 106 107 return 108 } 109 110 val srcReader = DbReader(t.db, TMP_TABLE, context) 111 val destReader = DbReader(t.db, TABLE_NAME, context) 112 113 val targetSize = Point(destDeviceState.columns, destDeviceState.rows) 114 115 // Here we keep all the DB ids we have in the destination DB such that we don't 116 // assign 117 // an item that we want to add to the destination DB the same id as an already 118 // existing 119 // item. 120 val idsInUse = mutableListOf<Int>() 121 122 // Migrate hotseat. 123 migrateHotseat( 124 srcDeviceState.numHotseat, 125 destDeviceState.numHotseat, 126 srcReader, 127 destReader, 128 target, 129 idsInUse, 130 ) 131 // Migrate workspace. 132 migrateWorkspace(srcReader, destReader, target, targetSize, idsInUse) 133 134 dropTable(t.db, TMP_TABLE) 135 t.commit() 136 137 if (isOneGridMigration(srcDeviceState, destDeviceState)) { 138 statsLogManager.logger().log(LAUNCHER_STANDARD_ONE_GRID_MIGRATION) 139 } 140 statsLogManager.logger().log(LAUNCHER_STANDARD_GRID_MIGRATION) 141 } 142 } catch (e: Exception) { 143 FileLog.e(TAG, "Error during grid migration", e) 144 } finally { 145 Log.v( 146 TAG, 147 "Workspace migration completed in " + 148 (System.currentTimeMillis() - migrationStartTime), 149 ) 150 151 // Save current configuration, so that the migration does not run again. 152 destDeviceState.writeToPrefs(context) 153 154 // Notify if we've migrated successfully 155 modelDelegate.gridMigrationComplete(srcDeviceState, destDeviceState) 156 } 157 } 158 159 /** Handles hotseat migration. */ 160 @VisibleForTesting 161 fun migrateHotseat( 162 srcHotseatSize: Int, 163 destHotseatSize: Int, 164 srcReader: DbReader, 165 destReader: DbReader, 166 helper: DatabaseHelper, 167 idsInUse: MutableList<Int>, 168 ) { 169 val srcHotseatItems = srcReader.loadHotseatEntries() 170 val dstHotseatItems = destReader.loadHotseatEntries() 171 172 // We want to filter out the hotseat items that are placed beyond the size of the source 173 // grid as we always want to keep those extra items from the destination grid. 174 var filteredDstHotseatItems = dstHotseatItems 175 if (srcHotseatSize < destHotseatSize) { 176 filteredDstHotseatItems = 177 filteredDstHotseatItems.filter { entry -> entry.screenId < srcHotseatSize } 178 } 179 180 val itemsToBeAdded = getItemsToBeAdded(srcHotseatItems, filteredDstHotseatItems) 181 val itemsToBeRemoved = getItemsToBeRemoved(srcHotseatItems, filteredDstHotseatItems) 182 183 if (DEBUG) { 184 Log.d( 185 TAG, 186 """Start hotseat migration: 187 |Removing Hotseat Items: [${filteredDstHotseatItems.filter { itemsToBeRemoved.contains(it.id) } 188 .joinToString(",\n") { it.toString() }}] 189 |Adding Hotseat Items: [${itemsToBeAdded 190 .joinToString(",\n") { it.toString() }}] 191 |""" 192 .trimMargin(), 193 ) 194 } 195 196 // Removes the items that we need to remove from the destination DB. 197 if (!itemsToBeRemoved.isEmpty) { 198 GridSizeMigrationDBController.removeEntryFromDb( 199 destReader.mDb, 200 destReader.mTableName, 201 itemsToBeRemoved, 202 ) 203 } 204 205 val remainingDstHotseatItems = destReader.loadHotseatEntries() 206 207 placeHotseatItems( 208 itemsToBeAdded, 209 remainingDstHotseatItems, 210 destHotseatSize, 211 helper, 212 srcReader, 213 destReader, 214 idsInUse, 215 ) 216 } 217 218 private fun placeHotseatItems( 219 hotseatToBeAdded: MutableList<DbEntry>, 220 dstHotseatItems: List<DbEntry>, 221 destHotseatSize: Int, 222 helper: DatabaseHelper, 223 srcReader: DbReader, 224 destReader: DbReader, 225 idsInUse: MutableList<Int>, 226 ) { 227 if (hotseatToBeAdded.isEmpty()) { 228 return 229 } 230 231 idsInUse.addAll(dstHotseatItems.map { entry: DbEntry -> entry.id }) 232 233 hotseatToBeAdded.sort() 234 235 val placementSolutionHotseat = 236 solveHotseatPlacement(destHotseatSize, dstHotseatItems, hotseatToBeAdded) 237 for (entryToPlace in placementSolutionHotseat) { 238 GridSizeMigrationDBController.insertEntryInDb( 239 helper, 240 entryToPlace, 241 srcReader.mTableName, 242 destReader.mTableName, 243 idsInUse, 244 ) 245 } 246 } 247 248 @VisibleForTesting 249 fun migrateWorkspace( 250 srcReader: DbReader, 251 destReader: DbReader, 252 helper: DatabaseHelper, 253 targetSize: Point, 254 idsInUse: MutableList<Int>, 255 ) { 256 val srcWorkspaceItems = srcReader.loadAllWorkspaceEntries() 257 258 val dstWorkspaceItems = destReader.loadAllWorkspaceEntries() 259 260 val toBeRemoved = IntArray() 261 262 val workspaceToBeAdded = getItemsToBeAdded(srcWorkspaceItems, dstWorkspaceItems) 263 toBeRemoved.addAll(getItemsToBeRemoved(srcWorkspaceItems, dstWorkspaceItems)) 264 265 if (DEBUG) { 266 Log.d( 267 TAG, 268 """Start workspace migration: 269 |Source Device: [${srcWorkspaceItems.joinToString(",\n") { it.toString() }}] 270 |Target Device: [${dstWorkspaceItems.joinToString(",\n") { it.toString() }}] 271 |Removing Workspace Items: [${dstWorkspaceItems.filter { toBeRemoved.contains(it.id) } 272 .joinToString(",\n") { it.toString() }}] 273 |Adding Workspace Items: [${workspaceToBeAdded 274 .joinToString(",\n") { it.toString() }}] 275 |""" 276 .trimMargin(), 277 ) 278 } 279 280 // Removes the items that we need to remove from the destination DB. 281 if (!toBeRemoved.isEmpty) { 282 GridSizeMigrationDBController.removeEntryFromDb( 283 destReader.mDb, 284 destReader.mTableName, 285 toBeRemoved, 286 ) 287 } 288 289 val remainingDstWorkspaceItems = destReader.loadAllWorkspaceEntries() 290 placeWorkspaceItems( 291 workspaceToBeAdded, 292 remainingDstWorkspaceItems, 293 targetSize.x, 294 targetSize.y, 295 helper, 296 srcReader, 297 destReader, 298 idsInUse, 299 ) 300 } 301 302 private fun placeWorkspaceItems( 303 workspaceToBeAdded: MutableList<DbEntry>, 304 dstWorkspaceItems: List<DbEntry>, 305 trgX: Int, 306 trgY: Int, 307 helper: DatabaseHelper, 308 srcReader: DbReader, 309 destReader: DbReader, 310 idsInUse: MutableList<Int>, 311 ) { 312 if (workspaceToBeAdded.isEmpty()) { 313 return 314 } 315 316 idsInUse.addAll(dstWorkspaceItems.map { entry: DbEntry -> entry.id }) 317 318 workspaceToBeAdded.sort() 319 320 // First we create a collection of the screens 321 val screens: MutableList<Int> = ArrayList() 322 for (screenId in 0..destReader.mLastScreenId) { 323 screens.add(screenId) 324 } 325 326 // Then we place the items on the screens 327 var itemsToPlace = WorkspaceItemsToPlace(workspaceToBeAdded, mutableListOf()) 328 for (screenId in screens) { 329 if (DEBUG) { 330 Log.d(TAG, "Migrating $screenId") 331 } 332 itemsToPlace = 333 solveGridPlacement( 334 destReader.mContext, 335 screenId, 336 trgX, 337 trgY, 338 itemsToPlace.mRemainingItemsToPlace, 339 destReader.mWorkspaceEntriesByScreenId[screenId], 340 ) 341 placeItems(itemsToPlace, helper, srcReader, destReader, idsInUse) 342 while (itemsToPlace.mPlacementSolution.isNotEmpty()) { 343 GridSizeMigrationDBController.insertEntryInDb( 344 helper, 345 itemsToPlace.mPlacementSolution.removeAt(0), 346 srcReader.mTableName, 347 destReader.mTableName, 348 idsInUse, 349 ) 350 } 351 if (itemsToPlace.mRemainingItemsToPlace.isEmpty()) { 352 break 353 } 354 } 355 356 // In case the new grid is smaller, there might be some leftover items that don't fit on 357 // any of the screens, in this case we add them to new screens until all of them are placed. 358 var screenId = destReader.mLastScreenId + 1 359 while (itemsToPlace.mRemainingItemsToPlace.isNotEmpty()) { 360 itemsToPlace = 361 solveGridPlacement( 362 destReader.mContext, 363 screenId, 364 trgX, 365 trgY, 366 itemsToPlace.mRemainingItemsToPlace, 367 destReader.mWorkspaceEntriesByScreenId[screenId], 368 ) 369 placeItems(itemsToPlace, helper, srcReader, destReader, idsInUse) 370 screenId++ 371 } 372 } 373 374 private fun placeItems( 375 itemsToPlace: WorkspaceItemsToPlace, 376 helper: DatabaseHelper, 377 srcReader: DbReader, 378 destReader: DbReader, 379 idsInUse: List<Int>, 380 ) { 381 while (itemsToPlace.mPlacementSolution.isNotEmpty()) { 382 GridSizeMigrationDBController.insertEntryInDb( 383 helper, 384 itemsToPlace.mPlacementSolution.removeAt(0), 385 srcReader.mTableName, 386 destReader.mTableName, 387 idsInUse, 388 ) 389 } 390 } 391 392 /** Only migrate the grid in this manner if the target grid is taller and not wider. */ 393 private fun shouldMigrateToStrictlyTallerGrid( 394 isDestNewDb: Boolean, 395 srcDeviceState: DeviceGridState, 396 destDeviceState: DeviceGridState, 397 ): Boolean { 398 return (Flags.oneGridSpecs() || isDestNewDb) && 399 srcDeviceState.columns == destDeviceState.columns && 400 srcDeviceState.rows < destDeviceState.rows 401 } 402 403 /** 404 * Finds all the items that are in the old grid which aren't in the new grid, meaning they need 405 * to be added to the new grid. 406 * 407 * @return a list of DbEntry's which we need to add. 408 */ 409 private fun getItemsToBeAdded(src: List<DbEntry>, dest: List<DbEntry>): MutableList<DbEntry> { 410 val entryCountDiff = calcDiff(src, dest) 411 val toBeAdded: MutableList<DbEntry> = ArrayList() 412 src.forEach { entry -> 413 entryCountDiff[entry]?.let { entryDiff -> 414 if (entryDiff > 0) { 415 toBeAdded.add(entry) 416 entryCountDiff[entry] = entryDiff - 1 417 } 418 } 419 } 420 return toBeAdded 421 } 422 423 /** 424 * Finds all the items that are in the new grid which aren't in the old grid, meaning they need 425 * to be removed from the new grid. 426 * 427 * @return an IntArray of item id's which we need to remove. 428 */ 429 private fun getItemsToBeRemoved(src: List<DbEntry>, dest: List<DbEntry>): IntArray { 430 val entryCountDiff = calcDiff(src, dest) 431 val toBeRemoved = 432 IntArray().apply { 433 dest.forEach { entry -> 434 entryCountDiff[entry]?.let { entryDiff -> 435 if (entryDiff < 0) { 436 add(entry.id) 437 if (entry.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) { 438 entry.mFolderItems.values.forEach { ids -> 439 ids.forEach { value -> add(value) } 440 } 441 } 442 } 443 entryCountDiff[entry] = entryDiff.plus(1) 444 } 445 } 446 } 447 return toBeRemoved 448 } 449 450 /** 451 * Calculates the difference between the old and new grid items in terms of how many of each 452 * item there are. E.g. if the old grid had 2 Calculator icons but the new grid has 0, then the 453 * difference there would be 2. While if the old grid has 0 Calculator icons and the new grid 454 * has 1, then the difference would be -1. 455 * 456 * @return a Map with each DbEntry as a key and the count of said entry as the value. 457 */ 458 private fun calcDiff(src: List<DbEntry>, dest: List<DbEntry>): MutableMap<DbEntry, Int> { 459 val entryCountDiff: MutableMap<DbEntry, Int> = HashMap() 460 src.forEach { entry -> entryCountDiff[entry] = entryCountDiff.getOrDefault(entry, 0) + 1 } 461 dest.forEach { entry -> entryCountDiff[entry] = entryCountDiff.getOrDefault(entry, 0) - 1 } 462 return entryCountDiff 463 } 464 465 private fun solveHotseatPlacement( 466 hotseatSize: Int, 467 placedHotseatItems: List<DbEntry>, 468 itemsToPlace: List<DbEntry>, 469 ): List<DbEntry> { 470 val placementSolution: MutableList<DbEntry> = ArrayList() 471 val remainingItemsToPlace: MutableList<DbEntry> = ArrayList(itemsToPlace) 472 val occupied = BooleanArray(hotseatSize) 473 for (entry in placedHotseatItems) { 474 occupied[entry.screenId] = true 475 } 476 477 for (i in occupied.indices) { 478 if (!occupied[i] && remainingItemsToPlace.isNotEmpty()) { 479 val entry: DbEntry = 480 remainingItemsToPlace.removeAt(0).apply { 481 screenId = i 482 // These values does not affect the item position, but we should set them 483 // to something other than -1. 484 cellX = i 485 cellY = 0 486 } 487 placementSolution.add(entry) 488 occupied[entry.screenId] = true 489 } 490 } 491 return placementSolution 492 } 493 494 private fun solveGridPlacement( 495 context: Context, 496 screenId: Int, 497 trgX: Int, 498 trgY: Int, 499 sortedItemsToPlace: MutableList<DbEntry>, 500 existedEntries: MutableList<DbEntry>?, 501 ): WorkspaceItemsToPlace { 502 val itemsToPlace = WorkspaceItemsToPlace(sortedItemsToPlace, mutableListOf()) 503 val occupied = GridOccupancy(trgX, trgY) 504 val trg = Point(trgX, trgY) 505 val next: Point = 506 if ( 507 screenId == 0 && 508 (FeatureFlags.QSB_ON_FIRST_SCREEN && 509 (!Flags.enableSmartspaceRemovalToggle() || 510 getPrefs(context) 511 .getBoolean(LoaderTask.SMARTSPACE_ON_HOME_SCREEN, true)) && 512 !Utilities.SHOULD_SHOW_FIRST_PAGE_WIDGET) 513 ) { 514 Point(0, 1 /* smartspace */) 515 } else { 516 Point(0, 0) 517 } 518 if (existedEntries != null) { 519 for (entry in existedEntries) { 520 occupied.markCells(entry, true) 521 } 522 } 523 val iterator = itemsToPlace.mRemainingItemsToPlace.iterator() 524 while (iterator.hasNext()) { 525 val entry = iterator.next() 526 if (entry.minSpanX > trgX || entry.minSpanY > trgY) { 527 iterator.remove() 528 continue 529 } 530 findPlacementForEntry(entry, next.x, next.y, trg, occupied)?.let { 531 entry.screenId = screenId 532 entry.cellX = it.cellX 533 entry.cellY = it.cellY 534 entry.spanX = it.spanX 535 entry.spanY = it.spanY 536 occupied.markCells(entry, true) 537 next[entry.cellX + entry.spanX] = entry.cellY 538 itemsToPlace.mPlacementSolution.add(entry) 539 iterator.remove() 540 } 541 } 542 return itemsToPlace 543 } 544 545 /** 546 * Search for the next possible placement of an item. (mNextStartX, mNextStartY) serves as a 547 * memoization of last placement, we can start our search for next placement from there to speed 548 * up the search. 549 * 550 * @return NewEntryPlacement object if we found a valid placement, null if we didn't. 551 */ 552 private fun findPlacementForEntry( 553 entry: DbEntry, 554 startPosX: Int, 555 startPosY: Int, 556 trg: Point, 557 occupied: GridOccupancy, 558 ): CellAndSpan? { 559 var newStartPosX = startPosX 560 for (y in startPosY until trg.y) { 561 for (x in newStartPosX until trg.x) { 562 if (occupied.isRegionVacant(x, y, entry.minSpanX, entry.minSpanY)) { 563 return (CellAndSpan(x, y, entry.minSpanX, entry.minSpanY)) 564 } 565 } 566 newStartPosX = 0 567 } 568 return null 569 } 570 571 private data class WorkspaceItemsToPlace( 572 val mRemainingItemsToPlace: MutableList<DbEntry>, 573 val mPlacementSolution: MutableList<DbEntry>, 574 ) 575 576 companion object { 577 private const val TAG = "GridSizeMigrationLogic" 578 private const val DEBUG = true 579 } 580 } 581