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