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