• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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