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