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