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