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