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