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