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