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