| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.launcher3.model; |
| |
| import android.content.ContentProviderOperation; |
| import android.content.ContentResolver; |
| import android.content.ContentValues; |
| import android.content.Context; |
| import android.net.Uri; |
| import android.util.Log; |
| |
| import com.android.launcher3.FolderInfo; |
| import com.android.launcher3.ItemInfo; |
| import com.android.launcher3.LauncherAppState; |
| import com.android.launcher3.LauncherModel; |
| import com.android.launcher3.LauncherProvider; |
| import com.android.launcher3.LauncherSettings; |
| import com.android.launcher3.LauncherSettings.Favorites; |
| import com.android.launcher3.LauncherSettings.Settings; |
| import com.android.launcher3.ShortcutInfo; |
| import com.android.launcher3.util.ContentWriter; |
| import com.android.launcher3.util.ItemInfoMatcher; |
| import com.android.launcher3.util.LooperExecutor; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.concurrent.Executor; |
| |
| /** |
| * Class for handling model updates. |
| */ |
| public class ModelWriter { |
| |
| private static final String TAG = "ModelWriter"; |
| |
| private final Context mContext; |
| private final BgDataModel mBgDataModel; |
| private final Executor mWorkerExecutor; |
| private final boolean mHasVerticalHotseat; |
| |
| public ModelWriter(Context context, BgDataModel dataModel, boolean hasVerticalHotseat) { |
| mContext = context; |
| mBgDataModel = dataModel; |
| mWorkerExecutor = new LooperExecutor(LauncherModel.getWorkerLooper()); |
| mHasVerticalHotseat = hasVerticalHotseat; |
| } |
| |
| private void updateItemInfoProps( |
| ItemInfo item, long container, long screenId, int cellX, int cellY) { |
| item.container = container; |
| item.cellX = cellX; |
| item.cellY = cellY; |
| // We store hotseat items in canonical form which is this orientation invariant position |
| // in the hotseat |
| if (container == Favorites.CONTAINER_HOTSEAT) { |
| item.screenId = mHasVerticalHotseat |
| ? LauncherAppState.getIDP(mContext).numHotseatIcons - cellY - 1 : cellX; |
| } else { |
| item.screenId = screenId; |
| } |
| } |
| |
| /** |
| * Adds an item to the DB if it was not created previously, or move it to a new |
| * <container, screen, cellX, cellY> |
| */ |
| public void addOrMoveItemInDatabase(ItemInfo item, |
| long container, long screenId, int cellX, int cellY) { |
| if (item.container == ItemInfo.NO_ID) { |
| // From all apps |
| addItemToDatabase(item, container, screenId, cellX, cellY); |
| } else { |
| // From somewhere else |
| moveItemInDatabase(item, container, screenId, cellX, cellY); |
| } |
| } |
| |
| private void checkItemInfoLocked(long itemId, ItemInfo item, StackTraceElement[] stackTrace) { |
| ItemInfo modelItem = mBgDataModel.itemsIdMap.get(itemId); |
| if (modelItem != null && item != modelItem) { |
| // check all the data is consistent |
| if (modelItem instanceof ShortcutInfo && item instanceof ShortcutInfo) { |
| ShortcutInfo modelShortcut = (ShortcutInfo) modelItem; |
| ShortcutInfo shortcut = (ShortcutInfo) item; |
| if (modelShortcut.title.toString().equals(shortcut.title.toString()) && |
| modelShortcut.intent.filterEquals(shortcut.intent) && |
| modelShortcut.id == shortcut.id && |
| modelShortcut.itemType == shortcut.itemType && |
| modelShortcut.container == shortcut.container && |
| modelShortcut.screenId == shortcut.screenId && |
| modelShortcut.cellX == shortcut.cellX && |
| modelShortcut.cellY == shortcut.cellY && |
| modelShortcut.spanX == shortcut.spanX && |
| modelShortcut.spanY == shortcut.spanY) { |
| // For all intents and purposes, this is the same object |
| return; |
| } |
| } |
| |
| // the modelItem needs to match up perfectly with item if our model is |
| // to be consistent with the database-- for now, just require |
| // modelItem == item or the equality check above |
| String msg = "item: " + ((item != null) ? item.toString() : "null") + |
| "modelItem: " + |
| ((modelItem != null) ? modelItem.toString() : "null") + |
| "Error: ItemInfo passed to checkItemInfo doesn't match original"; |
| RuntimeException e = new RuntimeException(msg); |
| if (stackTrace != null) { |
| e.setStackTrace(stackTrace); |
| } |
| throw e; |
| } |
| } |
| |
| /** |
| * Move an item in the DB to a new <container, screen, cellX, cellY> |
| */ |
| public void moveItemInDatabase(final ItemInfo item, |
| long container, long screenId, int cellX, int cellY) { |
| updateItemInfoProps(item, container, screenId, cellX, cellY); |
| |
| final ContentWriter writer = new ContentWriter(mContext) |
| .put(Favorites.CONTAINER, item.container) |
| .put(Favorites.CELLX, item.cellX) |
| .put(Favorites.CELLY, item.cellY) |
| .put(Favorites.RANK, item.rank) |
| .put(Favorites.SCREEN, item.screenId); |
| |
| mWorkerExecutor.execute(new UpdateItemRunnable(item, writer)); |
| } |
| |
| /** |
| * Move items in the DB to a new <container, screen, cellX, cellY>. We assume that the |
| * cellX, cellY have already been updated on the ItemInfos. |
| */ |
| public void moveItemsInDatabase(final ArrayList<ItemInfo> items, long container, int screen) { |
| ArrayList<ContentValues> contentValues = new ArrayList<>(); |
| int count = items.size(); |
| |
| for (int i = 0; i < count; i++) { |
| ItemInfo item = items.get(i); |
| updateItemInfoProps(item, container, screen, item.cellX, item.cellY); |
| |
| final ContentValues values = new ContentValues(); |
| values.put(Favorites.CONTAINER, item.container); |
| values.put(Favorites.CELLX, item.cellX); |
| values.put(Favorites.CELLY, item.cellY); |
| values.put(Favorites.RANK, item.rank); |
| values.put(Favorites.SCREEN, item.screenId); |
| |
| contentValues.add(values); |
| } |
| mWorkerExecutor.execute(new UpdateItemsRunnable(items, contentValues)); |
| } |
| |
| /** |
| * Move and/or resize item in the DB to a new <container, screen, cellX, cellY, spanX, spanY> |
| */ |
| public void modifyItemInDatabase(final ItemInfo item, |
| long container, long screenId, int cellX, int cellY, int spanX, int spanY) { |
| updateItemInfoProps(item, container, screenId, cellX, cellY); |
| item.spanX = spanX; |
| item.spanY = spanY; |
| |
| final ContentWriter writer = new ContentWriter(mContext) |
| .put(Favorites.CONTAINER, item.container) |
| .put(Favorites.CELLX, item.cellX) |
| .put(Favorites.CELLY, item.cellY) |
| .put(Favorites.RANK, item.rank) |
| .put(Favorites.SPANX, item.spanX) |
| .put(Favorites.SPANY, item.spanY) |
| .put(Favorites.SCREEN, item.screenId); |
| |
| mWorkerExecutor.execute(new UpdateItemRunnable(item, writer)); |
| } |
| |
| /** |
| * Update an item to the database in a specified container. |
| */ |
| public void updateItemInDatabase(ItemInfo item) { |
| ContentWriter writer = new ContentWriter(mContext); |
| item.onAddToDatabase(writer); |
| mWorkerExecutor.execute(new UpdateItemRunnable(item, writer)); |
| } |
| |
| /** |
| * Add an item to the database in a specified container. Sets the container, screen, cellX and |
| * cellY fields of the item. Also assigns an ID to the item. |
| */ |
| public void addItemToDatabase(final ItemInfo item, |
| long container, long screenId, int cellX, int cellY) { |
| updateItemInfoProps(item, container, screenId, cellX, cellY); |
| |
| final ContentWriter writer = new ContentWriter(mContext); |
| final ContentResolver cr = mContext.getContentResolver(); |
| item.onAddToDatabase(writer); |
| |
| item.id = Settings.call(cr, Settings.METHOD_NEW_ITEM_ID).getLong(Settings.EXTRA_VALUE); |
| writer.put(Favorites._ID, item.id); |
| |
| final StackTraceElement[] stackTrace = new Throwable().getStackTrace(); |
| mWorkerExecutor.execute(new Runnable() { |
| public void run() { |
| cr.insert(Favorites.CONTENT_URI, writer.getValues(mContext)); |
| |
| synchronized (mBgDataModel) { |
| checkItemInfoLocked(item.id, item, stackTrace); |
| mBgDataModel.addItem(mContext, item, true); |
| } |
| } |
| }); |
| } |
| |
| /** |
| * Removes the specified item from the database |
| */ |
| public void deleteItemFromDatabase(ItemInfo item) { |
| deleteItemsFromDatabase(Arrays.asList(item)); |
| } |
| |
| /** |
| * Removes all the items from the database matching {@param matcher}. |
| */ |
| public void deleteItemsFromDatabase(ItemInfoMatcher matcher) { |
| deleteItemsFromDatabase(matcher.filterItemInfos(mBgDataModel.itemsIdMap)); |
| } |
| |
| /** |
| * Removes the specified items from the database |
| */ |
| public void deleteItemsFromDatabase(final Iterable<? extends ItemInfo> items) { |
| mWorkerExecutor.execute(new Runnable() { |
| public void run() { |
| for (ItemInfo item : items) { |
| final Uri uri = Favorites.getContentUri(item.id); |
| mContext.getContentResolver().delete(uri, null, null); |
| |
| mBgDataModel.removeItem(mContext, item); |
| } |
| } |
| }); |
| } |
| |
| /** |
| * Remove the specified folder and all its contents from the database. |
| */ |
| public void deleteFolderAndContentsFromDatabase(final FolderInfo info) { |
| mWorkerExecutor.execute(new Runnable() { |
| public void run() { |
| ContentResolver cr = mContext.getContentResolver(); |
| cr.delete(LauncherSettings.Favorites.CONTENT_URI, |
| LauncherSettings.Favorites.CONTAINER + "=" + info.id, null); |
| mBgDataModel.removeItem(mContext, info.contents); |
| info.contents.clear(); |
| |
| cr.delete(LauncherSettings.Favorites.getContentUri(info.id), null, null); |
| mBgDataModel.removeItem(mContext, info); |
| } |
| }); |
| } |
| |
| private class UpdateItemRunnable extends UpdateItemBaseRunnable { |
| private final ItemInfo mItem; |
| private final ContentWriter mWriter; |
| private final long mItemId; |
| |
| UpdateItemRunnable(ItemInfo item, ContentWriter writer) { |
| mItem = item; |
| mWriter = writer; |
| mItemId = item.id; |
| } |
| |
| @Override |
| public void run() { |
| Uri uri = Favorites.getContentUri(mItemId); |
| mContext.getContentResolver().update(uri, mWriter.getValues(mContext), null, null); |
| updateItemArrays(mItem, mItemId); |
| } |
| } |
| |
| private class UpdateItemsRunnable extends UpdateItemBaseRunnable { |
| private final ArrayList<ContentValues> mValues; |
| private final ArrayList<ItemInfo> mItems; |
| |
| UpdateItemsRunnable(ArrayList<ItemInfo> items, ArrayList<ContentValues> values) { |
| mValues = values; |
| mItems = items; |
| } |
| |
| @Override |
| public void run() { |
| ArrayList<ContentProviderOperation> ops = new ArrayList<>(); |
| int count = mItems.size(); |
| for (int i = 0; i < count; i++) { |
| ItemInfo item = mItems.get(i); |
| final long itemId = item.id; |
| final Uri uri = Favorites.getContentUri(itemId); |
| ContentValues values = mValues.get(i); |
| |
| ops.add(ContentProviderOperation.newUpdate(uri).withValues(values).build()); |
| updateItemArrays(item, itemId); |
| } |
| try { |
| mContext.getContentResolver().applyBatch(LauncherProvider.AUTHORITY, ops); |
| } catch (Exception e) { |
| e.printStackTrace(); |
| } |
| } |
| } |
| |
| private abstract class UpdateItemBaseRunnable implements Runnable { |
| private final StackTraceElement[] mStackTrace; |
| |
| UpdateItemBaseRunnable() { |
| mStackTrace = new Throwable().getStackTrace(); |
| } |
| |
| protected void updateItemArrays(ItemInfo item, long itemId) { |
| // Lock on mBgLock *after* the db operation |
| synchronized (mBgDataModel) { |
| checkItemInfoLocked(itemId, item, mStackTrace); |
| |
| if (item.container != Favorites.CONTAINER_DESKTOP && |
| item.container != Favorites.CONTAINER_HOTSEAT) { |
| // Item is in a folder, make sure this folder exists |
| if (!mBgDataModel.folders.containsKey(item.container)) { |
| // An items container is being set to a that of an item which is not in |
| // the list of Folders. |
| String msg = "item: " + item + " container being set to: " + |
| item.container + ", not in the list of folders"; |
| Log.e(TAG, msg); |
| } |
| } |
| |
| // Items are added/removed from the corresponding FolderInfo elsewhere, such |
| // as in Workspace.onDrop. Here, we just add/remove them from the list of items |
| // that are on the desktop, as appropriate |
| ItemInfo modelItem = mBgDataModel.itemsIdMap.get(itemId); |
| if (modelItem != null && |
| (modelItem.container == Favorites.CONTAINER_DESKTOP || |
| modelItem.container == Favorites.CONTAINER_HOTSEAT)) { |
| switch (modelItem.itemType) { |
| case Favorites.ITEM_TYPE_APPLICATION: |
| case Favorites.ITEM_TYPE_SHORTCUT: |
| case Favorites.ITEM_TYPE_DEEP_SHORTCUT: |
| case Favorites.ITEM_TYPE_FOLDER: |
| if (!mBgDataModel.workspaceItems.contains(modelItem)) { |
| mBgDataModel.workspaceItems.add(modelItem); |
| } |
| break; |
| default: |
| break; |
| } |
| } else { |
| mBgDataModel.workspaceItems.remove(modelItem); |
| } |
| } |
| } |
| } |
| } |