blob: faecc067e46812b1e5cd58460ac528bf21827999 [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 Goyale5bb7052015-07-27 14:36:07 -070020import com.android.launcher3.InvariantDeviceProfile;
21import com.android.launcher3.ItemInfo;
22import com.android.launcher3.LauncherAppState;
23import com.android.launcher3.LauncherAppWidgetProviderInfo;
Sunny Goyale5bb7052015-07-27 14:36:07 -070024import com.android.launcher3.LauncherSettings;
25import com.android.launcher3.LauncherSettings.Favorites;
Sunny Goyal161a2142018-10-29 14:02:20 -070026import com.android.launcher3.LauncherSettings.Settings;
Sunny Goyale5bb7052015-07-27 14:36:07 -070027import com.android.launcher3.Utilities;
Sunny Goyalda4fe1a2016-05-26 16:05:17 -070028import com.android.launcher3.Workspace;
Sunny Goyal2e1efb42016-03-03 16:58:55 -080029import com.android.launcher3.compat.AppWidgetManagerCompat;
Sunny Goyale5bb7052015-07-27 14:36:07 -070030import com.android.launcher3.compat.PackageInstallerCompat;
Sunny Goyala9e2f5a2016-06-10 12:22:04 -070031import com.android.launcher3.config.FeatureFlags;
Sunny Goyal161a2142018-10-29 14:02:20 -070032import com.android.launcher3.provider.LauncherDbUtils;
33import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction;
Sunny Goyalff4ba2d2016-04-02 14:12:34 -070034import com.android.launcher3.util.GridOccupancy;
Sunny Goyalefb7e842018-10-04 15:11:00 -070035import com.android.launcher3.util.IntArray;
36import com.android.launcher3.util.IntSparseArrayMap;
37
Sunny Goyale5bb7052015-07-27 14:36:07 -070038import java.util.ArrayList;
39import java.util.Collections;
Sunny Goyale5bb7052015-07-27 14:36:07 -070040import java.util.HashSet;
Sunny Goyalc5939392018-12-07 11:43:47 -080041
42import androidx.annotation.VisibleForTesting;
Sunny Goyale5bb7052015-07-27 14:36:07 -070043
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<>();
695 while (c.moveToNext()) {
696 DbEntry entry = new DbEntry();
Sunny Goyalefb7e842018-10-04 15:11:00 -0700697 entry.id = c.getInt(indexId);
Sunny Goyalf076eae2016-01-11 12:25:10 -0800698 entry.itemType = c.getInt(indexItemType);
699 entry.cellX = c.getInt(indexCellX);
700 entry.cellY = c.getInt(indexCellY);
701 entry.spanX = c.getInt(indexSpanX);
702 entry.spanY = c.getInt(indexSpanY);
703 entry.screenId = screen;
Sunny Goyale5bb7052015-07-27 14:36:07 -0700704
Sunny Goyalf076eae2016-01-11 12:25:10 -0800705 try {
706 // calculate weight
707 switch (entry.itemType) {
708 case Favorites.ITEM_TYPE_SHORTCUT:
Tony Wickhambfbf7f92016-05-19 11:19:39 -0700709 case Favorites.ITEM_TYPE_DEEP_SHORTCUT:
Sunny Goyalf076eae2016-01-11 12:25:10 -0800710 case Favorites.ITEM_TYPE_APPLICATION: {
711 verifyIntent(c.getString(indexIntent));
Tony Wickhambfbf7f92016-05-19 11:19:39 -0700712 entry.weight = entry.itemType == Favorites.ITEM_TYPE_APPLICATION ?
713 WT_APPLICATION : WT_SHORTCUT;
Sunny Goyalf076eae2016-01-11 12:25:10 -0800714 break;
715 }
716 case Favorites.ITEM_TYPE_APPWIDGET: {
717 String provider = c.getString(indexAppWidgetProvider);
718 ComponentName cn = ComponentName.unflattenFromString(provider);
719 verifyPackage(cn.getPackageName());
720 entry.weight = Math.max(WT_WIDGET_MIN, WT_WIDGET_FACTOR
721 * entry.spanX * entry.spanY);
Sunny Goyale5bb7052015-07-27 14:36:07 -0700722
Sunny Goyal2e1efb42016-03-03 16:58:55 -0800723 int widgetId = c.getInt(indexAppWidgetId);
724 LauncherAppWidgetProviderInfo pInfo = AppWidgetManagerCompat.getInstance(
725 mContext).getLauncherAppWidgetInfo(widgetId);
Rajeev Kumar0ce439f2017-06-12 17:28:07 -0700726 Point spans = null;
727 if (pInfo != null) {
Sunny Goyal952e63d2017-08-16 04:59:08 -0700728 spans = pInfo.getMinSpans();
Rajeev Kumar0ce439f2017-06-12 17:28:07 -0700729 }
Sunny Goyalf076eae2016-01-11 12:25:10 -0800730 if (spans != null) {
731 entry.minSpanX = spans.x > 0 ? spans.x : entry.spanX;
732 entry.minSpanY = spans.y > 0 ? spans.y : entry.spanY;
733 } else {
734 // Assume that the widget be resized down to 2x2
735 entry.minSpanX = entry.minSpanY = 2;
736 }
Sunny Goyale5bb7052015-07-27 14:36:07 -0700737
Sunny Goyalf076eae2016-01-11 12:25:10 -0800738 if (entry.minSpanX > mTrgX || entry.minSpanY > mTrgY) {
739 throw new Exception("Widget can't be resized down to fit the grid");
740 }
741 break;
742 }
743 case Favorites.ITEM_TYPE_FOLDER: {
744 int total = getFolderItemsCount(entry.id);
745 if (total == 0) {
746 throw new Exception("Folder is empty");
747 }
748 entry.weight = WT_FOLDER_FACTOR * total;
749 break;
750 }
751 default:
752 throw new Exception("Invalid item type");
753 }
754 } catch (Exception e) {
755 if (DEBUG) {
756 Log.d(TAG, "Removing item " + entry.id, e);
757 }
758 mEntryToRemove.add(entry.id);
759 continue;
760 }
761 entries.add(entry);
762 }
763 c.close();
764 return entries;
Sunny Goyale5bb7052015-07-27 14:36:07 -0700765 }
766
767 /**
768 * @return the number of valid items in the folder.
769 */
Sunny Goyalefb7e842018-10-04 15:11:00 -0700770 private int getFolderItemsCount(int folderId) {
Sunny Goyala9e2f5a2016-06-10 12:22:04 -0700771 Cursor c = queryWorkspace(
Sunny Goyalf076eae2016-01-11 12:25:10 -0800772 new String[]{Favorites._ID, Favorites.INTENT},
Sunny Goyala9e2f5a2016-06-10 12:22:04 -0700773 Favorites.CONTAINER + " = " + folderId);
Sunny Goyale5bb7052015-07-27 14:36:07 -0700774
775 int total = 0;
776 while (c.moveToNext()) {
777 try {
778 verifyIntent(c.getString(1));
779 total++;
780 } catch (Exception e) {
Sunny Goyalefb7e842018-10-04 15:11:00 -0700781 mEntryToRemove.add(c.getInt(0));
Sunny Goyale5bb7052015-07-27 14:36:07 -0700782 }
783 }
Tony Wickhamfb062c62015-10-16 10:12:23 -0700784 c.close();
Sunny Goyale5bb7052015-07-27 14:36:07 -0700785 return total;
786 }
787
Sunny Goyala9e2f5a2016-06-10 12:22:04 -0700788 protected Cursor queryWorkspace(String[] columns, String where) {
Sunny Goyal161a2142018-10-29 14:02:20 -0700789 return mDb.query(Favorites.TABLE_NAME, columns, where, null, null, null, null);
Sunny Goyala9e2f5a2016-06-10 12:22:04 -0700790 }
791
Sunny Goyale5bb7052015-07-27 14:36:07 -0700792 /**
793 * Verifies if the intent should be restored.
794 */
795 private void verifyIntent(String intentStr) throws Exception {
796 Intent intent = Intent.parseUri(intentStr, 0);
797 if (intent.getComponent() != null) {
798 verifyPackage(intent.getComponent().getPackageName());
799 } else if (intent.getPackage() != null) {
800 // Only verify package if the component was null.
801 verifyPackage(intent.getPackage());
802 }
803 }
804
805 /**
806 * Verifies if the package should be restored
807 */
808 private void verifyPackage(String packageName) throws Exception {
809 if (!mValidPackages.contains(packageName)) {
810 throw new Exception("Package not available");
811 }
812 }
813
Sunny Goyala9e2f5a2016-06-10 12:22:04 -0700814 protected static class DbEntry extends ItemInfo implements Comparable<DbEntry> {
Sunny Goyale5bb7052015-07-27 14:36:07 -0700815
816 public float weight;
817
Ryan Lothianfa530cd2018-10-12 14:14:16 -0400818 public DbEntry() {
819 }
Sunny Goyale5bb7052015-07-27 14:36:07 -0700820
821 public DbEntry copy() {
822 DbEntry entry = new DbEntry();
823 entry.copyFrom(this);
824 entry.weight = weight;
825 entry.minSpanX = minSpanX;
826 entry.minSpanY = minSpanY;
827 return entry;
828 }
829
830 /**
831 * Comparator such that larger widgets come first, followed by all 1x1 items
832 * based on their weights.
833 */
834 @Override
835 public int compareTo(DbEntry another) {
836 if (itemType == Favorites.ITEM_TYPE_APPWIDGET) {
837 if (another.itemType == Favorites.ITEM_TYPE_APPWIDGET) {
838 return another.spanY * another.spanX - spanX * spanY;
839 } else {
840 return -1;
841 }
842 } else if (another.itemType == Favorites.ITEM_TYPE_APPWIDGET) {
843 return 1;
844 } else {
845 // Place higher weight before lower weight.
846 return Float.compare(another.weight, weight);
847 }
848 }
849
850 public boolean columnsSame(DbEntry org) {
851 return org.cellX == cellX && org.cellY == cellY && org.spanX == spanX &&
852 org.spanY == spanY && org.screenId == screenId;
853 }
854
855 public void addToContentValues(ContentValues values) {
Sunny Goyal161a2142018-10-29 14:02:20 -0700856 values.put(Favorites.SCREEN, screenId);
857 values.put(Favorites.CELLX, cellX);
858 values.put(Favorites.CELLY, cellY);
859 values.put(Favorites.SPANX, spanX);
860 values.put(Favorites.SPANY, spanY);
Sunny Goyale5bb7052015-07-27 14:36:07 -0700861 }
862 }
863
Sunny Goyalf862a262015-12-14 14:27:38 -0800864 private static ArrayList<DbEntry> deepCopy(ArrayList<DbEntry> src) {
Rajeev Kumar0ce439f2017-06-12 17:28:07 -0700865 ArrayList<DbEntry> dup = new ArrayList<>(src.size());
Sunny Goyale5bb7052015-07-27 14:36:07 -0700866 for (DbEntry e : src) {
867 dup.add(e.copy());
868 }
869 return dup;
870 }
871
Sunny Goyala5c8a9e2016-07-08 08:32:44 -0700872 public static void markForMigration(
873 Context context, int gridX, int gridY, int hotseatSize) {
874 Utilities.getPrefs(context).edit()
875 .putString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, getPointString(gridX, gridY))
876 .putInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, hotseatSize)
877 .apply();
878 }
879
Sunny Goyalf076eae2016-01-11 12:25:10 -0800880 /**
881 * Migrates the workspace and hotseat in case their sizes changed.
Ryan Lothianfa530cd2018-10-12 14:14:16 -0400882 *
Sunny Goyalf076eae2016-01-11 12:25:10 -0800883 * @return false if the migration failed.
884 */
885 public static boolean migrateGridIfNeeded(Context context) {
886 SharedPreferences prefs = Utilities.getPrefs(context);
Sunny Goyal87f784c2017-01-11 10:48:34 -0800887 InvariantDeviceProfile idp = LauncherAppState.getIDP(context);
Sunny Goyalf076eae2016-01-11 12:25:10 -0800888
889 String gridSizeString = getPointString(idp.numColumns, idp.numRows);
Sunny Goyalf076eae2016-01-11 12:25:10 -0800890
891 if (gridSizeString.equals(prefs.getString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, "")) &&
Ryan Lothianfa530cd2018-10-12 14:14:16 -0400892 idp.numHotseatIcons == prefs.getInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT,
893 idp.numHotseatIcons)) {
Sunny Goyalf076eae2016-01-11 12:25:10 -0800894 // Skip if workspace and hotseat sizes have not changed.
895 return true;
896 }
897
898 long migrationStartTime = System.currentTimeMillis();
Sunny Goyal161a2142018-10-29 14:02:20 -0700899 try (SQLiteTransaction transaction = (SQLiteTransaction) Settings.call(
900 context.getContentResolver(), Settings.METHOD_NEW_TRANSACTION)
901 .getBinder(Settings.EXTRA_VALUE)) {
902
903 int srcHotseatCount = prefs.getInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT,
904 idp.numHotseatIcons);
905 Point sourceSize = parsePoint(prefs.getString(
906 KEY_MIGRATION_SRC_WORKSPACE_SIZE, gridSizeString));
907
Sunny Goyalf076eae2016-01-11 12:25:10 -0800908 boolean dbChanged = false;
909
Sunny Goyal161a2142018-10-29 14:02:20 -0700910 GridBackupTable backupTable = new GridBackupTable(context, transaction.getDb(),
911 srcHotseatCount, sourceSize.x, sourceSize.y);
912 if (backupTable.backupOrRestoreAsNeeded()) {
913 dbChanged = true;
914 srcHotseatCount = backupTable.getRestoreHotseatAndGridSize(sourceSize);
915 }
916
Rajeev Kumar0ce439f2017-06-12 17:28:07 -0700917 HashSet<String> validPackages = getValidPackages(context);
Sunny Goyalf076eae2016-01-11 12:25:10 -0800918 // Hotseat
Sunny Goyalbb011da2016-06-15 15:42:29 -0700919 if (srcHotseatCount != idp.numHotseatIcons) {
Sunny Goyalf076eae2016-01-11 12:25:10 -0800920 // Migrate hotseat.
Sunny Goyal161a2142018-10-29 14:02:20 -0700921 dbChanged = new GridSizeMigrationTask(context, transaction.getDb(),
Sunny Goyalbb011da2016-06-15 15:42:29 -0700922 validPackages, srcHotseatCount, idp.numHotseatIcons).migrateHotseat();
Sunny Goyalf076eae2016-01-11 12:25:10 -0800923 }
924
925 // Grid size
926 Point targetSize = new Point(idp.numColumns, idp.numRows);
Sunny Goyal161a2142018-10-29 14:02:20 -0700927 if (new MultiStepMigrationTask(validPackages, context, transaction.getDb())
928 .migrate(sourceSize, targetSize)) {
Sunny Goyala5c8a9e2016-07-08 08:32:44 -0700929 dbChanged = true;
Sunny Goyalf076eae2016-01-11 12:25:10 -0800930 }
931
932 if (dbChanged) {
933 // Make sure we haven't removed everything.
934 final Cursor c = context.getContentResolver().query(
Sunny Goyal161a2142018-10-29 14:02:20 -0700935 Favorites.CONTENT_URI, null, null, null, null);
Sunny Goyalf076eae2016-01-11 12:25:10 -0800936 boolean hasData = c.moveToNext();
937 c.close();
938 if (!hasData) {
939 throw new Exception("Removed every thing during grid resize");
940 }
941 }
942
Sunny Goyal161a2142018-10-29 14:02:20 -0700943 transaction.commit();
944 Settings.call(context.getContentResolver(), Settings.METHOD_REFRESH_BACKUP_TABLE);
Sunny Goyalf076eae2016-01-11 12:25:10 -0800945 return true;
946 } catch (Exception e) {
947 Log.e(TAG, "Error during grid migration", e);
948
949 return false;
950 } finally {
951 Log.v(TAG, "Workspace migration completed in "
952 + (System.currentTimeMillis() - migrationStartTime));
953
954 // Save current configuration, so that the migration does not run again.
955 prefs.edit()
956 .putString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, gridSizeString)
Sunny Goyalbb011da2016-06-15 15:42:29 -0700957 .putInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, idp.numHotseatIcons)
Sunny Goyalf076eae2016-01-11 12:25:10 -0800958 .apply();
959 }
960 }
Sunny Goyala9e2f5a2016-06-10 12:22:04 -0700961
962 protected static HashSet<String> getValidPackages(Context context) {
963 // Initialize list of valid packages. This contain all the packages which are already on
964 // the device and packages which are being installed. Any item which doesn't belong to
965 // this set is removed.
966 // Since the loader removes such items anyway, removing these items here doesn't cause
967 // any extra data loss and gives us more free space on the grid for better migration.
Rajeev Kumar0ce439f2017-06-12 17:28:07 -0700968 HashSet<String> validPackages = new HashSet<>();
Sunny Goyala9e2f5a2016-06-10 12:22:04 -0700969 for (PackageInfo info : context.getPackageManager()
970 .getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES)) {
971 validPackages.add(info.packageName);
972 }
973 validPackages.addAll(PackageInstallerCompat.getInstance(context)
974 .updateAndGetActiveSessionCache().keySet());
975 return validPackages;
976 }
Sunny Goyala5c8a9e2016-07-08 08:32:44 -0700977
978 /**
Sunny Goyald70ef242016-08-24 11:30:33 -0700979 * Removes any broken item from the hotseat.
Ryan Lothianfa530cd2018-10-12 14:14:16 -0400980 *
Sunny Goyald70ef242016-08-24 11:30:33 -0700981 * @return a map with occupied hotseat position set to non-null value.
982 */
Ryan Lothianfa530cd2018-10-12 14:14:16 -0400983 public static IntSparseArrayMap<Object> removeBrokenHotseatItems(Context context)
984 throws Exception {
Sunny Goyal161a2142018-10-29 14:02:20 -0700985 try (SQLiteTransaction transaction = (SQLiteTransaction) Settings.call(
986 context.getContentResolver(), Settings.METHOD_NEW_TRANSACTION)
987 .getBinder(Settings.EXTRA_VALUE)) {
988 GridSizeMigrationTask task = new GridSizeMigrationTask(
989 context, transaction.getDb(), getValidPackages(context),
990 Integer.MAX_VALUE, Integer.MAX_VALUE);
Sunny Goyald70ef242016-08-24 11:30:33 -0700991
Sunny Goyal161a2142018-10-29 14:02:20 -0700992 // Load all the valid entries
993 ArrayList<DbEntry> items = task.loadHotseatEntries();
994 // Delete any entry marked for deletion by above load.
995 task.applyOperations();
996 IntSparseArrayMap<Object> positions = new IntSparseArrayMap<>();
997 for (DbEntry item : items) {
998 positions.put(item.screenId, item);
999 }
1000 transaction.commit();
1001 return positions;
Sunny Goyald70ef242016-08-24 11:30:33 -07001002 }
Sunny Goyald70ef242016-08-24 11:30:33 -07001003 }
1004
1005 /**
Sunny Goyala5c8a9e2016-07-08 08:32:44 -07001006 * Task to run grid migration in multiple steps when the size difference is more than 1.
1007 */
1008 protected static class MultiStepMigrationTask {
1009 private final HashSet<String> mValidPackages;
1010 private final Context mContext;
Sunny Goyal161a2142018-10-29 14:02:20 -07001011 private final SQLiteDatabase mDb;
Sunny Goyala5c8a9e2016-07-08 08:32:44 -07001012
Sunny Goyal161a2142018-10-29 14:02:20 -07001013 public MultiStepMigrationTask(HashSet<String> validPackages, Context context,
1014 SQLiteDatabase db) {
Sunny Goyala5c8a9e2016-07-08 08:32:44 -07001015 mValidPackages = validPackages;
1016 mContext = context;
Sunny Goyal161a2142018-10-29 14:02:20 -07001017 mDb = db;
Sunny Goyala5c8a9e2016-07-08 08:32:44 -07001018 }
1019
1020 public boolean migrate(Point sourceSize, Point targetSize) throws Exception {
1021 boolean dbChanged = false;
1022 if (!targetSize.equals(sourceSize)) {
1023 if (sourceSize.x < targetSize.x) {
1024 // Source is smaller that target, just expand the grid without actual migration.
1025 sourceSize.x = targetSize.x;
1026 }
1027 if (sourceSize.y < targetSize.y) {
1028 // Source is smaller that target, just expand the grid without actual migration.
1029 sourceSize.y = targetSize.y;
1030 }
1031
1032 // Migrate the workspace grid, such that the points differ by max 1 in x and y
1033 // each on every step.
1034 while (!targetSize.equals(sourceSize)) {
1035 // Get the next size, such that the points differ by max 1 in x and y each
1036 Point nextSize = new Point(sourceSize);
1037 if (targetSize.x < nextSize.x) {
1038 nextSize.x--;
1039 }
1040 if (targetSize.y < nextSize.y) {
1041 nextSize.y--;
1042 }
1043 if (runStepTask(sourceSize, nextSize)) {
1044 dbChanged = true;
1045 }
1046 sourceSize.set(nextSize.x, nextSize.y);
1047 }
1048 }
1049 return dbChanged;
1050 }
1051
1052 protected boolean runStepTask(Point sourceSize, Point nextSize) throws Exception {
Sunny Goyal161a2142018-10-29 14:02:20 -07001053 return new GridSizeMigrationTask(mContext, mDb,
Sunny Goyala5c8a9e2016-07-08 08:32:44 -07001054 mValidPackages, sourceSize, nextSize).migrateWorkspace();
1055 }
1056 }
Sunny Goyale5bb7052015-07-27 14:36:07 -07001057}