diff options
Diffstat (limited to 'src/com/android/launcher3/provider/ImportDataTask.java')
-rw-r--r-- | src/com/android/launcher3/provider/ImportDataTask.java | 438 |
1 files changed, 438 insertions, 0 deletions
diff --git a/src/com/android/launcher3/provider/ImportDataTask.java b/src/com/android/launcher3/provider/ImportDataTask.java new file mode 100644 index 0000000000..c9af2fe6fc --- /dev/null +++ b/src/com/android/launcher3/provider/ImportDataTask.java @@ -0,0 +1,438 @@ +/* + * Copyright (C) 2016 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.provider; + +import static com.android.launcher3.Utilities.getDevicePrefs; + +import android.content.ContentProviderOperation; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ProviderInfo; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; +import android.os.Process; +import android.text.TextUtils; +import android.util.ArrayMap; +import android.util.SparseBooleanArray; + +import com.android.launcher3.AutoInstallsLayout.LayoutParserCallback; +import com.android.launcher3.DefaultLayoutParser; +import com.android.launcher3.LauncherAppState; +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.Workspace; +import com.android.launcher3.config.FeatureFlags; +import com.android.launcher3.logging.FileLog; +import com.android.launcher3.model.GridSizeMigrationTask; +import com.android.launcher3.model.data.LauncherAppWidgetInfo; +import com.android.launcher3.pm.UserCache; +import com.android.launcher3.util.IntArray; +import com.android.launcher3.util.IntSparseArrayMap; +import com.android.launcher3.util.PackageManagerHelper; + +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.HashSet; + +/** + * Utility class to import data from another Launcher which is based on Launcher3 schema. + */ +public class ImportDataTask { + + public static final String KEY_DATA_IMPORT_SRC_PKG = "data_import_src_pkg"; + public static final String KEY_DATA_IMPORT_SRC_AUTHORITY = "data_import_src_authority"; + + private static final String TAG = "ImportDataTask"; + private static final int MIN_ITEM_COUNT_FOR_SUCCESSFUL_MIGRATION = 6; + // Insert items progressively to avoid OOM exception when loading icons. + private static final int BATCH_INSERT_SIZE = 15; + + private final Context mContext; + + private final Uri mOtherFavoritesUri; + + private int mHotseatSize; + private int mMaxGridSizeX; + private int mMaxGridSizeY; + + private ImportDataTask(Context context, String sourceAuthority) { + mContext = context; + mOtherFavoritesUri = Uri.parse("content://" + sourceAuthority + "/" + Favorites.TABLE_NAME); + } + + public boolean importWorkspace() throws Exception { + FileLog.d(TAG, "Importing DB from " + mOtherFavoritesUri); + + mHotseatSize = mMaxGridSizeX = mMaxGridSizeY = 0; + importWorkspaceItems(); + GridSizeMigrationTask.markForMigration(mContext, mMaxGridSizeX, mMaxGridSizeY, mHotseatSize); + + // Create empty DB flag. + LauncherSettings.Settings.call(mContext.getContentResolver(), + LauncherSettings.Settings.METHOD_CLEAR_EMPTY_DB_FLAG); + return true; + } + + /** + * 1) Imports all the workspace entries from the source provider. + * 2) For home screen entries, maps the screen id based on {@param screenIdMap} + * 3) In the end fills any holes in hotseat with items from default hotseat layout. + */ + private void importWorkspaceItems() throws Exception { + String profileId = Long.toString(UserCache.INSTANCE.get(mContext) + .getSerialNumberForUser(Process.myUserHandle())); + + boolean createEmptyRowOnFirstScreen; + if (FeatureFlags.QSB_ON_FIRST_SCREEN) { + try (Cursor c = mContext.getContentResolver().query(mOtherFavoritesUri, null, + // get items on the first row of the first screen (min screen id) + "profileId = ? AND container = -100 AND cellY = 0 AND screen = " + + "(SELECT MIN(screen) FROM favorites WHERE container = -100)", + new String[]{profileId}, + null)) { + // First row of first screen is not empty + createEmptyRowOnFirstScreen = c.moveToNext(); + } + } else { + createEmptyRowOnFirstScreen = false; + } + + ArrayList<ContentProviderOperation> insertOperations = new ArrayList<>(BATCH_INSERT_SIZE); + + // Set of package names present in hotseat + final HashSet<String> hotseatTargetApps = new HashSet<>(); + int maxId = 0; + + // Number of imported items on workspace and hotseat + int totalItemsOnWorkspace = 0; + + try (Cursor c = mContext.getContentResolver() + .query(mOtherFavoritesUri, null, + // Only migrate the primary user + Favorites.PROFILE_ID + " = ?", new String[]{profileId}, + // Get the items sorted by container, so that the folders are loaded + // before the corresponding items. + Favorites.CONTAINER + " , " + Favorites.SCREEN)) { + + // various columns we expect to exist. + final int idIndex = c.getColumnIndexOrThrow(Favorites._ID); + final int intentIndex = c.getColumnIndexOrThrow(Favorites.INTENT); + final int titleIndex = c.getColumnIndexOrThrow(Favorites.TITLE); + final int containerIndex = c.getColumnIndexOrThrow(Favorites.CONTAINER); + final int itemTypeIndex = c.getColumnIndexOrThrow(Favorites.ITEM_TYPE); + final int widgetProviderIndex = c.getColumnIndexOrThrow(Favorites.APPWIDGET_PROVIDER); + final int screenIndex = c.getColumnIndexOrThrow(Favorites.SCREEN); + final int cellXIndex = c.getColumnIndexOrThrow(Favorites.CELLX); + final int cellYIndex = c.getColumnIndexOrThrow(Favorites.CELLY); + final int spanXIndex = c.getColumnIndexOrThrow(Favorites.SPANX); + final int spanYIndex = c.getColumnIndexOrThrow(Favorites.SPANY); + final int rankIndex = c.getColumnIndexOrThrow(Favorites.RANK); + final int iconIndex = c.getColumnIndexOrThrow(Favorites.ICON); + final int iconPackageIndex = c.getColumnIndexOrThrow(Favorites.ICON_PACKAGE); + final int iconResourceIndex = c.getColumnIndexOrThrow(Favorites.ICON_RESOURCE); + + SparseBooleanArray mValidFolders = new SparseBooleanArray(); + ContentValues values = new ContentValues(); + + Integer firstScreenId = null; + while (c.moveToNext()) { + values.clear(); + int id = c.getInt(idIndex); + maxId = Math.max(maxId, id); + int type = c.getInt(itemTypeIndex); + int container = c.getInt(containerIndex); + + int screen = c.getInt(screenIndex); + + int cellX = c.getInt(cellXIndex); + int cellY = c.getInt(cellYIndex); + int spanX = c.getInt(spanXIndex); + int spanY = c.getInt(spanYIndex); + + switch (container) { + case Favorites.CONTAINER_DESKTOP: { + if (screen < Workspace.FIRST_SCREEN_ID) { + FileLog.d(TAG, String.format( + "Skipping item %d, type %d not on a valid screen %d", + id, type, screen)); + continue; + } + if (firstScreenId == null) { + firstScreenId = screen; + } + // Reset the screen to 0-index value + if (createEmptyRowOnFirstScreen && firstScreenId.equals(screen)) { + // Shift items by 1. + cellY++; + // Change the screen id to first screen + screen = Workspace.FIRST_SCREEN_ID; + } + + mMaxGridSizeX = Math.max(mMaxGridSizeX, cellX + spanX); + mMaxGridSizeY = Math.max(mMaxGridSizeY, cellY + spanY); + break; + } + case Favorites.CONTAINER_HOTSEAT: { + mHotseatSize = Math.max(mHotseatSize, screen + 1); + break; + } + default: + if (!mValidFolders.get(container)) { + FileLog.d(TAG, String.format("Skipping item %d, type %d not in a valid folder %d", id, type, container)); + continue; + } + } + + Intent intent = null; + switch (type) { + case Favorites.ITEM_TYPE_FOLDER: { + mValidFolders.put(id, true); + // Use a empty intent to indicate a folder. + intent = new Intent(); + break; + } + case Favorites.ITEM_TYPE_APPWIDGET: { + values.put(Favorites.RESTORED, + LauncherAppWidgetInfo.FLAG_ID_NOT_VALID | + LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY | + LauncherAppWidgetInfo.FLAG_UI_NOT_READY); + values.put(Favorites.APPWIDGET_PROVIDER, c.getString(widgetProviderIndex)); + break; + } + case Favorites.ITEM_TYPE_SHORTCUT: + case Favorites.ITEM_TYPE_APPLICATION: { + intent = Intent.parseUri(c.getString(intentIndex), 0); + if (PackageManagerHelper.isLauncherAppTarget(intent)) { + type = Favorites.ITEM_TYPE_APPLICATION; + } else { + values.put(Favorites.ICON_PACKAGE, c.getString(iconPackageIndex)); + values.put(Favorites.ICON_RESOURCE, c.getString(iconResourceIndex)); + } + values.put(Favorites.ICON, c.getBlob(iconIndex)); + values.put(Favorites.INTENT, intent.toUri(0)); + values.put(Favorites.RANK, c.getInt(rankIndex)); + + values.put(Favorites.RESTORED, 1); + break; + } + default: + FileLog.d(TAG, String.format("Skipping item %d, not a valid type %d", id, type)); + continue; + } + + if (container == Favorites.CONTAINER_HOTSEAT) { + if (intent == null) { + FileLog.d(TAG, String.format("Skipping item %d, null intent on hotseat", id)); + continue; + } + if (intent.getComponent() != null) { + intent.setPackage(intent.getComponent().getPackageName()); + } + hotseatTargetApps.add(getPackage(intent)); + } + + values.put(Favorites._ID, id); + values.put(Favorites.ITEM_TYPE, type); + values.put(Favorites.CONTAINER, container); + values.put(Favorites.SCREEN, screen); + values.put(Favorites.CELLX, cellX); + values.put(Favorites.CELLY, cellY); + values.put(Favorites.SPANX, spanX); + values.put(Favorites.SPANY, spanY); + values.put(Favorites.TITLE, c.getString(titleIndex)); + insertOperations.add(ContentProviderOperation + .newInsert(Favorites.CONTENT_URI).withValues(values).build()); + if (container < 0) { + totalItemsOnWorkspace++; + } + + if (insertOperations.size() >= BATCH_INSERT_SIZE) { + mContext.getContentResolver().applyBatch(LauncherProvider.AUTHORITY, + insertOperations); + insertOperations.clear(); + } + } + } + FileLog.d(TAG, totalItemsOnWorkspace + " items imported from external source"); + if (totalItemsOnWorkspace < MIN_ITEM_COUNT_FOR_SUCCESSFUL_MIGRATION) { + throw new Exception("Insufficient data"); + } + if (!insertOperations.isEmpty()) { + mContext.getContentResolver().applyBatch(LauncherProvider.AUTHORITY, + insertOperations); + insertOperations.clear(); + } + + IntSparseArrayMap<Object> hotseatItems = GridSizeMigrationTask + .removeBrokenHotseatItems(mContext); + int myHotseatCount = LauncherAppState.getIDP(mContext).numDatabaseHotseatIcons; + if (hotseatItems.size() < myHotseatCount) { + // Insufficient hotseat items. Add a few more. + HotseatParserCallback parserCallback = new HotseatParserCallback( + hotseatTargetApps, hotseatItems, insertOperations, maxId + 1, myHotseatCount); + new HotseatLayoutParser(mContext, + parserCallback).loadLayout(null, new IntArray()); + mHotseatSize = hotseatItems.keyAt(hotseatItems.size() - 1) + 1; + + if (!insertOperations.isEmpty()) { + mContext.getContentResolver().applyBatch(LauncherProvider.AUTHORITY, + insertOperations); + } + } + } + + private static String getPackage(Intent intent) { + return intent.getComponent() != null ? intent.getComponent().getPackageName() + : intent.getPackage(); + } + + /** + * Performs data import if possible. + * @return true on successful data import, false if it was not available + * @throws Exception if the import failed + */ + public static boolean performImportIfPossible(Context context) throws Exception { + SharedPreferences devicePrefs = getDevicePrefs(context); + String sourcePackage = devicePrefs.getString(KEY_DATA_IMPORT_SRC_PKG, ""); + String sourceAuthority = devicePrefs.getString(KEY_DATA_IMPORT_SRC_AUTHORITY, ""); + + if (TextUtils.isEmpty(sourcePackage) || TextUtils.isEmpty(sourceAuthority)) { + return false; + } + + // Synchronously clear the migration flags. This ensures that we do not try migration + // again and thus prevents potential crash loops due to migration failure. + devicePrefs.edit().remove(KEY_DATA_IMPORT_SRC_PKG).remove(KEY_DATA_IMPORT_SRC_AUTHORITY).commit(); + + if (!Settings.call(context.getContentResolver(), Settings.METHOD_WAS_EMPTY_DB_CREATED) + .getBoolean(Settings.EXTRA_VALUE, false)) { + // Only migration if a new DB was created. + return false; + } + + for (ProviderInfo info : context.getPackageManager().queryContentProviders( + null, context.getApplicationInfo().uid, 0)) { + + if (sourcePackage.equals(info.packageName)) { + if ((info.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) { + // Only migrate if the source launcher is also on system image. + return false; + } + + // Wait until we found a provider with matching authority. + if (sourceAuthority.equals(info.authority)) { + if (TextUtils.isEmpty(info.readPermission) || + context.checkPermission(info.readPermission, Process.myPid(), + Process.myUid()) == PackageManager.PERMISSION_GRANTED) { + // All checks passed, run the import task. + return new ImportDataTask(context, sourceAuthority).importWorkspace(); + } + } + } + } + return false; + } + + /** + * Extension of {@link DefaultLayoutParser} which only allows icons and shortcuts. + */ + private static class HotseatLayoutParser extends DefaultLayoutParser { + public HotseatLayoutParser(Context context, LayoutParserCallback callback) { + super(context, null, callback, context.getResources(), + LauncherAppState.getIDP(context).defaultLayoutId); + } + + @Override + protected ArrayMap<String, TagParser> getLayoutElementsMap() { + // Only allow shortcut parsers + ArrayMap<String, TagParser> parsers = new ArrayMap<>(); + parsers.put(TAG_FAVORITE, new AppShortcutWithUriParser()); + parsers.put(TAG_SHORTCUT, new UriShortcutParser(mSourceRes)); + parsers.put(TAG_RESOLVE, new ResolveParser()); + return parsers; + } + } + + /** + * {@link LayoutParserCallback} which adds items in empty hotseat spots. + */ + private static class HotseatParserCallback implements LayoutParserCallback { + private final HashSet<String> mExistingApps; + private final IntSparseArrayMap<Object> mExistingItems; + private final ArrayList<ContentProviderOperation> mOutOps; + private final int mRequiredSize; + private int mStartItemId; + + HotseatParserCallback( + HashSet<String> existingApps, IntSparseArrayMap<Object> existingItems, + ArrayList<ContentProviderOperation> outOps, int startItemId, int requiredSize) { + mExistingApps = existingApps; + mExistingItems = existingItems; + mOutOps = outOps; + mRequiredSize = requiredSize; + mStartItemId = startItemId; + } + + @Override + public int generateNewItemId() { + return mStartItemId++; + } + + @Override + public int insertAndCheck(SQLiteDatabase db, ContentValues values) { + if (mExistingItems.size() >= mRequiredSize) { + // No need to add more items. + return 0; + } + if (!Integer.valueOf(Favorites.CONTAINER_HOTSEAT) + .equals(values.getAsInteger(Favorites.CONTAINER))) { + // Ignore items which are not for hotseat. + return 0; + } + + Intent intent; + try { + intent = Intent.parseUri(values.getAsString(Favorites.INTENT), 0); + } catch (URISyntaxException e) { + return 0; + } + String pkg = getPackage(intent); + if (pkg == null || mExistingApps.contains(pkg)) { + // The item does not target an app or is already in hotseat. + return 0; + } + mExistingApps.add(pkg); + + // find next vacant spot. + int screen = 0; + while (mExistingItems.get(screen) != null) { + screen++; + } + mExistingItems.put(screen, intent); + values.put(Favorites.SCREEN, screen); + mOutOps.add(ContentProviderOperation.newInsert(Favorites.CONTENT_URI).withValues(values).build()); + return 0; + } + } +} |