blob: 4f9206629a95a634f9762704b8776d29d9373de8 [file] [log] [blame]
Sunny Goyale5bb7052015-07-27 14:36:07 -07001package com.android.launcher3.model;
2
Sunny Goyal161a2142018-10-29 14:02:20 -07003import static com.android.launcher3.LauncherSettings.Settings.EXTRA_VALUE;
Sunny Goyal415f1732018-11-29 10:33:47 -08004import static com.android.launcher3.Utilities.getPointString;
5import static com.android.launcher3.Utilities.parsePoint;
6
Sunny Goyale5bb7052015-07-27 14:36:07 -07007import android.content.ComponentName;
Sunny Goyale5bb7052015-07-27 14:36:07 -07008import android.content.ContentValues;
9import android.content.Context;
10import android.content.Intent;
11import android.content.SharedPreferences;
12import android.content.pm.PackageInfo;
Sunny Goyala9e2f5a2016-06-10 12:22:04 -070013import android.content.pm.PackageManager;
Sunny Goyale5bb7052015-07-27 14:36:07 -070014import android.database.Cursor;
Sunny Goyal161a2142018-10-29 14:02:20 -070015import android.database.sqlite.SQLiteDatabase;
Sunny Goyale5bb7052015-07-27 14:36:07 -070016import android.graphics.Point;
Sunny Goyale5bb7052015-07-27 14:36:07 -070017import android.util.Log;
Sunny Goyal161a2142018-10-29 14:02:20 -070018import android.util.SparseArray;
Ryan Lothianfa530cd2018-10-12 14:14:16 -040019
Sunny Goyal045b4fa2019-09-20 12:51:37 -070020import androidx.annotation.VisibleForTesting;
21
Sunny Goyale5bb7052015-07-27 14:36:07 -070022import com.android.launcher3.InvariantDeviceProfile;
23import com.android.launcher3.ItemInfo;
24import com.android.launcher3.LauncherAppState;
25import com.android.launcher3.LauncherAppWidgetProviderInfo;
Sunny Goyale5bb7052015-07-27 14:36:07 -070026import com.android.launcher3.LauncherSettings;
27import com.android.launcher3.LauncherSettings.Favorites;
Sunny Goyal161a2142018-10-29 14:02:20 -070028import com.android.launcher3.LauncherSettings.Settings;
Sunny Goyale5bb7052015-07-27 14:36:07 -070029import com.android.launcher3.Utilities;
Sunny Goyalda4fe1a2016-05-26 16:05:17 -070030import com.android.launcher3.Workspace;
Sunny Goyala9e2f5a2016-06-10 12:22:04 -070031import com.android.launcher3.config.FeatureFlags;
Sunny Goyal73b5a272019-12-09 14:55:56 -080032import com.android.launcher3.pm.InstallSessionHelper;
Sunny Goyal161a2142018-10-29 14:02:20 -070033import com.android.launcher3.provider.LauncherDbUtils;
34import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction;
Sunny Goyalff4ba2d2016-04-02 14:12:34 -070035import com.android.launcher3.util.GridOccupancy;
Sunny Goyalefb7e842018-10-04 15:11:00 -070036import com.android.launcher3.util.IntArray;
37import com.android.launcher3.util.IntSparseArrayMap;
Sunny Goyal337c81f2019-12-10 12:19:13 -080038import com.android.launcher3.widget.WidgetManagerHelper;
Sunny Goyalefb7e842018-10-04 15:11:00 -070039
Sunny Goyale5bb7052015-07-27 14:36:07 -070040import java.util.ArrayList;
41import java.util.Collections;
Sunny Goyale5bb7052015-07-27 14:36:07 -070042import java.util.HashSet;
43
44/**
45 * This class takes care of shrinking the workspace (by maximum of one row and one column), as a
Sunny Goyalf862a262015-12-14 14:27:38 -080046 * result of restoring from a larger device or device density change.
Sunny Goyale5bb7052015-07-27 14:36:07 -070047 */
Sunny Goyalf862a262015-12-14 14:27:38 -080048public class GridSizeMigrationTask {
Sunny Goyale5bb7052015-07-27 14:36:07 -070049
Sunny Goyalf862a262015-12-14 14:27:38 -080050 private static final String TAG = "GridSizeMigrationTask";
Sunny Goyald934e0b2015-07-31 12:40:57 -070051 private static final boolean DEBUG = true;
Sunny Goyale5bb7052015-07-27 14:36:07 -070052
Sunny Goyalf862a262015-12-14 14:27:38 -080053 private static final String KEY_MIGRATION_SRC_WORKSPACE_SIZE = "migration_src_workspace_size";
Sunny Goyalbb011da2016-06-15 15:42:29 -070054 private static final String KEY_MIGRATION_SRC_HOTSEAT_COUNT = "migration_src_hotseat_count";
Sunny Goyalf862a262015-12-14 14:27:38 -080055
Sunny Goyale5bb7052015-07-27 14:36:07 -070056 // These are carefully selected weights for various item types (Math.random?), to allow for
Sunny Goyalf862a262015-12-14 14:27:38 -080057 // the least absurd migration experience.
Sunny Goyale5bb7052015-07-27 14:36:07 -070058 private static final float WT_SHORTCUT = 1;
59 private static final float WT_APPLICATION = 0.8f;
60 private static final float WT_WIDGET_MIN = 2;
61 private static final float WT_WIDGET_FACTOR = 0.6f;
62 private static final float WT_FOLDER_FACTOR = 0.5f;
63
Sunny Goyal161a2142018-10-29 14:02:20 -070064 protected final SQLiteDatabase mDb;
65 protected final Context mContext;
Sunny Goyale5bb7052015-07-27 14:36:07 -070066
Sunny Goyalefb7e842018-10-04 15:11:00 -070067 protected final IntArray mEntryToRemove = new IntArray();
Sunny Goyala9e2f5a2016-06-10 12:22:04 -070068 protected final ArrayList<DbEntry> mCarryOver = new ArrayList<>();
Sunny Goyal161a2142018-10-29 14:02:20 -070069
70 private final SparseArray<ContentValues> mUpdateOperations = new SparseArray<>();
Sunny Goyalf076eae2016-01-11 12:25:10 -080071 private final HashSet<String> mValidPackages;
Sunny Goyale5bb7052015-07-27 14:36:07 -070072
73 private final int mSrcX, mSrcY;
Sunny Goyalf862a262015-12-14 14:27:38 -080074 private final int mTrgX, mTrgY;
Sunny Goyale5bb7052015-07-27 14:36:07 -070075 private final boolean mShouldRemoveX, mShouldRemoveY;
76
Sunny Goyalf862a262015-12-14 14:27:38 -080077 private final int mSrcHotseatSize;
Sunny Goyalf076eae2016-01-11 12:25:10 -080078 private final int mDestHotseatSize;
Sunny Goyalf862a262015-12-14 14:27:38 -080079
Sunny Goyal161a2142018-10-29 14:02:20 -070080 protected GridSizeMigrationTask(Context context, SQLiteDatabase db,
Sunny Goyaleb77aae2016-05-24 13:19:01 -070081 HashSet<String> validPackages, Point sourceSize, Point targetSize) {
Sunny Goyale5bb7052015-07-27 14:36:07 -070082 mContext = context;
Sunny Goyal161a2142018-10-29 14:02:20 -070083 mDb = db;
Sunny Goyalf076eae2016-01-11 12:25:10 -080084 mValidPackages = validPackages;
Sunny Goyale5bb7052015-07-27 14:36:07 -070085
Sunny Goyale5bb7052015-07-27 14:36:07 -070086 mSrcX = sourceSize.x;
87 mSrcY = sourceSize.y;
88
Sunny Goyalf076eae2016-01-11 12:25:10 -080089 mTrgX = targetSize.x;
90 mTrgY = targetSize.y;
Sunny Goyale5bb7052015-07-27 14:36:07 -070091
Sunny Goyale5bb7052015-07-27 14:36:07 -070092 mShouldRemoveX = mTrgX < mSrcX;
93 mShouldRemoveY = mTrgY < mSrcY;
Sunny Goyalf076eae2016-01-11 12:25:10 -080094
95 // Non-used variables
Sunny Goyalbb011da2016-06-15 15:42:29 -070096 mSrcHotseatSize = mDestHotseatSize = -1;
Sunny Goyale5bb7052015-07-27 14:36:07 -070097 }
98
Sunny Goyal161a2142018-10-29 14:02:20 -070099 protected GridSizeMigrationTask(Context context, SQLiteDatabase db,
100 HashSet<String> validPackages, int srcHotseatSize, int destHotseatSize) {
Sunny Goyalf076eae2016-01-11 12:25:10 -0800101 mContext = context;
Sunny Goyal161a2142018-10-29 14:02:20 -0700102 mDb = db;
Sunny Goyalf076eae2016-01-11 12:25:10 -0800103 mValidPackages = validPackages;
Sunny Goyale5bb7052015-07-27 14:36:07 -0700104
Sunny Goyalf076eae2016-01-11 12:25:10 -0800105 mSrcHotseatSize = srcHotseatSize;
Sunny Goyale5bb7052015-07-27 14:36:07 -0700106
Sunny Goyalf076eae2016-01-11 12:25:10 -0800107 mDestHotseatSize = destHotseatSize;
Sunny Goyalf862a262015-12-14 14:27:38 -0800108
Sunny Goyalf076eae2016-01-11 12:25:10 -0800109 // Non-used variables
110 mSrcX = mSrcY = mTrgX = mTrgY = -1;
111 mShouldRemoveX = mShouldRemoveY = false;
112 }
Sunny Goyalf862a262015-12-14 14:27:38 -0800113
Sunny Goyalf076eae2016-01-11 12:25:10 -0800114 /**
115 * Applied all the pending DB operations
Ryan Lothianfa530cd2018-10-12 14:14:16 -0400116 *
Sunny Goyalf076eae2016-01-11 12:25:10 -0800117 * @return true if any DB operation was commited.
118 */
119 private boolean applyOperations() throws Exception {
Sunny Goyalf862a262015-12-14 14:27:38 -0800120 // Update items
Sunny Goyal161a2142018-10-29 14:02:20 -0700121 int updateCount = mUpdateOperations.size();
122 for (int i = 0; i < updateCount; i++) {
123 mDb.update(Favorites.TABLE_NAME, mUpdateOperations.valueAt(i),
124 "_id=" + mUpdateOperations.keyAt(i), null);
Sunny Goyalf862a262015-12-14 14:27:38 -0800125 }
126
127 if (!mEntryToRemove.isEmpty()) {
128 if (DEBUG) {
Sunny Goyalefb7e842018-10-04 15:11:00 -0700129 Log.d(TAG, "Removing items: " + mEntryToRemove.toConcatString());
Sunny Goyalf862a262015-12-14 14:27:38 -0800130 }
Sunny Goyal161a2142018-10-29 14:02:20 -0700131 mDb.delete(Favorites.TABLE_NAME, Utilities.createDbSelectionQuery(
132 Favorites._ID, mEntryToRemove), null);
Sunny Goyalf862a262015-12-14 14:27:38 -0800133 }
134
Sunny Goyal161a2142018-10-29 14:02:20 -0700135 return updateCount > 0 || !mEntryToRemove.isEmpty();
Sunny Goyalf862a262015-12-14 14:27:38 -0800136 }
137
138 /**
139 * To migrate hotseat, we load all the entries in order (LTR or RTL) and arrange them
140 * in the order in the new hotseat while keeping an empty space for all-apps. If the number of
141 * entries is more than what can fit in the new hotseat, we drop the entries with least weight.
142 * For weight calculation {@see #WT_SHORTCUT}, {@see #WT_APPLICATION}
143 * & {@see #WT_FOLDER_FACTOR}.
Ryan Lothianfa530cd2018-10-12 14:14:16 -0400144 *
Sunny Goyalf076eae2016-01-11 12:25:10 -0800145 * @return true if any DB change was made
Sunny Goyalf862a262015-12-14 14:27:38 -0800146 */
Sunny Goyalf076eae2016-01-11 12:25:10 -0800147 protected boolean migrateHotseat() throws Exception {
Sunny Goyalf862a262015-12-14 14:27:38 -0800148 ArrayList<DbEntry> items = loadHotseatEntries();
Sunny Goyal36b54222018-07-10 13:50:50 -0700149 while (items.size() > mDestHotseatSize) {
Sunny Goyalf862a262015-12-14 14:27:38 -0800150 // Pick the center item by default.
151 DbEntry toRemove = items.get(items.size() / 2);
152
153 // Find the item with least weight.
154 for (DbEntry entry : items) {
155 if (entry.weight < toRemove.weight) {
156 toRemove = entry;
157 }
158 }
159
160 mEntryToRemove.add(toRemove.id);
161 items.remove(toRemove);
162 }
163
164 // Update screen IDS
165 int newScreenId = 0;
166 for (DbEntry entry : items) {
167 if (entry.screenId != newScreenId) {
168 entry.screenId = newScreenId;
169
170 // These values does not affect the item position, but we should set them
171 // to something other than -1.
172 entry.cellX = newScreenId;
173 entry.cellY = 0;
174
175 update(entry);
176 }
177
178 newScreenId++;
Sunny Goyalf862a262015-12-14 14:27:38 -0800179 }
Sunny Goyalf076eae2016-01-11 12:25:10 -0800180
181 return applyOperations();
Sunny Goyalf862a262015-12-14 14:27:38 -0800182 }
183
Sunny Goyalc5939392018-12-07 11:43:47 -0800184 @VisibleForTesting
Sunny Goyal161a2142018-10-29 14:02:20 -0700185 static IntArray getWorkspaceScreenIds(SQLiteDatabase db) {
186 return LauncherDbUtils.queryIntArray(db, Favorites.TABLE_NAME, Favorites.SCREEN,
Sunny Goyalc5939392018-12-07 11:43:47 -0800187 Favorites.CONTAINER + " = " + Favorites.CONTAINER_DESKTOP,
Sunny Goyal161a2142018-10-29 14:02:20 -0700188 Favorites.SCREEN, Favorites.SCREEN);
Sunny Goyalc5939392018-12-07 11:43:47 -0800189 }
190
Sunny Goyalf076eae2016-01-11 12:25:10 -0800191 /**
192 * @return true if any DB change was made
193 */
194 protected boolean migrateWorkspace() throws Exception {
Sunny Goyal161a2142018-10-29 14:02:20 -0700195 IntArray allScreens = getWorkspaceScreenIds(mDb);
Sunny Goyale5bb7052015-07-27 14:36:07 -0700196 if (allScreens.isEmpty()) {
197 throw new Exception("Unable to get workspace screens");
198 }
199
Sunny Goyalefb7e842018-10-04 15:11:00 -0700200 for (int i = 0; i < allScreens.size(); i++) {
201 int screenId = allScreens.get(i);
Sunny Goyale5bb7052015-07-27 14:36:07 -0700202 if (DEBUG) {
203 Log.d(TAG, "Migrating " + screenId);
204 }
205 migrateScreen(screenId);
206 }
207
208 if (!mCarryOver.isEmpty()) {
Sunny Goyalefb7e842018-10-04 15:11:00 -0700209 IntSparseArrayMap<DbEntry> itemMap = new IntSparseArrayMap<>();
Sunny Goyale5bb7052015-07-27 14:36:07 -0700210 for (DbEntry e : mCarryOver) {
211 itemMap.put(e.id, e);
212 }
213
214 do {
215 // Some items are still remaining. Try adding a few new screens.
216
217 // At every iteration, make sure that at least one item is removed from
218 // {@link #mCarryOver}, to prevent an infinite loop. If no item could be removed,
219 // break the loop and abort migration by throwing an exception.
220 OptimalPlacementSolution placement = new OptimalPlacementSolution(
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700221 new GridOccupancy(mTrgX, mTrgY), deepCopy(mCarryOver), 0, true);
Sunny Goyale5bb7052015-07-27 14:36:07 -0700222 placement.find();
223 if (placement.finalPlacedItems.size() > 0) {
Sunny Goyalefb7e842018-10-04 15:11:00 -0700224 int newScreenId = LauncherSettings.Settings.call(
Sunny Goyald2497482015-09-22 18:24:19 -0700225 mContext.getContentResolver(),
226 LauncherSettings.Settings.METHOD_NEW_SCREEN_ID)
Sunny Goyal161a2142018-10-29 14:02:20 -0700227 .getInt(EXTRA_VALUE);
Sunny Goyale5bb7052015-07-27 14:36:07 -0700228 for (DbEntry item : placement.finalPlacedItems) {
229 if (!mCarryOver.remove(itemMap.get(item.id))) {
230 throw new Exception("Unable to find matching items");
231 }
232 item.screenId = newScreenId;
233 update(item);
234 }
235 } else {
236 throw new Exception("None of the items can be placed on an empty screen");
237 }
238
239 } while (!mCarryOver.isEmpty());
Sunny Goyale5bb7052015-07-27 14:36:07 -0700240 }
Sunny Goyalf076eae2016-01-11 12:25:10 -0800241 return applyOperations();
Sunny Goyale5bb7052015-07-27 14:36:07 -0700242 }
243
244 /**
245 * Migrate a particular screen id.
246 * Strategy:
247 * 1) For all possible combinations of row and column, pick the one which causes the least
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700248 * data loss: {@link #tryRemove(int, int, int, ArrayList, float[])}
Sunny Goyale5bb7052015-07-27 14:36:07 -0700249 * 2) Maintain a list of all lost items before this screen, and add any new item lost from
250 * this screen to that list as well.
251 * 3) If all those items from the above list can be placed on this screen, place them
252 * (otherwise they are placed on a new screen).
253 */
Sunny Goyalefb7e842018-10-04 15:11:00 -0700254 protected void migrateScreen(int screenId) {
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700255 // If we are migrating the first screen, do not touch the first row.
Jon Miranda7143ba62019-03-15 09:00:05 -0700256 int startY = (FeatureFlags.QSB_ON_FIRST_SCREEN && screenId == Workspace.FIRST_SCREEN_ID)
Sunny Goyala9e2f5a2016-06-10 12:22:04 -0700257 ? 1 : 0;
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700258
Sunny Goyalf862a262015-12-14 14:27:38 -0800259 ArrayList<DbEntry> items = loadWorkspaceEntries(screenId);
Sunny Goyale5bb7052015-07-27 14:36:07 -0700260
261 int removedCol = Integer.MAX_VALUE;
262 int removedRow = Integer.MAX_VALUE;
263
264 // removeWt represents the cost function for loss of items during migration, and moveWt
265 // represents the cost function for repositioning the items. moveWt is only considered if
266 // removeWt is same for two different configurations.
267 // Start with Float.MAX_VALUE (assuming full data) and pick the configuration with least
268 // cost.
269 float removeWt = Float.MAX_VALUE;
270 float moveWt = Float.MAX_VALUE;
271 float[] outLoss = new float[2];
272 ArrayList<DbEntry> finalItems = null;
273
274 // Try removing all possible combinations
275 for (int x = 0; x < mSrcX; x++) {
Sunny Goyala5c8a9e2016-07-08 08:32:44 -0700276 // Try removing the rows first from bottom. This keeps the workspace
277 // nicely aligned with hotseat.
278 for (int y = mSrcY - 1; y >= startY; y--) {
Sunny Goyale5bb7052015-07-27 14:36:07 -0700279 // Use a deep copy when trying out a particular combination as it can change
280 // the underlying object.
Ryan Lothianfa530cd2018-10-12 14:14:16 -0400281 ArrayList<DbEntry> itemsOnScreen = tryRemove(x, y, startY, deepCopy(items),
282 outLoss);
Sunny Goyale5bb7052015-07-27 14:36:07 -0700283
Ryan Lothianfa530cd2018-10-12 14:14:16 -0400284 if ((outLoss[0] < removeWt) || ((outLoss[0] == removeWt) && (outLoss[1]
285 < moveWt))) {
Sunny Goyale5bb7052015-07-27 14:36:07 -0700286 removeWt = outLoss[0];
287 moveWt = outLoss[1];
288 removedCol = mShouldRemoveX ? x : removedCol;
289 removedRow = mShouldRemoveY ? y : removedRow;
290 finalItems = itemsOnScreen;
291 }
292
293 // No need to loop over all rows, if a row removal is not needed.
294 if (!mShouldRemoveY) {
295 break;
296 }
297 }
298
299 if (!mShouldRemoveX) {
300 break;
301 }
302 }
303
304 if (DEBUG) {
305 Log.d(TAG, String.format("Removing row %d, column %d on screen %d",
306 removedRow, removedCol, screenId));
307 }
308
Sunny Goyalefb7e842018-10-04 15:11:00 -0700309 IntSparseArrayMap<DbEntry> itemMap = new IntSparseArrayMap<>();
Sunny Goyale5bb7052015-07-27 14:36:07 -0700310 for (DbEntry e : deepCopy(items)) {
311 itemMap.put(e.id, e);
312 }
313
314 for (DbEntry item : finalItems) {
315 DbEntry org = itemMap.get(item.id);
316 itemMap.remove(item.id);
317
318 // Check if update is required
319 if (!item.columnsSame(org)) {
320 update(item);
321 }
322 }
323
324 // The remaining items in {@link #itemMap} are those which didn't get placed.
325 for (DbEntry item : itemMap) {
326 mCarryOver.add(item);
327 }
328
329 if (!mCarryOver.isEmpty() && removeWt == 0) {
330 // No new items were removed in this step. Try placing all the items on this screen.
Sunny Goyalff4ba2d2016-04-02 14:12:34 -0700331 GridOccupancy occupied = new GridOccupancy(mTrgX, mTrgY);
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700332 occupied.markCells(0, 0, mTrgX, startY, true);
Sunny Goyale5bb7052015-07-27 14:36:07 -0700333 for (DbEntry item : finalItems) {
Sunny Goyalff4ba2d2016-04-02 14:12:34 -0700334 occupied.markCells(item, true);
Sunny Goyale5bb7052015-07-27 14:36:07 -0700335 }
336
337 OptimalPlacementSolution placement = new OptimalPlacementSolution(occupied,
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700338 deepCopy(mCarryOver), startY, true);
Sunny Goyale5bb7052015-07-27 14:36:07 -0700339 placement.find();
340 if (placement.lowestWeightLoss == 0) {
341 // All items got placed
342
343 for (DbEntry item : placement.finalPlacedItems) {
344 item.screenId = screenId;
345 update(item);
346 }
347
348 mCarryOver.clear();
349 }
350 }
351 }
352
353 /**
354 * Updates an item in the DB.
355 */
Sunny Goyala9e2f5a2016-06-10 12:22:04 -0700356 protected void update(DbEntry item) {
Sunny Goyal161a2142018-10-29 14:02:20 -0700357 ContentValues values = new ContentValues();
358 item.addToContentValues(values);
359 mUpdateOperations.put(item.id, values);
Sunny Goyale5bb7052015-07-27 14:36:07 -0700360 }
361
362 /**
363 * Tries the remove the provided row and column.
Ryan Lothianfa530cd2018-10-12 14:14:16 -0400364 *
Sunny Goyale5bb7052015-07-27 14:36:07 -0700365 * @param items all the items on the screen under operation
366 * @param outLoss array of size 2. The first entry is filled with weight loss, and the second
367 * with the overall item movement.
368 */
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700369 private ArrayList<DbEntry> tryRemove(int col, int row, int startY,
370 ArrayList<DbEntry> items, float[] outLoss) {
Sunny Goyalff4ba2d2016-04-02 14:12:34 -0700371 GridOccupancy occupied = new GridOccupancy(mTrgX, mTrgY);
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700372 occupied.markCells(0, 0, mTrgX, startY, true);
Sunny Goyale5bb7052015-07-27 14:36:07 -0700373
374 col = mShouldRemoveX ? col : Integer.MAX_VALUE;
375 row = mShouldRemoveY ? row : Integer.MAX_VALUE;
376
377 ArrayList<DbEntry> finalItems = new ArrayList<>();
378 ArrayList<DbEntry> removedItems = new ArrayList<>();
379
380 for (DbEntry item : items) {
381 if ((item.cellX <= col && (item.spanX + item.cellX) > col)
382 || (item.cellY <= row && (item.spanY + item.cellY) > row)) {
383 removedItems.add(item);
384 if (item.cellX >= col) item.cellX --;
385 if (item.cellY >= row) item.cellY --;
386 } else {
387 if (item.cellX > col) item.cellX --;
388 if (item.cellY > row) item.cellY --;
389 finalItems.add(item);
Sunny Goyalff4ba2d2016-04-02 14:12:34 -0700390 occupied.markCells(item, true);
Sunny Goyale5bb7052015-07-27 14:36:07 -0700391 }
392 }
393
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700394 OptimalPlacementSolution placement =
395 new OptimalPlacementSolution(occupied, removedItems, startY);
Sunny Goyale5bb7052015-07-27 14:36:07 -0700396 placement.find();
397 finalItems.addAll(placement.finalPlacedItems);
398 outLoss[0] = placement.lowestWeightLoss;
399 outLoss[1] = placement.lowestMoveCost;
400 return finalItems;
401 }
402
Sunny Goyale5bb7052015-07-27 14:36:07 -0700403 private class OptimalPlacementSolution {
404 private final ArrayList<DbEntry> itemsToPlace;
Sunny Goyalff4ba2d2016-04-02 14:12:34 -0700405 private final GridOccupancy occupied;
Sunny Goyale5bb7052015-07-27 14:36:07 -0700406
407 // If set to true, item movement are not considered in move cost, leading to a more
408 // linear placement.
409 private final boolean ignoreMove;
410
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700411 // The first row in the grid from where the placement should start.
412 private final int startY;
413
Sunny Goyale5bb7052015-07-27 14:36:07 -0700414 float lowestWeightLoss = Float.MAX_VALUE;
415 float lowestMoveCost = Float.MAX_VALUE;
416 ArrayList<DbEntry> finalPlacedItems;
417
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700418 public OptimalPlacementSolution(
419 GridOccupancy occupied, ArrayList<DbEntry> itemsToPlace, int startY) {
420 this(occupied, itemsToPlace, startY, false);
Sunny Goyale5bb7052015-07-27 14:36:07 -0700421 }
422
Sunny Goyalff4ba2d2016-04-02 14:12:34 -0700423 public OptimalPlacementSolution(GridOccupancy occupied, ArrayList<DbEntry> itemsToPlace,
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700424 int startY, boolean ignoreMove) {
Sunny Goyale5bb7052015-07-27 14:36:07 -0700425 this.occupied = occupied;
426 this.itemsToPlace = itemsToPlace;
427 this.ignoreMove = ignoreMove;
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700428 this.startY = startY;
Sunny Goyale5bb7052015-07-27 14:36:07 -0700429
430 // Sort the items such that larger widgets appear first followed by 1x1 items
431 Collections.sort(this.itemsToPlace);
432 }
433
434 public void find() {
435 find(0, 0, 0, new ArrayList<DbEntry>());
436 }
437
438 /**
439 * Recursively finds a placement for the provided items.
Ryan Lothianfa530cd2018-10-12 14:14:16 -0400440 *
Sunny Goyale5bb7052015-07-27 14:36:07 -0700441 * @param index the position in {@link #itemsToPlace} to start looking at.
442 * @param weightLoss total weight loss upto this point
443 * @param moveCost total move cost upto this point
444 * @param itemsPlaced all the items already placed upto this point
445 */
446 public void find(int index, float weightLoss, float moveCost,
447 ArrayList<DbEntry> itemsPlaced) {
448 if ((weightLoss >= lowestWeightLoss) ||
449 ((weightLoss == lowestWeightLoss) && (moveCost >= lowestMoveCost))) {
450 // Abort, as we already have a better solution.
451 return;
452
453 } else if (index >= itemsToPlace.size()) {
454 // End loop.
455 lowestWeightLoss = weightLoss;
456 lowestMoveCost = moveCost;
457
458 // Keep a deep copy of current configuration as it can change during recursion.
459 finalPlacedItems = deepCopy(itemsPlaced);
460 return;
461 }
462
463 DbEntry me = itemsToPlace.get(index);
464 int myX = me.cellX;
465 int myY = me.cellY;
466
467 // List of items to pass over if this item was placed.
468 ArrayList<DbEntry> itemsIncludingMe = new ArrayList<>(itemsPlaced.size() + 1);
469 itemsIncludingMe.addAll(itemsPlaced);
470 itemsIncludingMe.add(me);
471
472 if (me.spanX > 1 || me.spanY > 1) {
473 // If the current item is a widget (and it greater than 1x1), try to place it at
474 // all possible positions. This is because a widget placed at one position can
475 // affect the placement of a different widget.
476 int myW = me.spanX;
477 int myH = me.spanY;
478
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700479 for (int y = startY; y < mTrgY; y++) {
Sunny Goyale5bb7052015-07-27 14:36:07 -0700480 for (int x = 0; x < mTrgX; x++) {
481 float newMoveCost = moveCost;
482 if (x != myX) {
483 me.cellX = x;
484 newMoveCost ++;
485 }
486 if (y != myY) {
487 me.cellY = y;
488 newMoveCost ++;
489 }
490 if (ignoreMove) {
491 newMoveCost = moveCost;
492 }
493
Sunny Goyalff4ba2d2016-04-02 14:12:34 -0700494 if (occupied.isRegionVacant(x, y, myW, myH)) {
Sunny Goyale5bb7052015-07-27 14:36:07 -0700495 // place at this position and continue search.
Sunny Goyalff4ba2d2016-04-02 14:12:34 -0700496 occupied.markCells(me, true);
Sunny Goyale5bb7052015-07-27 14:36:07 -0700497 find(index + 1, weightLoss, newMoveCost, itemsIncludingMe);
Sunny Goyalff4ba2d2016-04-02 14:12:34 -0700498 occupied.markCells(me, false);
Sunny Goyale5bb7052015-07-27 14:36:07 -0700499 }
500
501 // Try resizing horizontally
Sunny Goyalff4ba2d2016-04-02 14:12:34 -0700502 if (myW > me.minSpanX && occupied.isRegionVacant(x, y, myW - 1, myH)) {
Sunny Goyale5bb7052015-07-27 14:36:07 -0700503 me.spanX --;
Sunny Goyalff4ba2d2016-04-02 14:12:34 -0700504 occupied.markCells(me, true);
Sunny Goyale5bb7052015-07-27 14:36:07 -0700505 // 1 extra move cost
506 find(index + 1, weightLoss, newMoveCost + 1, itemsIncludingMe);
Sunny Goyalff4ba2d2016-04-02 14:12:34 -0700507 occupied.markCells(me, false);
Sunny Goyale5bb7052015-07-27 14:36:07 -0700508 me.spanX ++;
509 }
510
511 // Try resizing vertically
Sunny Goyalff4ba2d2016-04-02 14:12:34 -0700512 if (myH > me.minSpanY && occupied.isRegionVacant(x, y, myW, myH - 1)) {
Sunny Goyale5bb7052015-07-27 14:36:07 -0700513 me.spanY --;
Sunny Goyalff4ba2d2016-04-02 14:12:34 -0700514 occupied.markCells(me, true);
Sunny Goyale5bb7052015-07-27 14:36:07 -0700515 // 1 extra move cost
516 find(index + 1, weightLoss, newMoveCost + 1, itemsIncludingMe);
Sunny Goyalff4ba2d2016-04-02 14:12:34 -0700517 occupied.markCells(me, false);
Sunny Goyale5bb7052015-07-27 14:36:07 -0700518 me.spanY ++;
519 }
520
521 // Try resizing horizontally & vertically
522 if (myH > me.minSpanY && myW > me.minSpanX &&
Sunny Goyalff4ba2d2016-04-02 14:12:34 -0700523 occupied.isRegionVacant(x, y, myW - 1, myH - 1)) {
Sunny Goyale5bb7052015-07-27 14:36:07 -0700524 me.spanX --;
525 me.spanY --;
Sunny Goyalff4ba2d2016-04-02 14:12:34 -0700526 occupied.markCells(me, true);
Sunny Goyale5bb7052015-07-27 14:36:07 -0700527 // 2 extra move cost
528 find(index + 1, weightLoss, newMoveCost + 2, itemsIncludingMe);
Sunny Goyalff4ba2d2016-04-02 14:12:34 -0700529 occupied.markCells(me, false);
Sunny Goyale5bb7052015-07-27 14:36:07 -0700530 me.spanX ++;
531 me.spanY ++;
532 }
533 me.cellX = myX;
534 me.cellY = myY;
535 }
536 }
537
538 // Finally also try a solution when this item is not included. Trying it in the end
539 // causes it to get skipped in most cases due to higher weight loss, and prevents
540 // unnecessary deep copies of various configurations.
541 find(index + 1, weightLoss + me.weight, moveCost, itemsPlaced);
542 } else {
543 // Since this is a 1x1 item and all the following items are also 1x1, just place
544 // it at 'the most appropriate position' and hope for the best.
545 // The most appropriate position: one with lease straight line distance
546 int newDistance = Integer.MAX_VALUE;
547 int newX = Integer.MAX_VALUE, newY = Integer.MAX_VALUE;
548
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700549 for (int y = startY; y < mTrgY; y++) {
Sunny Goyale5bb7052015-07-27 14:36:07 -0700550 for (int x = 0; x < mTrgX; x++) {
Sunny Goyalff4ba2d2016-04-02 14:12:34 -0700551 if (!occupied.cells[x][y]) {
Sunny Goyale5bb7052015-07-27 14:36:07 -0700552 int dist = ignoreMove ? 0 :
Ryan Lothianfa530cd2018-10-12 14:14:16 -0400553 ((me.cellX - x) * (me.cellX - x) + (me.cellY - y) * (me.cellY
554 - y));
Sunny Goyale5bb7052015-07-27 14:36:07 -0700555 if (dist < newDistance) {
556 newX = x;
557 newY = y;
558 newDistance = dist;
559 }
560 }
561 }
562 }
563
564 if (newX < mTrgX && newY < mTrgY) {
565 float newMoveCost = moveCost;
566 if (newX != myX) {
567 me.cellX = newX;
568 newMoveCost ++;
569 }
570 if (newY != myY) {
571 me.cellY = newY;
572 newMoveCost ++;
573 }
574 if (ignoreMove) {
575 newMoveCost = moveCost;
576 }
Sunny Goyalff4ba2d2016-04-02 14:12:34 -0700577 occupied.markCells(me, true);
Sunny Goyale5bb7052015-07-27 14:36:07 -0700578 find(index + 1, weightLoss, newMoveCost, itemsIncludingMe);
Sunny Goyalff4ba2d2016-04-02 14:12:34 -0700579 occupied.markCells(me, false);
Sunny Goyale5bb7052015-07-27 14:36:07 -0700580 me.cellX = myX;
581 me.cellY = myY;
582
583 // Try to find a solution without this item, only if
584 // 1) there was at least one space, i.e., we were able to place this item
585 // 2) if the next item has the same weight (all items are already sorted), as
586 // if it has lower weight, that solution will automatically get discarded.
587 // 3) ignoreMove false otherwise, move cost is ignored and the weight will
588 // anyway be same.
589 if (index + 1 < itemsToPlace.size()
590 && itemsToPlace.get(index + 1).weight >= me.weight && !ignoreMove) {
591 find(index + 1, weightLoss + me.weight, moveCost, itemsPlaced);
592 }
593 } else {
594 // No more space. Jump to the end.
595 for (int i = index + 1; i < itemsToPlace.size(); i++) {
596 weightLoss += itemsToPlace.get(i).weight;
597 }
598 find(itemsToPlace.size(), weightLoss + me.weight, moveCost, itemsPlaced);
599 }
600 }
601 }
602 }
603
Sunny Goyalf862a262015-12-14 14:27:38 -0800604 private ArrayList<DbEntry> loadHotseatEntries() {
Sunny Goyal161a2142018-10-29 14:02:20 -0700605 Cursor c = queryWorkspace(
Sunny Goyalf862a262015-12-14 14:27:38 -0800606 new String[]{
607 Favorites._ID, // 0
608 Favorites.ITEM_TYPE, // 1
609 Favorites.INTENT, // 2
610 Favorites.SCREEN}, // 3
Sunny Goyal161a2142018-10-29 14:02:20 -0700611 Favorites.CONTAINER + " = " + Favorites.CONTAINER_HOTSEAT);
Sunny Goyalf862a262015-12-14 14:27:38 -0800612
613 final int indexId = c.getColumnIndexOrThrow(Favorites._ID);
614 final int indexItemType = c.getColumnIndexOrThrow(Favorites.ITEM_TYPE);
615 final int indexIntent = c.getColumnIndexOrThrow(Favorites.INTENT);
616 final int indexScreen = c.getColumnIndexOrThrow(Favorites.SCREEN);
617
618 ArrayList<DbEntry> entries = new ArrayList<>();
619 while (c.moveToNext()) {
620 DbEntry entry = new DbEntry();
Sunny Goyalefb7e842018-10-04 15:11:00 -0700621 entry.id = c.getInt(indexId);
Sunny Goyalf862a262015-12-14 14:27:38 -0800622 entry.itemType = c.getInt(indexItemType);
Sunny Goyalefb7e842018-10-04 15:11:00 -0700623 entry.screenId = c.getInt(indexScreen);
Sunny Goyalf862a262015-12-14 14:27:38 -0800624
625 if (entry.screenId >= mSrcHotseatSize) {
626 mEntryToRemove.add(entry.id);
627 continue;
628 }
629
630 try {
631 // calculate weight
632 switch (entry.itemType) {
633 case Favorites.ITEM_TYPE_SHORTCUT:
Tony Wickhambfbf7f92016-05-19 11:19:39 -0700634 case Favorites.ITEM_TYPE_DEEP_SHORTCUT:
Sunny Goyalf862a262015-12-14 14:27:38 -0800635 case Favorites.ITEM_TYPE_APPLICATION: {
636 verifyIntent(c.getString(indexIntent));
Tony Wickhambfbf7f92016-05-19 11:19:39 -0700637 entry.weight = entry.itemType == Favorites.ITEM_TYPE_APPLICATION ?
638 WT_APPLICATION : WT_SHORTCUT;
Sunny Goyalf862a262015-12-14 14:27:38 -0800639 break;
640 }
641 case Favorites.ITEM_TYPE_FOLDER: {
642 int total = getFolderItemsCount(entry.id);
643 if (total == 0) {
644 throw new Exception("Folder is empty");
645 }
646 entry.weight = WT_FOLDER_FACTOR * total;
647 break;
648 }
649 default:
650 throw new Exception("Invalid item type");
651 }
652 } catch (Exception e) {
653 if (DEBUG) {
654 Log.d(TAG, "Removing item " + entry.id, e);
655 }
656 mEntryToRemove.add(entry.id);
657 continue;
658 }
659 entries.add(entry);
660 }
661 c.close();
662 return entries;
663 }
664
665
Sunny Goyale5bb7052015-07-27 14:36:07 -0700666 /**
667 * Loads entries for a particular screen id.
668 */
Sunny Goyalefb7e842018-10-04 15:11:00 -0700669 protected ArrayList<DbEntry> loadWorkspaceEntries(int screen) {
Sunny Goyala9e2f5a2016-06-10 12:22:04 -0700670 Cursor c = queryWorkspace(
Sunny Goyalf076eae2016-01-11 12:25:10 -0800671 new String[]{
672 Favorites._ID, // 0
673 Favorites.ITEM_TYPE, // 1
674 Favorites.CELLX, // 2
675 Favorites.CELLY, // 3
676 Favorites.SPANX, // 4
677 Favorites.SPANY, // 5
678 Favorites.INTENT, // 6
Sunny Goyal2e1efb42016-03-03 16:58:55 -0800679 Favorites.APPWIDGET_PROVIDER, // 7
680 Favorites.APPWIDGET_ID}, // 8
Sunny Goyale5bb7052015-07-27 14:36:07 -0700681 Favorites.CONTAINER + " = " + Favorites.CONTAINER_DESKTOP
Sunny Goyala9e2f5a2016-06-10 12:22:04 -0700682 + " AND " + Favorites.SCREEN + " = " + screen);
Sunny Goyale5bb7052015-07-27 14:36:07 -0700683
Sunny Goyalf076eae2016-01-11 12:25:10 -0800684 final int indexId = c.getColumnIndexOrThrow(Favorites._ID);
685 final int indexItemType = c.getColumnIndexOrThrow(Favorites.ITEM_TYPE);
686 final int indexCellX = c.getColumnIndexOrThrow(Favorites.CELLX);
687 final int indexCellY = c.getColumnIndexOrThrow(Favorites.CELLY);
688 final int indexSpanX = c.getColumnIndexOrThrow(Favorites.SPANX);
689 final int indexSpanY = c.getColumnIndexOrThrow(Favorites.SPANY);
690 final int indexIntent = c.getColumnIndexOrThrow(Favorites.INTENT);
691 final int indexAppWidgetProvider = c.getColumnIndexOrThrow(Favorites.APPWIDGET_PROVIDER);
Sunny Goyal2e1efb42016-03-03 16:58:55 -0800692 final int indexAppWidgetId = c.getColumnIndexOrThrow(Favorites.APPWIDGET_ID);
Sunny Goyale5bb7052015-07-27 14:36:07 -0700693
Sunny Goyalf076eae2016-01-11 12:25:10 -0800694 ArrayList<DbEntry> entries = new ArrayList<>();
Sunny Goyal337c81f2019-12-10 12:19:13 -0800695 WidgetManagerHelper widgetManagerHelper = new WidgetManagerHelper(mContext);
Sunny Goyalf076eae2016-01-11 12:25:10 -0800696 while (c.moveToNext()) {
697 DbEntry entry = new DbEntry();
Sunny Goyalefb7e842018-10-04 15:11:00 -0700698 entry.id = c.getInt(indexId);
Sunny Goyalf076eae2016-01-11 12:25:10 -0800699 entry.itemType = c.getInt(indexItemType);
700 entry.cellX = c.getInt(indexCellX);
701 entry.cellY = c.getInt(indexCellY);
702 entry.spanX = c.getInt(indexSpanX);
703 entry.spanY = c.getInt(indexSpanY);
704 entry.screenId = screen;
Sunny Goyale5bb7052015-07-27 14:36:07 -0700705
Sunny Goyalf076eae2016-01-11 12:25:10 -0800706 try {
707 // calculate weight
708 switch (entry.itemType) {
709 case Favorites.ITEM_TYPE_SHORTCUT:
Tony Wickhambfbf7f92016-05-19 11:19:39 -0700710 case Favorites.ITEM_TYPE_DEEP_SHORTCUT:
Sunny Goyalf076eae2016-01-11 12:25:10 -0800711 case Favorites.ITEM_TYPE_APPLICATION: {
712 verifyIntent(c.getString(indexIntent));
Tony Wickhambfbf7f92016-05-19 11:19:39 -0700713 entry.weight = entry.itemType == Favorites.ITEM_TYPE_APPLICATION ?
714 WT_APPLICATION : WT_SHORTCUT;
Sunny Goyalf076eae2016-01-11 12:25:10 -0800715 break;
716 }
717 case Favorites.ITEM_TYPE_APPWIDGET: {
718 String provider = c.getString(indexAppWidgetProvider);
719 ComponentName cn = ComponentName.unflattenFromString(provider);
720 verifyPackage(cn.getPackageName());
721 entry.weight = Math.max(WT_WIDGET_MIN, WT_WIDGET_FACTOR
722 * entry.spanX * entry.spanY);
Sunny Goyale5bb7052015-07-27 14:36:07 -0700723
Sunny Goyal2e1efb42016-03-03 16:58:55 -0800724 int widgetId = c.getInt(indexAppWidgetId);
Sunny Goyal337c81f2019-12-10 12:19:13 -0800725 LauncherAppWidgetProviderInfo pInfo =
726 widgetManagerHelper.getLauncherAppWidgetInfo(widgetId);
Rajeev Kumar0ce439f2017-06-12 17:28:07 -0700727 Point spans = null;
728 if (pInfo != null) {
Sunny Goyal952e63d2017-08-16 04:59:08 -0700729 spans = pInfo.getMinSpans();
Rajeev Kumar0ce439f2017-06-12 17:28:07 -0700730 }
Sunny Goyalf076eae2016-01-11 12:25:10 -0800731 if (spans != null) {
732 entry.minSpanX = spans.x > 0 ? spans.x : entry.spanX;
733 entry.minSpanY = spans.y > 0 ? spans.y : entry.spanY;
734 } else {
735 // Assume that the widget be resized down to 2x2
736 entry.minSpanX = entry.minSpanY = 2;
737 }
Sunny Goyale5bb7052015-07-27 14:36:07 -0700738
Sunny Goyalf076eae2016-01-11 12:25:10 -0800739 if (entry.minSpanX > mTrgX || entry.minSpanY > mTrgY) {
740 throw new Exception("Widget can't be resized down to fit the grid");
741 }
742 break;
743 }
744 case Favorites.ITEM_TYPE_FOLDER: {
745 int total = getFolderItemsCount(entry.id);
746 if (total == 0) {
747 throw new Exception("Folder is empty");
748 }
749 entry.weight = WT_FOLDER_FACTOR * total;
750 break;
751 }
752 default:
753 throw new Exception("Invalid item type");
754 }
755 } catch (Exception e) {
756 if (DEBUG) {
757 Log.d(TAG, "Removing item " + entry.id, e);
758 }
759 mEntryToRemove.add(entry.id);
760 continue;
761 }
762 entries.add(entry);
763 }
764 c.close();
765 return entries;
Sunny Goyale5bb7052015-07-27 14:36:07 -0700766 }
767
768 /**
769 * @return the number of valid items in the folder.
770 */
Sunny Goyalefb7e842018-10-04 15:11:00 -0700771 private int getFolderItemsCount(int folderId) {
Sunny Goyala9e2f5a2016-06-10 12:22:04 -0700772 Cursor c = queryWorkspace(
Sunny Goyalf076eae2016-01-11 12:25:10 -0800773 new String[]{Favorites._ID, Favorites.INTENT},
Sunny Goyala9e2f5a2016-06-10 12:22:04 -0700774 Favorites.CONTAINER + " = " + folderId);
Sunny Goyale5bb7052015-07-27 14:36:07 -0700775
776 int total = 0;
777 while (c.moveToNext()) {
778 try {
779 verifyIntent(c.getString(1));
780 total++;
781 } catch (Exception e) {
Sunny Goyalefb7e842018-10-04 15:11:00 -0700782 mEntryToRemove.add(c.getInt(0));
Sunny Goyale5bb7052015-07-27 14:36:07 -0700783 }
784 }
Tony Wickhamfb062c62015-10-16 10:12:23 -0700785 c.close();
Sunny Goyale5bb7052015-07-27 14:36:07 -0700786 return total;
787 }
788
Sunny Goyala9e2f5a2016-06-10 12:22:04 -0700789 protected Cursor queryWorkspace(String[] columns, String where) {
Sunny Goyal161a2142018-10-29 14:02:20 -0700790 return mDb.query(Favorites.TABLE_NAME, columns, where, null, null, null, null);
Sunny Goyala9e2f5a2016-06-10 12:22:04 -0700791 }
792
Sunny Goyale5bb7052015-07-27 14:36:07 -0700793 /**
794 * Verifies if the intent should be restored.
795 */
796 private void verifyIntent(String intentStr) throws Exception {
797 Intent intent = Intent.parseUri(intentStr, 0);
798 if (intent.getComponent() != null) {
799 verifyPackage(intent.getComponent().getPackageName());
800 } else if (intent.getPackage() != null) {
801 // Only verify package if the component was null.
802 verifyPackage(intent.getPackage());
803 }
804 }
805
806 /**
807 * Verifies if the package should be restored
808 */
809 private void verifyPackage(String packageName) throws Exception {
810 if (!mValidPackages.contains(packageName)) {
811 throw new Exception("Package not available");
812 }
813 }
814
Sunny Goyala9e2f5a2016-06-10 12:22:04 -0700815 protected static class DbEntry extends ItemInfo implements Comparable<DbEntry> {
Sunny Goyale5bb7052015-07-27 14:36:07 -0700816
817 public float weight;
818
Ryan Lothianfa530cd2018-10-12 14:14:16 -0400819 public DbEntry() {
820 }
Sunny Goyale5bb7052015-07-27 14:36:07 -0700821
822 public DbEntry copy() {
823 DbEntry entry = new DbEntry();
824 entry.copyFrom(this);
825 entry.weight = weight;
826 entry.minSpanX = minSpanX;
827 entry.minSpanY = minSpanY;
828 return entry;
829 }
830
831 /**
832 * Comparator such that larger widgets come first, followed by all 1x1 items
833 * based on their weights.
834 */
835 @Override
836 public int compareTo(DbEntry another) {
837 if (itemType == Favorites.ITEM_TYPE_APPWIDGET) {
838 if (another.itemType == Favorites.ITEM_TYPE_APPWIDGET) {
839 return another.spanY * another.spanX - spanX * spanY;
840 } else {
841 return -1;
842 }
843 } else if (another.itemType == Favorites.ITEM_TYPE_APPWIDGET) {
844 return 1;
845 } else {
846 // Place higher weight before lower weight.
847 return Float.compare(another.weight, weight);
848 }
849 }
850
851 public boolean columnsSame(DbEntry org) {
852 return org.cellX == cellX && org.cellY == cellY && org.spanX == spanX &&
853 org.spanY == spanY && org.screenId == screenId;
854 }
855
856 public void addToContentValues(ContentValues values) {
Sunny Goyal161a2142018-10-29 14:02:20 -0700857 values.put(Favorites.SCREEN, screenId);
858 values.put(Favorites.CELLX, cellX);
859 values.put(Favorites.CELLY, cellY);
860 values.put(Favorites.SPANX, spanX);
861 values.put(Favorites.SPANY, spanY);
Sunny Goyale5bb7052015-07-27 14:36:07 -0700862 }
863 }
864
Sunny Goyalf862a262015-12-14 14:27:38 -0800865 private static ArrayList<DbEntry> deepCopy(ArrayList<DbEntry> src) {
Rajeev Kumar0ce439f2017-06-12 17:28:07 -0700866 ArrayList<DbEntry> dup = new ArrayList<>(src.size());
Sunny Goyale5bb7052015-07-27 14:36:07 -0700867 for (DbEntry e : src) {
868 dup.add(e.copy());
869 }
870 return dup;
871 }
872
Sunny Goyala5c8a9e2016-07-08 08:32:44 -0700873 public static void markForMigration(
874 Context context, int gridX, int gridY, int hotseatSize) {
875 Utilities.getPrefs(context).edit()
876 .putString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, getPointString(gridX, gridY))
877 .putInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, hotseatSize)
878 .apply();
879 }
880
Sunny Goyalf076eae2016-01-11 12:25:10 -0800881 /**
882 * Migrates the workspace and hotseat in case their sizes changed.
Ryan Lothianfa530cd2018-10-12 14:14:16 -0400883 *
Sunny Goyalf076eae2016-01-11 12:25:10 -0800884 * @return false if the migration failed.
885 */
886 public static boolean migrateGridIfNeeded(Context context) {
887 SharedPreferences prefs = Utilities.getPrefs(context);
Sunny Goyal87f784c2017-01-11 10:48:34 -0800888 InvariantDeviceProfile idp = LauncherAppState.getIDP(context);
Sunny Goyalf076eae2016-01-11 12:25:10 -0800889
890 String gridSizeString = getPointString(idp.numColumns, idp.numRows);
Sunny Goyalf076eae2016-01-11 12:25:10 -0800891
892 if (gridSizeString.equals(prefs.getString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, "")) &&
Ryan Lothianfa530cd2018-10-12 14:14:16 -0400893 idp.numHotseatIcons == prefs.getInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT,
894 idp.numHotseatIcons)) {
Sunny Goyalf076eae2016-01-11 12:25:10 -0800895 // Skip if workspace and hotseat sizes have not changed.
896 return true;
897 }
898
899 long migrationStartTime = System.currentTimeMillis();
Sunny Goyal161a2142018-10-29 14:02:20 -0700900 try (SQLiteTransaction transaction = (SQLiteTransaction) Settings.call(
901 context.getContentResolver(), Settings.METHOD_NEW_TRANSACTION)
902 .getBinder(Settings.EXTRA_VALUE)) {
903
904 int srcHotseatCount = prefs.getInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT,
905 idp.numHotseatIcons);
906 Point sourceSize = parsePoint(prefs.getString(
907 KEY_MIGRATION_SRC_WORKSPACE_SIZE, gridSizeString));
908
Sunny Goyalf076eae2016-01-11 12:25:10 -0800909 boolean dbChanged = false;
910
Sunny Goyal161a2142018-10-29 14:02:20 -0700911 GridBackupTable backupTable = new GridBackupTable(context, transaction.getDb(),
Tracy Zhou7df93d22020-01-27 13:44:06 -0800912 transaction.getDb(), srcHotseatCount, sourceSize.x, sourceSize.y);
Sunny Goyal161a2142018-10-29 14:02:20 -0700913 if (backupTable.backupOrRestoreAsNeeded()) {
914 dbChanged = true;
915 srcHotseatCount = backupTable.getRestoreHotseatAndGridSize(sourceSize);
916 }
917
Rajeev Kumar0ce439f2017-06-12 17:28:07 -0700918 HashSet<String> validPackages = getValidPackages(context);
Sunny Goyalf076eae2016-01-11 12:25:10 -0800919 // Hotseat
Sunny Goyalbb011da2016-06-15 15:42:29 -0700920 if (srcHotseatCount != idp.numHotseatIcons) {
Sunny Goyalf076eae2016-01-11 12:25:10 -0800921 // Migrate hotseat.
Sunny Goyal161a2142018-10-29 14:02:20 -0700922 dbChanged = new GridSizeMigrationTask(context, transaction.getDb(),
Sunny Goyalbb011da2016-06-15 15:42:29 -0700923 validPackages, srcHotseatCount, idp.numHotseatIcons).migrateHotseat();
Sunny Goyalf076eae2016-01-11 12:25:10 -0800924 }
925
926 // Grid size
927 Point targetSize = new Point(idp.numColumns, idp.numRows);
Sunny Goyal161a2142018-10-29 14:02:20 -0700928 if (new MultiStepMigrationTask(validPackages, context, transaction.getDb())
929 .migrate(sourceSize, targetSize)) {
Sunny Goyala5c8a9e2016-07-08 08:32:44 -0700930 dbChanged = true;
Sunny Goyalf076eae2016-01-11 12:25:10 -0800931 }
932
933 if (dbChanged) {
934 // Make sure we haven't removed everything.
935 final Cursor c = context.getContentResolver().query(
Sunny Goyal161a2142018-10-29 14:02:20 -0700936 Favorites.CONTENT_URI, null, null, null, null);
Sunny Goyalf076eae2016-01-11 12:25:10 -0800937 boolean hasData = c.moveToNext();
938 c.close();
939 if (!hasData) {
940 throw new Exception("Removed every thing during grid resize");
941 }
942 }
943
Sunny Goyal161a2142018-10-29 14:02:20 -0700944 transaction.commit();
945 Settings.call(context.getContentResolver(), Settings.METHOD_REFRESH_BACKUP_TABLE);
Sunny Goyalf076eae2016-01-11 12:25:10 -0800946 return true;
947 } catch (Exception e) {
948 Log.e(TAG, "Error during grid migration", e);
949
950 return false;
951 } finally {
952 Log.v(TAG, "Workspace migration completed in "
953 + (System.currentTimeMillis() - migrationStartTime));
954
955 // Save current configuration, so that the migration does not run again.
956 prefs.edit()
957 .putString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, gridSizeString)
Sunny Goyalbb011da2016-06-15 15:42:29 -0700958 .putInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, idp.numHotseatIcons)
Sunny Goyalf076eae2016-01-11 12:25:10 -0800959 .apply();
960 }
961 }
Sunny Goyala9e2f5a2016-06-10 12:22:04 -0700962
963 protected static HashSet<String> getValidPackages(Context context) {
964 // Initialize list of valid packages. This contain all the packages which are already on
965 // the device and packages which are being installed. Any item which doesn't belong to
966 // this set is removed.
967 // Since the loader removes such items anyway, removing these items here doesn't cause
968 // any extra data loss and gives us more free space on the grid for better migration.
Rajeev Kumar0ce439f2017-06-12 17:28:07 -0700969 HashSet<String> validPackages = new HashSet<>();
Sunny Goyala9e2f5a2016-06-10 12:22:04 -0700970 for (PackageInfo info : context.getPackageManager()
971 .getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES)) {
972 validPackages.add(info.packageName);
973 }
Sunny Goyal73b5a272019-12-09 14:55:56 -0800974 InstallSessionHelper.INSTANCE.get(context)
Sunny Goyal045b4fa2019-09-20 12:51:37 -0700975 .getActiveSessions().keySet()
Jon Mirandac1322b62019-09-16 21:38:28 -0700976 .forEach(packageUserKey -> validPackages.add(packageUserKey.mPackageName));
Sunny Goyala9e2f5a2016-06-10 12:22:04 -0700977 return validPackages;
978 }
Sunny Goyala5c8a9e2016-07-08 08:32:44 -0700979
980 /**
Sunny Goyald70ef242016-08-24 11:30:33 -0700981 * Removes any broken item from the hotseat.
Ryan Lothianfa530cd2018-10-12 14:14:16 -0400982 *
Sunny Goyald70ef242016-08-24 11:30:33 -0700983 * @return a map with occupied hotseat position set to non-null value.
984 */
Ryan Lothianfa530cd2018-10-12 14:14:16 -0400985 public static IntSparseArrayMap<Object> removeBrokenHotseatItems(Context context)
986 throws Exception {
Sunny Goyal161a2142018-10-29 14:02:20 -0700987 try (SQLiteTransaction transaction = (SQLiteTransaction) Settings.call(
988 context.getContentResolver(), Settings.METHOD_NEW_TRANSACTION)
989 .getBinder(Settings.EXTRA_VALUE)) {
990 GridSizeMigrationTask task = new GridSizeMigrationTask(
991 context, transaction.getDb(), getValidPackages(context),
992 Integer.MAX_VALUE, Integer.MAX_VALUE);
Sunny Goyald70ef242016-08-24 11:30:33 -0700993
Sunny Goyal161a2142018-10-29 14:02:20 -0700994 // Load all the valid entries
995 ArrayList<DbEntry> items = task.loadHotseatEntries();
996 // Delete any entry marked for deletion by above load.
997 task.applyOperations();
998 IntSparseArrayMap<Object> positions = new IntSparseArrayMap<>();
999 for (DbEntry item : items) {
1000 positions.put(item.screenId, item);
1001 }
1002 transaction.commit();
1003 return positions;
Sunny Goyald70ef242016-08-24 11:30:33 -07001004 }
Sunny Goyald70ef242016-08-24 11:30:33 -07001005 }
1006
1007 /**
Sunny Goyala5c8a9e2016-07-08 08:32:44 -07001008 * Task to run grid migration in multiple steps when the size difference is more than 1.
1009 */
1010 protected static class MultiStepMigrationTask {
1011 private final HashSet<String> mValidPackages;
1012 private final Context mContext;
Sunny Goyal161a2142018-10-29 14:02:20 -07001013 private final SQLiteDatabase mDb;
Sunny Goyala5c8a9e2016-07-08 08:32:44 -07001014
Sunny Goyal161a2142018-10-29 14:02:20 -07001015 public MultiStepMigrationTask(HashSet<String> validPackages, Context context,
1016 SQLiteDatabase db) {
Sunny Goyala5c8a9e2016-07-08 08:32:44 -07001017 mValidPackages = validPackages;
1018 mContext = context;
Sunny Goyal161a2142018-10-29 14:02:20 -07001019 mDb = db;
Sunny Goyala5c8a9e2016-07-08 08:32:44 -07001020 }
1021
1022 public boolean migrate(Point sourceSize, Point targetSize) throws Exception {
1023 boolean dbChanged = false;
1024 if (!targetSize.equals(sourceSize)) {
1025 if (sourceSize.x < targetSize.x) {
1026 // Source is smaller that target, just expand the grid without actual migration.
1027 sourceSize.x = targetSize.x;
1028 }
1029 if (sourceSize.y < targetSize.y) {
1030 // Source is smaller that target, just expand the grid without actual migration.
1031 sourceSize.y = targetSize.y;
1032 }
1033
1034 // Migrate the workspace grid, such that the points differ by max 1 in x and y
1035 // each on every step.
1036 while (!targetSize.equals(sourceSize)) {
1037 // Get the next size, such that the points differ by max 1 in x and y each
1038 Point nextSize = new Point(sourceSize);
1039 if (targetSize.x < nextSize.x) {
1040 nextSize.x--;
1041 }
1042 if (targetSize.y < nextSize.y) {
1043 nextSize.y--;
1044 }
1045 if (runStepTask(sourceSize, nextSize)) {
1046 dbChanged = true;
1047 }
1048 sourceSize.set(nextSize.x, nextSize.y);
1049 }
1050 }
1051 return dbChanged;
1052 }
1053
1054 protected boolean runStepTask(Point sourceSize, Point nextSize) throws Exception {
Sunny Goyal161a2142018-10-29 14:02:20 -07001055 return new GridSizeMigrationTask(mContext, mDb,
Sunny Goyala5c8a9e2016-07-08 08:32:44 -07001056 mValidPackages, sourceSize, nextSize).migrateWorkspace();
1057 }
1058 }
Sunny Goyale5bb7052015-07-27 14:36:07 -07001059}