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