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