Loading src/com/android/launcher3/LauncherAppState.java +25 −0 Original line number Diff line number Diff line Loading @@ -40,7 +40,9 @@ import android.content.SharedPreferences; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.content.pm.LauncherApps; import android.content.pm.LauncherApps.ArchiveCompatibilityParams; import android.net.Uri; import android.os.UserHandle; import android.provider.Settings; import android.util.Log; import androidx.annotation.Nullable; Loading @@ -53,6 +55,7 @@ import com.android.launcher3.icons.LauncherIconProvider; import com.android.launcher3.icons.LauncherIcons; import com.android.launcher3.lineage.trust.HiddenAppsFilter; import com.android.launcher3.logging.FileLog; import com.android.launcher3.model.CompactWorkspaceAfterRestoreTask; import com.android.launcher3.model.ModelLauncherCallbacks; import com.android.launcher3.model.WidgetsFilterDataProvider; import com.android.launcher3.notification.NotificationListener; Loading Loading @@ -200,6 +203,7 @@ public class LauncherAppState implements SafeCloseable { mAppMonitor.onAppCreated(mContext); // Register an observer to notify Launcher about Private Space settings toggle. registerPrivateSpaceHideWhenLockListener(settingsCache); registerSetupCompleteListener(settingsCache); } public LauncherAppState(Context context, @Nullable String iconCacheFileName) { Loading @@ -225,6 +229,27 @@ public class LauncherAppState implements SafeCloseable { } } private void registerSetupCompleteListener(SettingsCache settingsCache) { // After restore, compact the workspace only once SUW is complete. Uri setupCompleteUri = Settings.Secure.getUriFor(Settings.Secure.USER_SETUP_COMPLETE); SettingsCache.OnChangeListener setupCompleteListener = isSetupComplete -> { if (!isSetupComplete || !LauncherPrefs.get(mContext).get( LauncherPrefs.NEEDS_WORKSPACE_REORDER_AFTER_RESTORE)) { return; } mModel.enqueueModelUpdateTask(new CompactWorkspaceAfterRestoreTask()); }; settingsCache.register(setupCompleteUri, setupCompleteListener); setupCompleteListener.onSettingsChanged( settingsCache.getValue(setupCompleteUri, 0)); mOnTerminateCallback.add(() -> settingsCache.unregister(setupCompleteUri, setupCompleteListener)); } private void registerPrivateSpaceHideWhenLockListener(SettingsCache settingsCache) { SettingsCache.OnChangeListener psHideWhenLockChangedListener = this::onPrivateSpaceHideWhenLockChanged; Loading src/com/android/launcher3/LauncherBackupAgent.java +2 −0 Original line number Diff line number Diff line package com.android.launcher3; import static com.android.launcher3.LauncherPrefs.NEEDS_WIDGET_REBIND_AFTER_RESTORE; import static com.android.launcher3.LauncherPrefs.NEEDS_WORKSPACE_REORDER_AFTER_RESTORE; import static com.android.launcher3.LauncherPrefs.NO_DB_FILES_RESTORED; import android.app.backup.BackupAgent; Loading Loading @@ -56,6 +57,7 @@ public class LauncherBackupAgent extends BackupAgent { RestoreDbTask.setPending(this); FileLog.d(TAG, "onRestoreFinished: set pending for RestoreDbTask"); LauncherPrefs.get(this).putSync(NEEDS_WIDGET_REBIND_AFTER_RESTORE.to(true)); LauncherPrefs.get(this).putSync(NEEDS_WORKSPACE_REORDER_AFTER_RESTORE.to(true)); markIfFilesWereNotActuallyRestored(); } Loading src/com/android/launcher3/LauncherPrefs.kt +7 −0 Original line number Diff line number Diff line Loading @@ -137,6 +137,13 @@ abstract class LauncherPrefs : SafeCloseable { @JvmField val NEEDS_WIDGET_REBIND_AFTER_RESTORE = nonRestorableItem("needs_widget_rebind_after_restore", false, EncryptionType.ENCRYPTED) @JvmField val NEEDS_WORKSPACE_REORDER_AFTER_RESTORE = nonRestorableItem( "needs_workspace_reorder_after_restore", false, EncryptionType.ENCRYPTED, ) @JvmField val APP_WIDGET_IDS = backedUpItem(RestoreDbTask.APPWIDGET_IDS, "") @JvmField val OLD_APP_WIDGET_IDS = backedUpItem(RestoreDbTask.APPWIDGET_OLD_IDS, "") Loading src/com/android/launcher3/model/CompactWorkspaceAfterRestoreTask.kt 0 → 100644 +129 −0 Original line number Diff line number Diff line /* * Copyright (C) 2026 e Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * */ package com.android.launcher3.model import com.android.launcher3.InvariantDeviceProfile import com.android.launcher3.LauncherPrefs import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP import com.android.launcher3.ModelUpdateTask import com.android.launcher3.Utilities.SHOULD_SHOW_FIRST_PAGE_WIDGET import com.android.launcher3.WorkspaceLayoutManager.FIRST_SCREEN_ID import com.android.launcher3.config.FeatureFlags import com.android.launcher3.model.data.ItemInfo import com.android.launcher3.util.GridOccupancy import com.android.launcher3.util.IntSet class CompactWorkspaceAfterRestoreTask : ModelUpdateTask { override fun execute(taskController: ModelTaskController, dataModel: BgDataModel, apps: AllAppsList) { val context = taskController.app.context val model = taskController.app.model val prefs = LauncherPrefs.get(context) if (!prefs.get(LauncherPrefs.NEEDS_WORKSPACE_REORDER_AFTER_RESTORE)) return val idp = InvariantDeviceProfile.INSTANCE.get(context) val columns = idp.numColumnsFixed val rows = idp.numRowsFixed val screenIds = mutableListOf<Int>() val fixedItems = ArrayList<ItemInfo>() val movableItems = ArrayList<ItemInfo>() synchronized(dataModel) { val screens = dataModel.collectWorkspaceScreens() for (i in 0 until screens.size()) { screenIds.add(screens[i]) } screenIds.sort() for (item in dataModel.itemsIdMap) { if (item.container != CONTAINER_DESKTOP) continue val isFixed = item.spanX > 1 || item.spanY > 1 if (isFixed) { fixedItems.add(item) } else if (item.spanX == 1 && item.spanY == 1) { movableItems.add(item) } } } movableItems.sortWith(compareBy({ it.screenId }, { it.cellY }, { it.cellX })) val screensToExclude = IntSet() if (FeatureFlags.QSB_ON_FIRST_SCREEN.get() && !SHOULD_SHOW_FIRST_PAGE_WIDGET) { screensToExclude.add(FIRST_SCREEN_ID) } val occupiedByScreen = HashMap<Int, GridOccupancy>(screenIds.size) fun occupancyFor(screenId: Int) = occupiedByScreen.getOrPut(screenId) { GridOccupancy(columns, rows) } fixedItems.forEach { occupancyFor(it.screenId).markCells(it, true) } val updated = ArrayList<ItemInfo>() val xy = IntArray(2) movableItems.forEach { item -> var placed = false for (screenId in screenIds) { if (screensToExclude.contains(screenId)) continue val occupancy = occupancyFor(screenId) if (!occupancy.findVacantCell(xy, item.spanX, item.spanY)) continue placed = true updatedIfChanged(item, screenId, xy[0], xy[1], columns, updated) occupancy.markCells(xy[0], xy[1], item.spanX, item.spanY, true) break } if (!placed) { val newScreenId = model.modelDbController.getNewScreenId() screenIds.add(newScreenId) val occupancy = occupancyFor(newScreenId) if (occupancy.findVacantCell(xy, item.spanX, item.spanY)) { updatedIfChanged(item, newScreenId, xy[0], xy[1], columns, updated) occupancy.markCells(xy[0], xy[1], item.spanX, item.spanY, true) } } } prefs.putSync(LauncherPrefs.NEEDS_WORKSPACE_REORDER_AFTER_RESTORE.to(false)) if (updated.isEmpty()) { return } val writer = taskController.getModelWriter() updated.forEach { writer.updateItemInDatabase(it) } model.forceReload() } private fun updatedIfChanged( item: ItemInfo, screenId: Int, cellX: Int, cellY: Int, columns: Int, updated: MutableList<ItemInfo>, ) { if (item.screenId == screenId && item.cellX == cellX && item.cellY == cellY) { return } item.screenId = screenId item.cellX = cellX item.cellY = cellY item.rank = cellX + (cellY * columns) updated.add(item) } } Loading
src/com/android/launcher3/LauncherAppState.java +25 −0 Original line number Diff line number Diff line Loading @@ -40,7 +40,9 @@ import android.content.SharedPreferences; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.content.pm.LauncherApps; import android.content.pm.LauncherApps.ArchiveCompatibilityParams; import android.net.Uri; import android.os.UserHandle; import android.provider.Settings; import android.util.Log; import androidx.annotation.Nullable; Loading @@ -53,6 +55,7 @@ import com.android.launcher3.icons.LauncherIconProvider; import com.android.launcher3.icons.LauncherIcons; import com.android.launcher3.lineage.trust.HiddenAppsFilter; import com.android.launcher3.logging.FileLog; import com.android.launcher3.model.CompactWorkspaceAfterRestoreTask; import com.android.launcher3.model.ModelLauncherCallbacks; import com.android.launcher3.model.WidgetsFilterDataProvider; import com.android.launcher3.notification.NotificationListener; Loading Loading @@ -200,6 +203,7 @@ public class LauncherAppState implements SafeCloseable { mAppMonitor.onAppCreated(mContext); // Register an observer to notify Launcher about Private Space settings toggle. registerPrivateSpaceHideWhenLockListener(settingsCache); registerSetupCompleteListener(settingsCache); } public LauncherAppState(Context context, @Nullable String iconCacheFileName) { Loading @@ -225,6 +229,27 @@ public class LauncherAppState implements SafeCloseable { } } private void registerSetupCompleteListener(SettingsCache settingsCache) { // After restore, compact the workspace only once SUW is complete. Uri setupCompleteUri = Settings.Secure.getUriFor(Settings.Secure.USER_SETUP_COMPLETE); SettingsCache.OnChangeListener setupCompleteListener = isSetupComplete -> { if (!isSetupComplete || !LauncherPrefs.get(mContext).get( LauncherPrefs.NEEDS_WORKSPACE_REORDER_AFTER_RESTORE)) { return; } mModel.enqueueModelUpdateTask(new CompactWorkspaceAfterRestoreTask()); }; settingsCache.register(setupCompleteUri, setupCompleteListener); setupCompleteListener.onSettingsChanged( settingsCache.getValue(setupCompleteUri, 0)); mOnTerminateCallback.add(() -> settingsCache.unregister(setupCompleteUri, setupCompleteListener)); } private void registerPrivateSpaceHideWhenLockListener(SettingsCache settingsCache) { SettingsCache.OnChangeListener psHideWhenLockChangedListener = this::onPrivateSpaceHideWhenLockChanged; Loading
src/com/android/launcher3/LauncherBackupAgent.java +2 −0 Original line number Diff line number Diff line package com.android.launcher3; import static com.android.launcher3.LauncherPrefs.NEEDS_WIDGET_REBIND_AFTER_RESTORE; import static com.android.launcher3.LauncherPrefs.NEEDS_WORKSPACE_REORDER_AFTER_RESTORE; import static com.android.launcher3.LauncherPrefs.NO_DB_FILES_RESTORED; import android.app.backup.BackupAgent; Loading Loading @@ -56,6 +57,7 @@ public class LauncherBackupAgent extends BackupAgent { RestoreDbTask.setPending(this); FileLog.d(TAG, "onRestoreFinished: set pending for RestoreDbTask"); LauncherPrefs.get(this).putSync(NEEDS_WIDGET_REBIND_AFTER_RESTORE.to(true)); LauncherPrefs.get(this).putSync(NEEDS_WORKSPACE_REORDER_AFTER_RESTORE.to(true)); markIfFilesWereNotActuallyRestored(); } Loading
src/com/android/launcher3/LauncherPrefs.kt +7 −0 Original line number Diff line number Diff line Loading @@ -137,6 +137,13 @@ abstract class LauncherPrefs : SafeCloseable { @JvmField val NEEDS_WIDGET_REBIND_AFTER_RESTORE = nonRestorableItem("needs_widget_rebind_after_restore", false, EncryptionType.ENCRYPTED) @JvmField val NEEDS_WORKSPACE_REORDER_AFTER_RESTORE = nonRestorableItem( "needs_workspace_reorder_after_restore", false, EncryptionType.ENCRYPTED, ) @JvmField val APP_WIDGET_IDS = backedUpItem(RestoreDbTask.APPWIDGET_IDS, "") @JvmField val OLD_APP_WIDGET_IDS = backedUpItem(RestoreDbTask.APPWIDGET_OLD_IDS, "") Loading
src/com/android/launcher3/model/CompactWorkspaceAfterRestoreTask.kt 0 → 100644 +129 −0 Original line number Diff line number Diff line /* * Copyright (C) 2026 e Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * */ package com.android.launcher3.model import com.android.launcher3.InvariantDeviceProfile import com.android.launcher3.LauncherPrefs import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP import com.android.launcher3.ModelUpdateTask import com.android.launcher3.Utilities.SHOULD_SHOW_FIRST_PAGE_WIDGET import com.android.launcher3.WorkspaceLayoutManager.FIRST_SCREEN_ID import com.android.launcher3.config.FeatureFlags import com.android.launcher3.model.data.ItemInfo import com.android.launcher3.util.GridOccupancy import com.android.launcher3.util.IntSet class CompactWorkspaceAfterRestoreTask : ModelUpdateTask { override fun execute(taskController: ModelTaskController, dataModel: BgDataModel, apps: AllAppsList) { val context = taskController.app.context val model = taskController.app.model val prefs = LauncherPrefs.get(context) if (!prefs.get(LauncherPrefs.NEEDS_WORKSPACE_REORDER_AFTER_RESTORE)) return val idp = InvariantDeviceProfile.INSTANCE.get(context) val columns = idp.numColumnsFixed val rows = idp.numRowsFixed val screenIds = mutableListOf<Int>() val fixedItems = ArrayList<ItemInfo>() val movableItems = ArrayList<ItemInfo>() synchronized(dataModel) { val screens = dataModel.collectWorkspaceScreens() for (i in 0 until screens.size()) { screenIds.add(screens[i]) } screenIds.sort() for (item in dataModel.itemsIdMap) { if (item.container != CONTAINER_DESKTOP) continue val isFixed = item.spanX > 1 || item.spanY > 1 if (isFixed) { fixedItems.add(item) } else if (item.spanX == 1 && item.spanY == 1) { movableItems.add(item) } } } movableItems.sortWith(compareBy({ it.screenId }, { it.cellY }, { it.cellX })) val screensToExclude = IntSet() if (FeatureFlags.QSB_ON_FIRST_SCREEN.get() && !SHOULD_SHOW_FIRST_PAGE_WIDGET) { screensToExclude.add(FIRST_SCREEN_ID) } val occupiedByScreen = HashMap<Int, GridOccupancy>(screenIds.size) fun occupancyFor(screenId: Int) = occupiedByScreen.getOrPut(screenId) { GridOccupancy(columns, rows) } fixedItems.forEach { occupancyFor(it.screenId).markCells(it, true) } val updated = ArrayList<ItemInfo>() val xy = IntArray(2) movableItems.forEach { item -> var placed = false for (screenId in screenIds) { if (screensToExclude.contains(screenId)) continue val occupancy = occupancyFor(screenId) if (!occupancy.findVacantCell(xy, item.spanX, item.spanY)) continue placed = true updatedIfChanged(item, screenId, xy[0], xy[1], columns, updated) occupancy.markCells(xy[0], xy[1], item.spanX, item.spanY, true) break } if (!placed) { val newScreenId = model.modelDbController.getNewScreenId() screenIds.add(newScreenId) val occupancy = occupancyFor(newScreenId) if (occupancy.findVacantCell(xy, item.spanX, item.spanY)) { updatedIfChanged(item, newScreenId, xy[0], xy[1], columns, updated) occupancy.markCells(xy[0], xy[1], item.spanX, item.spanY, true) } } } prefs.putSync(LauncherPrefs.NEEDS_WORKSPACE_REORDER_AFTER_RESTORE.to(false)) if (updated.isEmpty()) { return } val writer = taskController.getModelWriter() updated.forEach { writer.updateItemInDatabase(it) } model.forceReload() } private fun updatedIfChanged( item: ItemInfo, screenId: Int, cellX: Int, cellY: Int, columns: Int, updated: MutableList<ItemInfo>, ) { if (item.screenId == screenId && item.cellX == cellX && item.cellY == cellY) { return } item.screenId = screenId item.cellX = cellX item.cellY = cellY item.rank = cellX + (cellY * columns) updated.add(item) } }