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