1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.launcher3.model; 18 19 import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME; 20 import static com.android.launcher3.provider.LauncherDbUtils.itemIdMatch; 21 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; 22 23 import android.content.ContentValues; 24 import android.content.Context; 25 import android.text.TextUtils; 26 import android.util.Log; 27 28 import androidx.annotation.NonNull; 29 import androidx.annotation.Nullable; 30 31 import com.android.launcher3.LauncherAppState; 32 import com.android.launcher3.LauncherModel; 33 import com.android.launcher3.LauncherModel.CallbackTask; 34 import com.android.launcher3.LauncherSettings.Favorites; 35 import com.android.launcher3.Utilities; 36 import com.android.launcher3.celllayout.CellPosMapper; 37 import com.android.launcher3.celllayout.CellPosMapper.CellPos; 38 import com.android.launcher3.config.FeatureFlags; 39 import com.android.launcher3.logging.FileLog; 40 import com.android.launcher3.model.BgDataModel.Callbacks; 41 import com.android.launcher3.model.data.FolderInfo; 42 import com.android.launcher3.model.data.ItemInfo; 43 import com.android.launcher3.model.data.LauncherAppWidgetInfo; 44 import com.android.launcher3.model.data.WorkspaceItemInfo; 45 import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction; 46 import com.android.launcher3.util.ContentWriter; 47 import com.android.launcher3.util.Executors; 48 import com.android.launcher3.util.ItemInfoMatcher; 49 import com.android.launcher3.util.LooperExecutor; 50 import com.android.launcher3.widget.LauncherWidgetHolder; 51 52 import java.util.ArrayList; 53 import java.util.Arrays; 54 import java.util.Collection; 55 import java.util.Collections; 56 import java.util.List; 57 import java.util.function.Predicate; 58 import java.util.function.Supplier; 59 import java.util.stream.Collectors; 60 import java.util.stream.StreamSupport; 61 62 /** 63 * Class for handling model updates. 64 */ 65 public class ModelWriter { 66 67 private static final String TAG = "ModelWriter"; 68 69 private final Context mContext; 70 private final LauncherModel mModel; 71 private final BgDataModel mBgDataModel; 72 private final LooperExecutor mUiExecutor; 73 74 @Nullable 75 private final Callbacks mOwner; 76 77 private final boolean mHasVerticalHotseat; 78 private final boolean mVerifyChanges; 79 80 // Keep track of delete operations that occur when an Undo option is present; we may not commit. 81 private final List<ModelTask> mDeleteRunnables = new ArrayList<>(); 82 private boolean mPreparingToUndo; 83 private final CellPosMapper mCellPosMapper; 84 ModelWriter(Context context, LauncherModel model, BgDataModel dataModel, boolean hasVerticalHotseat, boolean verifyChanges, CellPosMapper cellPosMapper, @Nullable Callbacks owner)85 public ModelWriter(Context context, LauncherModel model, BgDataModel dataModel, 86 boolean hasVerticalHotseat, boolean verifyChanges, CellPosMapper cellPosMapper, 87 @Nullable Callbacks owner) { 88 mContext = context; 89 mModel = model; 90 mBgDataModel = dataModel; 91 mHasVerticalHotseat = hasVerticalHotseat; 92 mVerifyChanges = verifyChanges; 93 mOwner = owner; 94 mCellPosMapper = cellPosMapper; 95 mUiExecutor = Executors.MAIN_EXECUTOR; 96 } 97 updateItemInfoProps( ItemInfo item, int container, int screenId, int cellX, int cellY)98 private void updateItemInfoProps( 99 ItemInfo item, int container, int screenId, int cellX, int cellY) { 100 CellPos modelPos = mCellPosMapper.mapPresenterToModel(cellX, cellY, screenId, container); 101 102 item.container = container; 103 item.cellX = modelPos.cellX; 104 item.cellY = modelPos.cellY; 105 // We store hotseat items in canonical form which is this orientation invariant position 106 // in the hotseat 107 if (container == Favorites.CONTAINER_HOTSEAT) { 108 item.screenId = mHasVerticalHotseat 109 ? LauncherAppState.getIDP(mContext).numDatabaseHotseatIcons - cellY - 1 : cellX; 110 } else { 111 item.screenId = modelPos.screenId; 112 } 113 } 114 115 /** 116 * Adds an item to the DB if it was not created previously, or move it to a new 117 * <container, screen, cellX, cellY> 118 */ addOrMoveItemInDatabase(ItemInfo item, int container, int screenId, int cellX, int cellY)119 public void addOrMoveItemInDatabase(ItemInfo item, 120 int container, int screenId, int cellX, int cellY) { 121 if (item.id == ItemInfo.NO_ID) { 122 // From all apps 123 addItemToDatabase(item, container, screenId, cellX, cellY); 124 } else { 125 // From somewhere else 126 moveItemInDatabase(item, container, screenId, cellX, cellY); 127 } 128 } 129 checkItemInfoLocked(int itemId, ItemInfo item, StackTraceElement[] stackTrace)130 private void checkItemInfoLocked(int itemId, ItemInfo item, StackTraceElement[] stackTrace) { 131 ItemInfo modelItem = mBgDataModel.itemsIdMap.get(itemId); 132 if (modelItem != null && item != modelItem) { 133 // check all the data is consistent 134 if (!Utilities.IS_DEBUG_DEVICE && !FeatureFlags.IS_STUDIO_BUILD 135 && modelItem instanceof WorkspaceItemInfo 136 && item instanceof WorkspaceItemInfo) { 137 if (modelItem.title.toString().equals(item.title.toString()) && 138 modelItem.getIntent().filterEquals(item.getIntent()) && 139 modelItem.id == item.id && 140 modelItem.itemType == item.itemType && 141 modelItem.container == item.container && 142 modelItem.screenId == item.screenId && 143 modelItem.cellX == item.cellX && 144 modelItem.cellY == item.cellY && 145 modelItem.spanX == item.spanX && 146 modelItem.spanY == item.spanY) { 147 // For all intents and purposes, this is the same object 148 return; 149 } 150 } 151 152 // the modelItem needs to match up perfectly with item if our model is 153 // to be consistent with the database-- for now, just require 154 // modelItem == item or the equality check above 155 String msg = "item: " + ((item != null) ? item.toString() : "null") + 156 "modelItem: " + 157 ((modelItem != null) ? modelItem.toString() : "null") + 158 "Error: ItemInfo passed to checkItemInfo doesn't match original"; 159 RuntimeException e = new RuntimeException(msg); 160 if (stackTrace != null) { 161 e.setStackTrace(stackTrace); 162 } 163 throw e; 164 } 165 } 166 167 /** 168 * Move an item in the DB to a new <container, screen, cellX, cellY> 169 */ moveItemInDatabase(final ItemInfo item, int container, int screenId, int cellX, int cellY)170 public void moveItemInDatabase(final ItemInfo item, 171 int container, int screenId, int cellX, int cellY) { 172 updateItemInfoProps(item, container, screenId, cellX, cellY); 173 notifyItemModified(item); 174 175 enqueueDeleteRunnable(new UpdateItemRunnable(item, () -> 176 new ContentWriter(mContext) 177 .put(Favorites.CONTAINER, item.container) 178 .put(Favorites.CELLX, item.cellX) 179 .put(Favorites.CELLY, item.cellY) 180 .put(Favorites.RANK, item.rank) 181 .put(Favorites.SCREEN, item.screenId))); 182 } 183 184 /** 185 * Move items in the DB to a new <container, screen, cellX, cellY>. We assume that the 186 * cellX, cellY have already been updated on the ItemInfos. 187 */ moveItemsInDatabase(final ArrayList<ItemInfo> items, int container, int screen)188 public void moveItemsInDatabase(final ArrayList<ItemInfo> items, int container, int screen) { 189 ArrayList<ContentValues> contentValues = new ArrayList<>(); 190 int count = items.size(); 191 notifyOtherCallbacks(c -> c.bindItemsModified(items)); 192 193 for (int i = 0; i < count; i++) { 194 ItemInfo item = items.get(i); 195 updateItemInfoProps(item, container, screen, item.cellX, item.cellY); 196 197 final ContentValues values = new ContentValues(); 198 values.put(Favorites.CONTAINER, item.container); 199 values.put(Favorites.CELLX, item.cellX); 200 values.put(Favorites.CELLY, item.cellY); 201 values.put(Favorites.RANK, item.rank); 202 values.put(Favorites.SCREEN, item.screenId); 203 204 contentValues.add(values); 205 } 206 enqueueDeleteRunnable(new UpdateItemsRunnable(items, contentValues)); 207 } 208 209 /** 210 * Move and/or resize item in the DB to a new <container, screen, cellX, cellY, spanX, spanY> 211 */ modifyItemInDatabase(final ItemInfo item, int container, int screenId, int cellX, int cellY, int spanX, int spanY)212 public void modifyItemInDatabase(final ItemInfo item, 213 int container, int screenId, int cellX, int cellY, int spanX, int spanY) { 214 updateItemInfoProps(item, container, screenId, cellX, cellY); 215 item.spanX = spanX; 216 item.spanY = spanY; 217 notifyItemModified(item); 218 new UpdateItemRunnable(item, () -> 219 new ContentWriter(mContext) 220 .put(Favorites.CONTAINER, item.container) 221 .put(Favorites.CELLX, item.cellX) 222 .put(Favorites.CELLY, item.cellY) 223 .put(Favorites.RANK, item.rank) 224 .put(Favorites.SPANX, item.spanX) 225 .put(Favorites.SPANY, item.spanY) 226 .put(Favorites.SCREEN, item.screenId)) 227 .executeOnModelThread(); 228 } 229 230 /** 231 * Update an item to the database in a specified container. 232 */ updateItemInDatabase(ItemInfo item)233 public void updateItemInDatabase(ItemInfo item) { 234 notifyItemModified(item); 235 new UpdateItemRunnable(item, () -> { 236 ContentWriter writer = new ContentWriter(mContext); 237 item.onAddToDatabase(writer); 238 return writer; 239 }).executeOnModelThread(); 240 } 241 notifyItemModified(ItemInfo item)242 private void notifyItemModified(ItemInfo item) { 243 notifyOtherCallbacks(c -> c.bindItemsModified(Collections.singletonList(item))); 244 } 245 246 /** 247 * Add an item to the database in a specified container. Sets the container, screen, cellX and 248 * cellY fields of the item. Also assigns an ID to the item. 249 */ addItemToDatabase(final ItemInfo item, int container, int screenId, int cellX, int cellY)250 public void addItemToDatabase(final ItemInfo item, 251 int container, int screenId, int cellX, int cellY) { 252 updateItemInfoProps(item, container, screenId, cellX, cellY); 253 254 item.id = mModel.getModelDbController().generateNewItemId(); 255 notifyOtherCallbacks(c -> c.bindItems(Collections.singletonList(item), false)); 256 257 ModelVerifier verifier = new ModelVerifier(); 258 final StackTraceElement[] stackTrace = new Throwable().getStackTrace(); 259 newModelTask(() -> { 260 // Write the item on background thread, as some properties might have been updated in 261 // the background. 262 final ContentWriter writer = new ContentWriter(mContext); 263 item.onAddToDatabase(writer); 264 writer.put(Favorites._ID, item.id); 265 266 mModel.getModelDbController().insert(Favorites.TABLE_NAME, writer.getValues(mContext)); 267 synchronized (mBgDataModel) { 268 checkItemInfoLocked(item.id, item, stackTrace); 269 mBgDataModel.addItem(mContext, item, true); 270 verifier.verifyModel(); 271 } 272 }).executeOnModelThread(); 273 } 274 275 /** 276 * Removes the specified item from the database 277 */ deleteItemFromDatabase(ItemInfo item, @Nullable final String reason)278 public void deleteItemFromDatabase(ItemInfo item, @Nullable final String reason) { 279 deleteItemsFromDatabase(Arrays.asList(item), reason); 280 } 281 282 /** 283 * Removes all the items from the database matching {@param matcher}. 284 */ deleteItemsFromDatabase(@onNull final Predicate<ItemInfo> matcher, @Nullable final String reason)285 public void deleteItemsFromDatabase(@NonNull final Predicate<ItemInfo> matcher, 286 @Nullable final String reason) { 287 deleteItemsFromDatabase(StreamSupport.stream(mBgDataModel.itemsIdMap.spliterator(), false) 288 .filter(matcher).collect(Collectors.toList()), reason); 289 } 290 291 /** 292 * Removes the specified items from the database 293 */ deleteItemsFromDatabase(final Collection<? extends ItemInfo> items, @Nullable final String reason)294 public void deleteItemsFromDatabase(final Collection<? extends ItemInfo> items, 295 @Nullable final String reason) { 296 ModelVerifier verifier = new ModelVerifier(); 297 FileLog.d(TAG, "removing items from db " + items.stream().map( 298 (item) -> item.getTargetComponent() == null ? "" 299 : item.getTargetComponent().getPackageName()).collect( 300 Collectors.joining(",")) 301 + ". Reason: [" + (TextUtils.isEmpty(reason) ? "unknown" : reason) + "]"); 302 notifyDelete(items); 303 enqueueDeleteRunnable(newModelTask(() -> { 304 for (ItemInfo item : items) { 305 mModel.getModelDbController().delete(TABLE_NAME, itemIdMatch(item.id), null); 306 mBgDataModel.removeItem(mContext, item); 307 verifier.verifyModel(); 308 } 309 })); 310 } 311 312 /** 313 * Remove the specified folder and all its contents from the database. 314 */ deleteFolderAndContentsFromDatabase(final FolderInfo info)315 public void deleteFolderAndContentsFromDatabase(final FolderInfo info) { 316 ModelVerifier verifier = new ModelVerifier(); 317 notifyDelete(Collections.singleton(info)); 318 319 enqueueDeleteRunnable(newModelTask(() -> { 320 mModel.getModelDbController().delete(Favorites.TABLE_NAME, 321 Favorites.CONTAINER + "=" + info.id, null); 322 mBgDataModel.removeItem(mContext, info.contents); 323 info.contents.clear(); 324 325 mModel.getModelDbController().delete(Favorites.TABLE_NAME, 326 Favorites._ID + "=" + info.id, null); 327 mBgDataModel.removeItem(mContext, info); 328 verifier.verifyModel(); 329 })); 330 } 331 332 /** 333 * Deletes the widget info and the widget id. 334 */ deleteWidgetInfo(final LauncherAppWidgetInfo info, LauncherWidgetHolder holder, @Nullable final String reason)335 public void deleteWidgetInfo(final LauncherAppWidgetInfo info, LauncherWidgetHolder holder, 336 @Nullable final String reason) { 337 notifyDelete(Collections.singleton(info)); 338 if (holder != null && !info.isCustomWidget() && info.isWidgetIdAllocated()) { 339 // Deleting an app widget ID is a void call but writes to disk before returning 340 // to the caller... 341 enqueueDeleteRunnable(newModelTask(() -> holder.deleteAppWidgetId(info.appWidgetId))); 342 } 343 deleteItemFromDatabase(info, reason); 344 } 345 notifyDelete(Collection<? extends ItemInfo> items)346 private void notifyDelete(Collection<? extends ItemInfo> items) { 347 notifyOtherCallbacks(c -> c.bindWorkspaceComponentsRemoved(ItemInfoMatcher.ofItems(items))); 348 } 349 350 /** 351 * Delete operations tracked using {@link #enqueueDeleteRunnable} will only be called 352 * if {@link #commitDelete} is called. Note that one of {@link #commitDelete()} or 353 * {@link #abortDelete} MUST be called after this method, or else all delete 354 * operations will remain uncommitted indefinitely. 355 */ prepareToUndoDelete()356 public void prepareToUndoDelete() { 357 if (!mPreparingToUndo) { 358 if (!mDeleteRunnables.isEmpty() && FeatureFlags.IS_STUDIO_BUILD) { 359 throw new IllegalStateException("There are still uncommitted delete operations!"); 360 } 361 mDeleteRunnables.clear(); 362 mPreparingToUndo = true; 363 } 364 } 365 366 /** 367 * If {@link #prepareToUndoDelete} has been called, we store the Runnable to be run when 368 * {@link #commitDelete()} is called (or abandoned if {@link #abortDelete} is called). 369 * Otherwise, we run the Runnable immediately. 370 */ enqueueDeleteRunnable(ModelTask r)371 private void enqueueDeleteRunnable(ModelTask r) { 372 if (mPreparingToUndo) { 373 mDeleteRunnables.add(r); 374 } else { 375 r.executeOnModelThread(); 376 } 377 } 378 commitDelete()379 public void commitDelete() { 380 mPreparingToUndo = false; 381 mDeleteRunnables.forEach(ModelTask::executeOnModelThread); 382 mDeleteRunnables.clear(); 383 } 384 385 /** 386 * Aborts a previous delete operation pending commit 387 */ abortDelete()388 public void abortDelete() { 389 mPreparingToUndo = false; 390 mDeleteRunnables.clear(); 391 // We do a full reload here instead of just a rebind because Folders change their internal 392 // state when dragging an item out, which clobbers the rebind unless we load from the DB. 393 mModel.forceReload(); 394 } 395 notifyOtherCallbacks(CallbackTask task)396 private void notifyOtherCallbacks(CallbackTask task) { 397 if (mOwner == null) { 398 // If the call is happening from a model, it will take care of updating the callbacks 399 return; 400 } 401 mUiExecutor.execute(() -> { 402 for (Callbacks c : mModel.getCallbacks()) { 403 if (c != mOwner) { 404 task.execute(c); 405 } 406 } 407 }); 408 } 409 410 private class UpdateItemRunnable extends UpdateItemBaseRunnable { 411 private final ItemInfo mItem; 412 private final Supplier<ContentWriter> mWriter; 413 private final int mItemId; 414 UpdateItemRunnable(ItemInfo item, Supplier<ContentWriter> writer)415 UpdateItemRunnable(ItemInfo item, Supplier<ContentWriter> writer) { 416 mItem = item; 417 mWriter = writer; 418 mItemId = item.id; 419 } 420 421 @Override runImpl()422 public void runImpl() { 423 mModel.getModelDbController().update( 424 TABLE_NAME, mWriter.get().getValues(mContext), itemIdMatch(mItemId), null); 425 updateItemArrays(mItem, mItemId); 426 } 427 } 428 429 private class UpdateItemsRunnable extends UpdateItemBaseRunnable { 430 private final ArrayList<ContentValues> mValues; 431 private final ArrayList<ItemInfo> mItems; 432 UpdateItemsRunnable(ArrayList<ItemInfo> items, ArrayList<ContentValues> values)433 UpdateItemsRunnable(ArrayList<ItemInfo> items, ArrayList<ContentValues> values) { 434 mValues = values; 435 mItems = items; 436 } 437 438 @Override runImpl()439 public void runImpl() { 440 try (SQLiteTransaction t = mModel.getModelDbController().newTransaction()) { 441 int count = mItems.size(); 442 for (int i = 0; i < count; i++) { 443 ItemInfo item = mItems.get(i); 444 final int itemId = item.id; 445 mModel.getModelDbController().update( 446 TABLE_NAME, mValues.get(i), itemIdMatch(itemId), null); 447 updateItemArrays(item, itemId); 448 } 449 t.commit(); 450 } catch (Exception e) { 451 e.printStackTrace(); 452 } 453 } 454 } 455 456 private abstract class UpdateItemBaseRunnable extends ModelTask { 457 private final StackTraceElement[] mStackTrace; 458 private final ModelVerifier mVerifier = new ModelVerifier(); 459 UpdateItemBaseRunnable()460 UpdateItemBaseRunnable() { 461 mStackTrace = new Throwable().getStackTrace(); 462 } 463 updateItemArrays(ItemInfo item, int itemId)464 protected void updateItemArrays(ItemInfo item, int itemId) { 465 // Lock on mBgLock *after* the db operation 466 synchronized (mBgDataModel) { 467 checkItemInfoLocked(itemId, item, mStackTrace); 468 469 if (item.container != Favorites.CONTAINER_DESKTOP && 470 item.container != Favorites.CONTAINER_HOTSEAT) { 471 // Item is in a folder, make sure this folder exists 472 if (!mBgDataModel.folders.containsKey(item.container)) { 473 // An items container is being set to a that of an item which is not in 474 // the list of Folders. 475 String msg = "item: " + item + " container being set to: " + 476 item.container + ", not in the list of folders"; 477 Log.e(TAG, msg); 478 } 479 } 480 481 // Items are added/removed from the corresponding FolderInfo elsewhere, such 482 // as in Workspace.onDrop. Here, we just add/remove them from the list of items 483 // that are on the desktop, as appropriate 484 ItemInfo modelItem = mBgDataModel.itemsIdMap.get(itemId); 485 if (modelItem != null && 486 (modelItem.container == Favorites.CONTAINER_DESKTOP || 487 modelItem.container == Favorites.CONTAINER_HOTSEAT)) { 488 switch (modelItem.itemType) { 489 case Favorites.ITEM_TYPE_APPLICATION: 490 case Favorites.ITEM_TYPE_DEEP_SHORTCUT: 491 case Favorites.ITEM_TYPE_FOLDER: 492 case Favorites.ITEM_TYPE_APP_PAIR: 493 if (!mBgDataModel.workspaceItems.contains(modelItem)) { 494 mBgDataModel.workspaceItems.add(modelItem); 495 } 496 break; 497 default: 498 break; 499 } 500 } else { 501 mBgDataModel.workspaceItems.remove(modelItem); 502 } 503 mVerifier.verifyModel(); 504 } 505 } 506 } 507 508 private abstract class ModelTask implements Runnable { 509 510 private final int mLoadId = mBgDataModel.lastLoadId; 511 512 @Override run()513 public final void run() { 514 if (mLoadId != mModel.getLastLoadId()) { 515 Log.d(TAG, "Model changed before the task could execute"); 516 return; 517 } 518 runImpl(); 519 } 520 executeOnModelThread()521 public final void executeOnModelThread() { 522 MODEL_EXECUTOR.execute(this); 523 } 524 runImpl()525 public abstract void runImpl(); 526 } 527 newModelTask(Runnable r)528 private ModelTask newModelTask(Runnable r) { 529 return new ModelTask() { 530 @Override 531 public void runImpl() { 532 r.run(); 533 } 534 }; 535 } 536 537 /** 538 * Utility class to verify model updates are propagated properly to the callback. 539 */ 540 public class ModelVerifier { 541 542 final int startId; 543 544 ModelVerifier() { 545 startId = mBgDataModel.lastBindId; 546 } 547 548 void verifyModel() { 549 if (!mVerifyChanges || !mModel.hasCallbacks()) { 550 return; 551 } 552 553 int executeId = mBgDataModel.lastBindId; 554 555 mUiExecutor.post(() -> { 556 int currentId = mBgDataModel.lastBindId; 557 if (currentId > executeId) { 558 // Model was already bound after job was executed. 559 return; 560 } 561 if (executeId == startId) { 562 // Bound model has not changed during the job 563 return; 564 } 565 566 // Bound model was changed between submitting the job and executing the job 567 mModel.rebindCallbacks(); 568 }); 569 } 570 } 571 } 572