Home | History | Annotate | Download | only in provider
      1 /*
      2  * Copyright (C) 2016 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.provider;
     18 
     19 import android.content.ContentProviderOperation;
     20 import android.content.ContentValues;
     21 import android.content.Context;
     22 import android.content.Intent;
     23 import android.content.SharedPreferences;
     24 import android.content.pm.ApplicationInfo;
     25 import android.content.pm.PackageManager;
     26 import android.content.pm.ProviderInfo;
     27 import android.database.Cursor;
     28 import android.database.sqlite.SQLiteDatabase;
     29 import android.net.Uri;
     30 import android.os.Process;
     31 import android.text.TextUtils;
     32 import android.util.LongSparseArray;
     33 import android.util.SparseBooleanArray;
     34 
     35 import com.android.launcher3.AutoInstallsLayout.LayoutParserCallback;
     36 import com.android.launcher3.DefaultLayoutParser;
     37 import com.android.launcher3.LauncherAppState;
     38 import com.android.launcher3.LauncherAppWidgetInfo;
     39 import com.android.launcher3.LauncherFiles;
     40 import com.android.launcher3.LauncherSettings;
     41 import com.android.launcher3.LauncherSettings.Favorites;
     42 import com.android.launcher3.LauncherSettings.Settings;
     43 import com.android.launcher3.LauncherSettings.WorkspaceScreens;
     44 import com.android.launcher3.R;
     45 import com.android.launcher3.Utilities;
     46 import com.android.launcher3.Workspace;
     47 import com.android.launcher3.compat.UserManagerCompat;
     48 import com.android.launcher3.config.FeatureFlags;
     49 import com.android.launcher3.config.ProviderConfig;
     50 import com.android.launcher3.logging.FileLog;
     51 import com.android.launcher3.model.GridSizeMigrationTask;
     52 import com.android.launcher3.util.LongArrayMap;
     53 
     54 import java.net.URISyntaxException;
     55 import java.util.ArrayList;
     56 import java.util.HashMap;
     57 import java.util.HashSet;
     58 
     59 /**
     60  * Utility class to import data from another Launcher which is based on Launcher3 schema.
     61  */
     62 public class ImportDataTask {
     63 
     64     public static final String KEY_DATA_IMPORT_SRC_PKG = "data_import_src_pkg";
     65     public static final String KEY_DATA_IMPORT_SRC_AUTHORITY = "data_import_src_authority";
     66 
     67     private static final String TAG = "ImportDataTask";
     68     private static final int MIN_ITEM_COUNT_FOR_SUCCESSFUL_MIGRATION = 6;
     69     // Insert items progressively to avoid OOM exception when loading icons.
     70     private static final int BATCH_INSERT_SIZE = 15;
     71 
     72     private final Context mContext;
     73 
     74     private final Uri mOtherScreensUri;
     75     private final Uri mOtherFavoritesUri;
     76 
     77     private int mHotseatSize;
     78     private int mMaxGridSizeX;
     79     private int mMaxGridSizeY;
     80 
     81     private ImportDataTask(Context context, String sourceAuthority) {
     82         mContext = context;
     83         mOtherScreensUri = Uri.parse("content://" +
     84                 sourceAuthority + "/" + WorkspaceScreens.TABLE_NAME);
     85         mOtherFavoritesUri = Uri.parse("content://" + sourceAuthority + "/" + Favorites.TABLE_NAME);
     86     }
     87 
     88     public boolean importWorkspace() throws Exception {
     89         ArrayList<Long> allScreens = LauncherDbUtils.getScreenIdsFromCursor(
     90                 mContext.getContentResolver().query(mOtherScreensUri, null, null, null,
     91                         LauncherSettings.WorkspaceScreens.SCREEN_RANK));
     92         FileLog.d(TAG, "Importing DB from " + mOtherFavoritesUri);
     93 
     94         // During import we reset the screen IDs to 0-indexed values.
     95         if (allScreens.isEmpty()) {
     96             // No thing to migrate
     97             FileLog.e(TAG, "No data found to import");
     98             return false;
     99         }
    100 
    101         mHotseatSize = mMaxGridSizeX = mMaxGridSizeY = 0;
    102 
    103         // Build screen update
    104         ArrayList<ContentProviderOperation> screenOps = new ArrayList<>();
    105         int count = allScreens.size();
    106         LongSparseArray<Long> screenIdMap = new LongSparseArray<>(count);
    107         for (int i = 0; i < count; i++) {
    108             ContentValues v = new ContentValues();
    109             v.put(LauncherSettings.WorkspaceScreens._ID, i);
    110             v.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, i);
    111             screenIdMap.put(allScreens.get(i), (long) i);
    112             screenOps.add(ContentProviderOperation.newInsert(
    113                     LauncherSettings.WorkspaceScreens.CONTENT_URI).withValues(v).build());
    114         }
    115         mContext.getContentResolver().applyBatch(ProviderConfig.AUTHORITY, screenOps);
    116         importWorkspaceItems(allScreens.get(0), screenIdMap);
    117 
    118         GridSizeMigrationTask.markForMigration(mContext, mMaxGridSizeX, mMaxGridSizeY, mHotseatSize);
    119 
    120         // Create empty DB flag.
    121         LauncherSettings.Settings.call(mContext.getContentResolver(),
    122                 LauncherSettings.Settings.METHOD_CLEAR_EMPTY_DB_FLAG);
    123         return true;
    124     }
    125 
    126     /**
    127      * 1) Imports all the workspace entries from the source provider.
    128      * 2) For home screen entries, maps the screen id based on {@param screenIdMap}
    129      * 3) In the end fills any holes in hotseat with items from default hotseat layout.
    130      */
    131     private void importWorkspaceItems(
    132             long firsetScreenId, LongSparseArray<Long> screenIdMap) throws Exception {
    133         String profileId = Long.toString(UserManagerCompat.getInstance(mContext)
    134                 .getSerialNumberForUser(Process.myUserHandle()));
    135 
    136         boolean createEmptyRowOnFirstScreen = false;
    137         if (FeatureFlags.QSB_ON_FIRST_SCREEN) {
    138             try (Cursor c = mContext.getContentResolver().query(mOtherFavoritesUri, null,
    139                     // get items on the first row of the first screen
    140                     "profileId = ? AND container = -100 AND screen = ? AND cellY = 0",
    141                     new String[]{profileId, Long.toString(firsetScreenId)},
    142                     null)) {
    143                 // First row of first screen is not empty
    144                 createEmptyRowOnFirstScreen = c.moveToNext();
    145             }
    146         }
    147 
    148         ArrayList<ContentProviderOperation> insertOperations = new ArrayList<>(BATCH_INSERT_SIZE);
    149 
    150         // Set of package names present in hotseat
    151         final HashSet<String> hotseatTargetApps = new HashSet<>();
    152         int maxId = 0;
    153 
    154         // Number of imported items on workspace and hotseat
    155         int totalItemsOnWorkspace = 0;
    156 
    157         try (Cursor c = mContext.getContentResolver()
    158                 .query(mOtherFavoritesUri, null,
    159                         // Only migrate the primary user
    160                         Favorites.PROFILE_ID + " = ?", new String[]{profileId},
    161                         // Get the items sorted by container, so that the folders are loaded
    162                         // before the corresponding items.
    163                         Favorites.CONTAINER)) {
    164 
    165             // various columns we expect to exist.
    166             final int idIndex = c.getColumnIndexOrThrow(Favorites._ID);
    167             final int intentIndex = c.getColumnIndexOrThrow(Favorites.INTENT);
    168             final int titleIndex = c.getColumnIndexOrThrow(Favorites.TITLE);
    169             final int containerIndex = c.getColumnIndexOrThrow(Favorites.CONTAINER);
    170             final int itemTypeIndex = c.getColumnIndexOrThrow(Favorites.ITEM_TYPE);
    171             final int widgetProviderIndex = c.getColumnIndexOrThrow(Favorites.APPWIDGET_PROVIDER);
    172             final int screenIndex = c.getColumnIndexOrThrow(Favorites.SCREEN);
    173             final int cellXIndex = c.getColumnIndexOrThrow(Favorites.CELLX);
    174             final int cellYIndex = c.getColumnIndexOrThrow(Favorites.CELLY);
    175             final int spanXIndex = c.getColumnIndexOrThrow(Favorites.SPANX);
    176             final int spanYIndex = c.getColumnIndexOrThrow(Favorites.SPANY);
    177             final int rankIndex = c.getColumnIndexOrThrow(Favorites.RANK);
    178             final int iconIndex = c.getColumnIndexOrThrow(Favorites.ICON);
    179             final int iconPackageIndex = c.getColumnIndexOrThrow(Favorites.ICON_PACKAGE);
    180             final int iconResourceIndex = c.getColumnIndexOrThrow(Favorites.ICON_RESOURCE);
    181 
    182             SparseBooleanArray mValidFolders = new SparseBooleanArray();
    183             ContentValues values = new ContentValues();
    184 
    185             while (c.moveToNext()) {
    186                 values.clear();
    187                 int id = c.getInt(idIndex);
    188                 maxId = Math.max(maxId, id);
    189                 int type = c.getInt(itemTypeIndex);
    190                 int container = c.getInt(containerIndex);
    191 
    192                 long screen = c.getLong(screenIndex);
    193 
    194                 int cellX = c.getInt(cellXIndex);
    195                 int cellY = c.getInt(cellYIndex);
    196                 int spanX = c.getInt(spanXIndex);
    197                 int spanY = c.getInt(spanYIndex);
    198 
    199                 switch (container) {
    200                     case Favorites.CONTAINER_DESKTOP: {
    201                         Long newScreenId = screenIdMap.get(screen);
    202                         if (newScreenId == null) {
    203                             FileLog.d(TAG, String.format("Skipping item %d, type %d not on a valid screen %d", id, type, screen));
    204                             continue;
    205                         }
    206                         // Reset the screen to 0-index value
    207                         screen = newScreenId;
    208                         if (createEmptyRowOnFirstScreen && screen == Workspace.FIRST_SCREEN_ID) {
    209                             // Shift items by 1.
    210                             cellY++;
    211                         }
    212 
    213                         mMaxGridSizeX = Math.max(mMaxGridSizeX, cellX + spanX);
    214                         mMaxGridSizeY = Math.max(mMaxGridSizeY, cellY + spanY);
    215                         break;
    216                     }
    217                     case Favorites.CONTAINER_HOTSEAT: {
    218                         mHotseatSize = Math.max(mHotseatSize, (int) screen + 1);
    219                         break;
    220                     }
    221                     default:
    222                         if (!mValidFolders.get(container)) {
    223                             FileLog.d(TAG, String.format("Skipping item %d, type %d not in a valid folder %d", id, type, container));
    224                             continue;
    225                         }
    226                 }
    227 
    228                 Intent intent = null;
    229                 switch (type) {
    230                     case Favorites.ITEM_TYPE_FOLDER: {
    231                         mValidFolders.put(id, true);
    232                         // Use a empty intent to indicate a folder.
    233                         intent = new Intent();
    234                         break;
    235                     }
    236                     case Favorites.ITEM_TYPE_APPWIDGET: {
    237                         values.put(Favorites.RESTORED,
    238                                 LauncherAppWidgetInfo.FLAG_ID_NOT_VALID |
    239                                         LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY |
    240                                         LauncherAppWidgetInfo.FLAG_UI_NOT_READY);
    241                         values.put(Favorites.APPWIDGET_PROVIDER, c.getString(widgetProviderIndex));
    242                         break;
    243                     }
    244                     case Favorites.ITEM_TYPE_SHORTCUT:
    245                     case Favorites.ITEM_TYPE_APPLICATION: {
    246                         intent = Intent.parseUri(c.getString(intentIndex), 0);
    247                         if (Utilities.isLauncherAppTarget(intent)) {
    248                             type = Favorites.ITEM_TYPE_APPLICATION;
    249                         } else {
    250                             values.put(Favorites.ICON_PACKAGE, c.getString(iconPackageIndex));
    251                             values.put(Favorites.ICON_RESOURCE, c.getString(iconResourceIndex));
    252                         }
    253                         values.put(Favorites.ICON,  c.getBlob(iconIndex));
    254                         values.put(Favorites.INTENT, intent.toUri(0));
    255                         values.put(Favorites.RANK, c.getInt(rankIndex));
    256 
    257                         values.put(Favorites.RESTORED, 1);
    258                         break;
    259                     }
    260                     default:
    261                         FileLog.d(TAG, String.format("Skipping item %d, not a valid type %d", id, type));
    262                         continue;
    263                 }
    264 
    265                 if (container == Favorites.CONTAINER_HOTSEAT) {
    266                     if (intent == null) {
    267                         FileLog.d(TAG, String.format("Skipping item %d, null intent on hotseat", id));
    268                         continue;
    269                     }
    270                     if (intent.getComponent() != null) {
    271                         intent.setPackage(intent.getComponent().getPackageName());
    272                     }
    273                     hotseatTargetApps.add(getPackage(intent));
    274                 }
    275 
    276                 values.put(Favorites._ID, id);
    277                 values.put(Favorites.ITEM_TYPE, type);
    278                 values.put(Favorites.CONTAINER, container);
    279                 values.put(Favorites.SCREEN, screen);
    280                 values.put(Favorites.CELLX, cellX);
    281                 values.put(Favorites.CELLY, cellY);
    282                 values.put(Favorites.SPANX, spanX);
    283                 values.put(Favorites.SPANY, spanY);
    284                 values.put(Favorites.TITLE, c.getString(titleIndex));
    285                 insertOperations.add(ContentProviderOperation
    286                         .newInsert(Favorites.CONTENT_URI).withValues(values).build());
    287                 if (container < 0) {
    288                     totalItemsOnWorkspace++;
    289                 }
    290 
    291                 if (insertOperations.size() >= BATCH_INSERT_SIZE) {
    292                     mContext.getContentResolver().applyBatch(ProviderConfig.AUTHORITY,
    293                             insertOperations);
    294                     insertOperations.clear();
    295                 }
    296             }
    297         }
    298         FileLog.d(TAG, totalItemsOnWorkspace + " items imported from external source");
    299         if (totalItemsOnWorkspace < MIN_ITEM_COUNT_FOR_SUCCESSFUL_MIGRATION) {
    300             throw new Exception("Insufficient data");
    301         }
    302         if (!insertOperations.isEmpty()) {
    303             mContext.getContentResolver().applyBatch(ProviderConfig.AUTHORITY,
    304                     insertOperations);
    305             insertOperations.clear();
    306         }
    307 
    308         LongArrayMap<Object> hotseatItems = GridSizeMigrationTask.removeBrokenHotseatItems(mContext);
    309         int myHotseatCount = LauncherAppState.getIDP(mContext).numHotseatIcons;
    310         if (!FeatureFlags.NO_ALL_APPS_ICON) {
    311             myHotseatCount--;
    312         }
    313         if (hotseatItems.size() < myHotseatCount) {
    314             // Insufficient hotseat items. Add a few more.
    315             HotseatParserCallback parserCallback = new HotseatParserCallback(
    316                     hotseatTargetApps, hotseatItems, insertOperations, maxId + 1, myHotseatCount);
    317             new HotseatLayoutParser(mContext,
    318                     parserCallback).loadLayout(null, new ArrayList<Long>());
    319             mHotseatSize = (int) hotseatItems.keyAt(hotseatItems.size() - 1) + 1;
    320 
    321             if (!insertOperations.isEmpty()) {
    322                 mContext.getContentResolver().applyBatch(ProviderConfig.AUTHORITY,
    323                         insertOperations);
    324             }
    325         }
    326     }
    327 
    328     private static final String getPackage(Intent intent) {
    329         return intent.getComponent() != null ? intent.getComponent().getPackageName()
    330                 : intent.getPackage();
    331     }
    332 
    333     /**
    334      * Performs data import if possible.
    335      * @return true on successful data import, false if it was not available
    336      * @throws Exception if the import failed
    337      */
    338     public static boolean performImportIfPossible(Context context) throws Exception {
    339         SharedPreferences devicePrefs = getDevicePrefs(context);
    340         String sourcePackage = devicePrefs.getString(KEY_DATA_IMPORT_SRC_PKG, "");
    341         String sourceAuthority = devicePrefs.getString(KEY_DATA_IMPORT_SRC_AUTHORITY, "");
    342 
    343         if (TextUtils.isEmpty(sourcePackage) || TextUtils.isEmpty(sourceAuthority)) {
    344             return false;
    345         }
    346 
    347         // Synchronously clear the migration flags. This ensures that we do not try migration
    348         // again and thus prevents potential crash loops due to migration failure.
    349         devicePrefs.edit().remove(KEY_DATA_IMPORT_SRC_PKG).remove(KEY_DATA_IMPORT_SRC_AUTHORITY).commit();
    350 
    351         if (!Settings.call(context.getContentResolver(), Settings.METHOD_WAS_EMPTY_DB_CREATED)
    352                 .getBoolean(Settings.EXTRA_VALUE, false)) {
    353             // Only migration if a new DB was created.
    354             return false;
    355         }
    356 
    357         for (ProviderInfo info : context.getPackageManager().queryContentProviders(
    358                 null, context.getApplicationInfo().uid, 0)) {
    359 
    360             if (sourcePackage.equals(info.packageName)) {
    361                 if ((info.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) {
    362                     // Only migrate if the source launcher is also on system image.
    363                     return false;
    364                 }
    365 
    366                 // Wait until we found a provider with matching authority.
    367                 if (sourceAuthority.equals(info.authority)) {
    368                     if (TextUtils.isEmpty(info.readPermission) ||
    369                             context.checkPermission(info.readPermission, Process.myPid(),
    370                                     Process.myUid()) == PackageManager.PERMISSION_GRANTED) {
    371                         // All checks passed, run the import task.
    372                         return new ImportDataTask(context, sourceAuthority).importWorkspace();
    373                     }
    374                 }
    375             }
    376         }
    377         return false;
    378     }
    379 
    380     private static SharedPreferences getDevicePrefs(Context c) {
    381         return c.getSharedPreferences(LauncherFiles.DEVICE_PREFERENCES_KEY, Context.MODE_PRIVATE);
    382     }
    383 
    384     private static final int getMyHotseatLayoutId(Context context) {
    385         return LauncherAppState.getIDP(context).numHotseatIcons <= 5
    386                 ? R.xml.dw_phone_hotseat
    387                 : R.xml.dw_tablet_hotseat;
    388     }
    389 
    390     /**
    391      * Extension of {@link DefaultLayoutParser} which only allows icons and shortcuts.
    392      */
    393     private static class HotseatLayoutParser extends DefaultLayoutParser {
    394         public HotseatLayoutParser(Context context, LayoutParserCallback callback) {
    395             super(context, null, callback, context.getResources(), getMyHotseatLayoutId(context));
    396         }
    397 
    398         @Override
    399         protected HashMap<String, TagParser> getLayoutElementsMap() {
    400             // Only allow shortcut parsers
    401             HashMap<String, TagParser> parsers = new HashMap<String, TagParser>();
    402             parsers.put(TAG_FAVORITE, new AppShortcutWithUriParser());
    403             parsers.put(TAG_SHORTCUT, new UriShortcutParser(mSourceRes));
    404             parsers.put(TAG_RESOLVE, new ResolveParser());
    405             return parsers;
    406         }
    407     }
    408 
    409     /**
    410      * {@link LayoutParserCallback} which adds items in empty hotseat spots.
    411      */
    412     private static class HotseatParserCallback implements LayoutParserCallback {
    413         private final HashSet<String> mExisitingApps;
    414         private final LongArrayMap<Object> mExistingItems;
    415         private final ArrayList<ContentProviderOperation> mOutOps;
    416         private final int mRequiredSize;
    417         private int mStartItemId;
    418 
    419         HotseatParserCallback(
    420                 HashSet<String> existingApps, LongArrayMap<Object> existingItems,
    421                 ArrayList<ContentProviderOperation> outOps, int startItemId, int requiredSize) {
    422             mExisitingApps = existingApps;
    423             mExistingItems = existingItems;
    424             mOutOps = outOps;
    425             mRequiredSize = requiredSize;
    426             mStartItemId = startItemId;
    427         }
    428 
    429         @Override
    430         public long generateNewItemId() {
    431             return mStartItemId++;
    432         }
    433 
    434         @Override
    435         public long insertAndCheck(SQLiteDatabase db, ContentValues values) {
    436             if (mExistingItems.size() >= mRequiredSize) {
    437                 // No need to add more items.
    438                 return 0;
    439             }
    440             Intent intent;
    441             try {
    442                 intent = Intent.parseUri(values.getAsString(Favorites.INTENT), 0);
    443             } catch (URISyntaxException e) {
    444                 return 0;
    445             }
    446             String pkg = getPackage(intent);
    447             if (pkg == null || mExisitingApps.contains(pkg)) {
    448                 // The item does not target an app or is already in hotseat.
    449                 return 0;
    450             }
    451             mExisitingApps.add(pkg);
    452 
    453             // find next vacant spot.
    454             long screen = 0;
    455             while (mExistingItems.get(screen) != null) {
    456                 screen++;
    457             }
    458             mExistingItems.put(screen, intent);
    459             values.put(Favorites.SCREEN, screen);
    460             mOutOps.add(ContentProviderOperation.newInsert(Favorites.CONTENT_URI).withValues(values).build());
    461             return 0;
    462         }
    463     }
    464 }
    465