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