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 ModelWriter(Context context, LauncherModel model, BgDataModel dataModel, boolean hasVerticalHotseat, boolean verifyChanges)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 updateItemInfoProps( ItemInfo item, long container, long screenId, int cellX, int cellY)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 */ addOrMoveItemInDatabase(ItemInfo item, long container, long screenId, int cellX, int cellY)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 checkItemInfoLocked(long itemId, ItemInfo item, StackTraceElement[] stackTrace)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 */ moveItemInDatabase(final ItemInfo item, long container, long screenId, int cellX, int cellY)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 */ moveItemsInDatabase(final ArrayList<ItemInfo> items, long container, int screen)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 */ modifyItemInDatabase(final ItemInfo item, long container, long screenId, int cellX, int cellY, int spanX, int spanY)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 */ updateItemInDatabase(ItemInfo item)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 */ addItemToDatabase(final ItemInfo item, long container, long screenId, int cellX, int cellY)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 */ deleteItemFromDatabase(ItemInfo item)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 */ deleteItemsFromDatabase(ItemInfoMatcher matcher)251 public void deleteItemsFromDatabase(ItemInfoMatcher matcher) { 252 deleteItemsFromDatabase(matcher.filterItemInfos(mBgDataModel.itemsIdMap)); 253 } 254 255 /** 256 * Removes the specified items from the database 257 */ deleteItemsFromDatabase(final Iterable<? extends ItemInfo> items)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 */ deleteFolderAndContentsFromDatabase(final FolderInfo info)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 UpdateItemRunnable(ItemInfo item, ContentWriter writer)296 UpdateItemRunnable(ItemInfo item, ContentWriter writer) { 297 mItem = item; 298 mWriter = writer; 299 mItemId = item.id; 300 } 301 302 @Override run()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 UpdateItemsRunnable(ArrayList<ItemInfo> items, ArrayList<ContentValues> values)314 UpdateItemsRunnable(ArrayList<ItemInfo> items, ArrayList<ContentValues> values) { 315 mValues = values; 316 mItems = items; 317 } 318 319 @Override run()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 UpdateItemBaseRunnable()344 UpdateItemBaseRunnable() { 345 mStackTrace = new Throwable().getStackTrace(); 346 } 347 updateItemArrays(ItemInfo item, long itemId)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 ModelVerifier()399 ModelVerifier() { 400 startId = mBgDataModel.lastBindId; 401 } 402 verifyModel()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