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