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