Home | History | Annotate | Download | only in model
      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 
     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 
     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      */
     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 
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    251     public void deleteItemsFromDatabase(ItemInfoMatcher matcher) {
    252         deleteItemsFromDatabase(matcher.filterItemInfos(mBgDataModel.itemsIdMap));
    253     }
    254 
    255     /**
    256      * Removes the specified items from the database
    257      */
    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      */
    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 
    296         UpdateItemRunnable(ItemInfo item, ContentWriter writer) {
    297             mItem = item;
    298             mWriter = writer;
    299             mItemId = item.id;
    300         }
    301 
    302         @Override
    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 
    314         UpdateItemsRunnable(ArrayList<ItemInfo> items, ArrayList<ContentValues> values) {
    315             mValues = values;
    316             mItems = items;
    317         }
    318 
    319         @Override
    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 
    344         UpdateItemBaseRunnable() {
    345             mStackTrace = new Throwable().getStackTrace();
    346         }
    347 
    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 
    399         ModelVerifier() {
    400             startId = mBgDataModel.lastBindId;
    401         }
    402 
    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