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