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.FolderInfo; 31 import com.android.launcher3.ItemInfo; 32 import com.android.launcher3.LauncherAppState; 33 import com.android.launcher3.LauncherAppWidgetHost; 34 import com.android.launcher3.LauncherAppWidgetInfo; 35 import com.android.launcher3.LauncherModel; 36 import com.android.launcher3.LauncherProvider; 37 import com.android.launcher3.LauncherSettings; 38 import com.android.launcher3.LauncherSettings.Favorites; 39 import com.android.launcher3.LauncherSettings.Settings; 40 import com.android.launcher3.Utilities; 41 import com.android.launcher3.WorkspaceItemInfo; 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.util.ContentWriter; 46 import com.android.launcher3.util.ItemInfoMatcher; 47 48 import java.util.ArrayList; 49 import java.util.Arrays; 50 import java.util.Collection; 51 import java.util.List; 52 import java.util.concurrent.Executor; 53 import java.util.function.Supplier; 54 import java.util.stream.Collectors; 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).numHotseatIcons - 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_DOGFOOD_BUILD && 120 modelItem instanceof WorkspaceItemInfo && 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_DOGFOOD_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 abortDelete(int pageToBindFirst)353 public void abortDelete(int pageToBindFirst) { 354 mPreparingToUndo = false; 355 mDeleteRunnables.clear(); 356 // We do a full reload here instead of just a rebind because Folders change their internal 357 // state when dragging an item out, which clobbers the rebind unless we load from the DB. 358 mModel.forceReload(pageToBindFirst); 359 } 360 361 private class UpdateItemRunnable extends UpdateItemBaseRunnable { 362 private final ItemInfo mItem; 363 private final Supplier<ContentWriter> mWriter; 364 private final int mItemId; 365 UpdateItemRunnable(ItemInfo item, Supplier<ContentWriter> writer)366 UpdateItemRunnable(ItemInfo item, Supplier<ContentWriter> writer) { 367 mItem = item; 368 mWriter = writer; 369 mItemId = item.id; 370 } 371 372 @Override run()373 public void run() { 374 Uri uri = Favorites.getContentUri(mItemId); 375 mContext.getContentResolver().update(uri, mWriter.get().getValues(mContext), 376 null, null); 377 updateItemArrays(mItem, mItemId); 378 } 379 } 380 381 private class UpdateItemsRunnable extends UpdateItemBaseRunnable { 382 private final ArrayList<ContentValues> mValues; 383 private final ArrayList<ItemInfo> mItems; 384 UpdateItemsRunnable(ArrayList<ItemInfo> items, ArrayList<ContentValues> values)385 UpdateItemsRunnable(ArrayList<ItemInfo> items, ArrayList<ContentValues> values) { 386 mValues = values; 387 mItems = items; 388 } 389 390 @Override run()391 public void run() { 392 ArrayList<ContentProviderOperation> ops = new ArrayList<>(); 393 int count = mItems.size(); 394 for (int i = 0; i < count; i++) { 395 ItemInfo item = mItems.get(i); 396 final int itemId = item.id; 397 final Uri uri = Favorites.getContentUri(itemId); 398 ContentValues values = mValues.get(i); 399 400 ops.add(ContentProviderOperation.newUpdate(uri).withValues(values).build()); 401 updateItemArrays(item, itemId); 402 } 403 try { 404 mContext.getContentResolver().applyBatch(LauncherProvider.AUTHORITY, ops); 405 } catch (Exception e) { 406 e.printStackTrace(); 407 } 408 } 409 } 410 411 private abstract class UpdateItemBaseRunnable implements Runnable { 412 private final StackTraceElement[] mStackTrace; 413 private final ModelVerifier mVerifier = new ModelVerifier(); 414 UpdateItemBaseRunnable()415 UpdateItemBaseRunnable() { 416 mStackTrace = new Throwable().getStackTrace(); 417 } 418 updateItemArrays(ItemInfo item, int itemId)419 protected void updateItemArrays(ItemInfo item, int itemId) { 420 // Lock on mBgLock *after* the db operation 421 synchronized (mBgDataModel) { 422 checkItemInfoLocked(itemId, item, mStackTrace); 423 424 if (item.container != Favorites.CONTAINER_DESKTOP && 425 item.container != Favorites.CONTAINER_HOTSEAT) { 426 // Item is in a folder, make sure this folder exists 427 if (!mBgDataModel.folders.containsKey(item.container)) { 428 // An items container is being set to a that of an item which is not in 429 // the list of Folders. 430 String msg = "item: " + item + " container being set to: " + 431 item.container + ", not in the list of folders"; 432 Log.e(TAG, msg); 433 } 434 } 435 436 // Items are added/removed from the corresponding FolderInfo elsewhere, such 437 // as in Workspace.onDrop. Here, we just add/remove them from the list of items 438 // that are on the desktop, as appropriate 439 ItemInfo modelItem = mBgDataModel.itemsIdMap.get(itemId); 440 if (modelItem != null && 441 (modelItem.container == Favorites.CONTAINER_DESKTOP || 442 modelItem.container == Favorites.CONTAINER_HOTSEAT)) { 443 switch (modelItem.itemType) { 444 case Favorites.ITEM_TYPE_APPLICATION: 445 case Favorites.ITEM_TYPE_SHORTCUT: 446 case Favorites.ITEM_TYPE_DEEP_SHORTCUT: 447 case Favorites.ITEM_TYPE_FOLDER: 448 if (!mBgDataModel.workspaceItems.contains(modelItem)) { 449 mBgDataModel.workspaceItems.add(modelItem); 450 } 451 break; 452 default: 453 break; 454 } 455 } else { 456 mBgDataModel.workspaceItems.remove(modelItem); 457 } 458 mVerifier.verifyModel(); 459 } 460 } 461 } 462 463 /** 464 * Utility class to verify model updates are propagated properly to the callback. 465 */ 466 public class ModelVerifier { 467 468 final int startId; 469 ModelVerifier()470 ModelVerifier() { 471 startId = mBgDataModel.lastBindId; 472 } 473 verifyModel()474 void verifyModel() { 475 if (!mVerifyChanges || mModel.getCallback() == null) { 476 return; 477 } 478 479 int executeId = mBgDataModel.lastBindId; 480 481 mUiHandler.post(() -> { 482 int currentId = mBgDataModel.lastBindId; 483 if (currentId > executeId) { 484 // Model was already bound after job was executed. 485 return; 486 } 487 if (executeId == startId) { 488 // Bound model has not changed during the job 489 return; 490 } 491 // Bound model was changed between submitting the job and executing the job 492 Callbacks callbacks = mModel.getCallback(); 493 if (callbacks != null) { 494 callbacks.rebindModel(); 495 } 496 }); 497 } 498 } 499 } 500