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 17package com.android.launcher3.model; 18 19import android.content.ContentProviderOperation; 20import android.content.ContentResolver; 21import android.content.ContentValues; 22import android.content.Context; 23import android.net.Uri; 24import android.util.Log; 25 26import com.android.launcher3.FolderInfo; 27import com.android.launcher3.ItemInfo; 28import com.android.launcher3.LauncherAppState; 29import com.android.launcher3.LauncherModel; 30import com.android.launcher3.LauncherProvider; 31import com.android.launcher3.LauncherSettings; 32import com.android.launcher3.LauncherSettings.Favorites; 33import com.android.launcher3.LauncherSettings.Settings; 34import com.android.launcher3.ShortcutInfo; 35import com.android.launcher3.util.ContentWriter; 36import com.android.launcher3.util.ItemInfoMatcher; 37import com.android.launcher3.util.LooperExecuter; 38 39import java.util.ArrayList; 40import java.util.Arrays; 41import java.util.concurrent.Executor; 42 43/** 44 * Class for handling model updates. 45 */ 46public 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 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 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 */ 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 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 238 public void deleteItemsFromDatabase(ItemInfoMatcher matcher) { 239 deleteItemsFromDatabase(matcher.filterItemInfos(mBgDataModel.itemsIdMap)); 240 } 241 242 /** 243 * Removes the specified items from the database 244 */ 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 */ 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 281 UpdateItemRunnable(ItemInfo item, ContentWriter writer) { 282 mItem = item; 283 mWriter = writer; 284 mItemId = item.id; 285 } 286 287 @Override 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 299 UpdateItemsRunnable(ArrayList<ItemInfo> items, ArrayList<ContentValues> values) { 300 mValues = values; 301 mItems = items; 302 } 303 304 @Override 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 328 UpdateItemBaseRunnable() { 329 mStackTrace = new Throwable().getStackTrace(); 330 } 331 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