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.FolderInfo;
31 import com.android.launcher3.ItemInfo;
32 import com.android.launcher3.LauncherAppState;
33 import com.android.launcher3.LauncherAppWidgetHost;
34 import com.android.launcher3.LauncherAppWidgetInfo;
35 import com.android.launcher3.LauncherModel;
36 import com.android.launcher3.LauncherProvider;
37 import com.android.launcher3.LauncherSettings;
38 import com.android.launcher3.LauncherSettings.Favorites;
39 import com.android.launcher3.LauncherSettings.Settings;
40 import com.android.launcher3.Utilities;
41 import com.android.launcher3.WorkspaceItemInfo;
42 import com.android.launcher3.config.FeatureFlags;
43 import com.android.launcher3.logging.FileLog;
44 import com.android.launcher3.model.BgDataModel.Callbacks;
45 import com.android.launcher3.util.ContentWriter;
46 import com.android.launcher3.util.ItemInfoMatcher;
47 
48 import java.util.ArrayList;
49 import java.util.Arrays;
50 import java.util.Collection;
51 import java.util.List;
52 import java.util.concurrent.Executor;
53 import java.util.function.Supplier;
54 import java.util.stream.Collectors;
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).numHotseatIcons - 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_DOGFOOD_BUILD &&
120                     modelItem instanceof WorkspaceItemInfo && 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_DOGFOOD_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 
abortDelete(int pageToBindFirst)353     public void abortDelete(int pageToBindFirst) {
354         mPreparingToUndo = false;
355         mDeleteRunnables.clear();
356         // We do a full reload here instead of just a rebind because Folders change their internal
357         // state when dragging an item out, which clobbers the rebind unless we load from the DB.
358         mModel.forceReload(pageToBindFirst);
359     }
360 
361     private class UpdateItemRunnable extends UpdateItemBaseRunnable {
362         private final ItemInfo mItem;
363         private final Supplier<ContentWriter> mWriter;
364         private final int mItemId;
365 
UpdateItemRunnable(ItemInfo item, Supplier<ContentWriter> writer)366         UpdateItemRunnable(ItemInfo item, Supplier<ContentWriter> writer) {
367             mItem = item;
368             mWriter = writer;
369             mItemId = item.id;
370         }
371 
372         @Override
run()373         public void run() {
374             Uri uri = Favorites.getContentUri(mItemId);
375             mContext.getContentResolver().update(uri, mWriter.get().getValues(mContext),
376                     null, null);
377             updateItemArrays(mItem, mItemId);
378         }
379     }
380 
381     private class UpdateItemsRunnable extends UpdateItemBaseRunnable {
382         private final ArrayList<ContentValues> mValues;
383         private final ArrayList<ItemInfo> mItems;
384 
UpdateItemsRunnable(ArrayList<ItemInfo> items, ArrayList<ContentValues> values)385         UpdateItemsRunnable(ArrayList<ItemInfo> items, ArrayList<ContentValues> values) {
386             mValues = values;
387             mItems = items;
388         }
389 
390         @Override
run()391         public void run() {
392             ArrayList<ContentProviderOperation> ops = new ArrayList<>();
393             int count = mItems.size();
394             for (int i = 0; i < count; i++) {
395                 ItemInfo item = mItems.get(i);
396                 final int itemId = item.id;
397                 final Uri uri = Favorites.getContentUri(itemId);
398                 ContentValues values = mValues.get(i);
399 
400                 ops.add(ContentProviderOperation.newUpdate(uri).withValues(values).build());
401                 updateItemArrays(item, itemId);
402             }
403             try {
404                 mContext.getContentResolver().applyBatch(LauncherProvider.AUTHORITY, ops);
405             } catch (Exception e) {
406                 e.printStackTrace();
407             }
408         }
409     }
410 
411     private abstract class UpdateItemBaseRunnable implements Runnable {
412         private final StackTraceElement[] mStackTrace;
413         private final ModelVerifier mVerifier = new ModelVerifier();
414 
UpdateItemBaseRunnable()415         UpdateItemBaseRunnable() {
416             mStackTrace = new Throwable().getStackTrace();
417         }
418 
updateItemArrays(ItemInfo item, int itemId)419         protected void updateItemArrays(ItemInfo item, int itemId) {
420             // Lock on mBgLock *after* the db operation
421             synchronized (mBgDataModel) {
422                 checkItemInfoLocked(itemId, item, mStackTrace);
423 
424                 if (item.container != Favorites.CONTAINER_DESKTOP &&
425                         item.container != Favorites.CONTAINER_HOTSEAT) {
426                     // Item is in a folder, make sure this folder exists
427                     if (!mBgDataModel.folders.containsKey(item.container)) {
428                         // An items container is being set to a that of an item which is not in
429                         // the list of Folders.
430                         String msg = "item: " + item + " container being set to: " +
431                                 item.container + ", not in the list of folders";
432                         Log.e(TAG, msg);
433                     }
434                 }
435 
436                 // Items are added/removed from the corresponding FolderInfo elsewhere, such
437                 // as in Workspace.onDrop. Here, we just add/remove them from the list of items
438                 // that are on the desktop, as appropriate
439                 ItemInfo modelItem = mBgDataModel.itemsIdMap.get(itemId);
440                 if (modelItem != null &&
441                         (modelItem.container == Favorites.CONTAINER_DESKTOP ||
442                                 modelItem.container == Favorites.CONTAINER_HOTSEAT)) {
443                     switch (modelItem.itemType) {
444                         case Favorites.ITEM_TYPE_APPLICATION:
445                         case Favorites.ITEM_TYPE_SHORTCUT:
446                         case Favorites.ITEM_TYPE_DEEP_SHORTCUT:
447                         case Favorites.ITEM_TYPE_FOLDER:
448                             if (!mBgDataModel.workspaceItems.contains(modelItem)) {
449                                 mBgDataModel.workspaceItems.add(modelItem);
450                             }
451                             break;
452                         default:
453                             break;
454                     }
455                 } else {
456                     mBgDataModel.workspaceItems.remove(modelItem);
457                 }
458                 mVerifier.verifyModel();
459             }
460         }
461     }
462 
463     /**
464      * Utility class to verify model updates are propagated properly to the callback.
465      */
466     public class ModelVerifier {
467 
468         final int startId;
469 
ModelVerifier()470         ModelVerifier() {
471             startId = mBgDataModel.lastBindId;
472         }
473 
verifyModel()474         void verifyModel() {
475             if (!mVerifyChanges || mModel.getCallback() == null) {
476                 return;
477             }
478 
479             int executeId = mBgDataModel.lastBindId;
480 
481             mUiHandler.post(() -> {
482                 int currentId = mBgDataModel.lastBindId;
483                 if (currentId > executeId) {
484                     // Model was already bound after job was executed.
485                     return;
486                 }
487                 if (executeId == startId) {
488                     // Bound model has not changed during the job
489                     return;
490                 }
491                 // Bound model was changed between submitting the job and executing the job
492                 Callbacks callbacks = mModel.getCallback();
493                 if (callbacks != null) {
494                     callbacks.rebindModel();
495                 }
496             });
497         }
498     }
499 }
500