/* * 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 * */ 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 */ 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 . We assume that the * cellX, cellY have already been updated on the ItemInfos. */ public void moveItemsInDatabase(final ArrayList items, long container, int screen) { ArrayList 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 */ 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 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 mValues; private final ArrayList mItems; UpdateItemsRunnable(ArrayList items, ArrayList values) { mValues = values; mItems = items; } @Override public void run() { ArrayList 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); } } } } }