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