diff options
author | Sunny Goyal <sunnygoyal@google.com> | 2016-07-08 08:32:44 -0700 |
---|---|---|
committer | Sunny Goyal <sunnygoyal@google.com> | 2016-07-09 16:19:26 -0700 |
commit | a5c8a9eb666da16bc4c9ea4412868e22ace8d1f0 (patch) | |
tree | 1e612ea9a361beae540c943d18af1ad233fb0f5d /src/com/android/launcher3/provider/ImportDataTask.java | |
parent | f03bd4f5470eed9808a0e6f345de94f4e578ae85 (diff) | |
download | android_packages_apps_Trebuchet-a5c8a9eb666da16bc4c9ea4412868e22ace8d1f0.tar.gz android_packages_apps_Trebuchet-a5c8a9eb666da16bc4c9ea4412868e22ace8d1f0.tar.bz2 android_packages_apps_Trebuchet-a5c8a9eb666da16bc4c9ea4412868e22ace8d1f0.zip |
Adding logic to pull in workspace data from another Launcher3 based
provider. This allows OEMs to keep the user's homescreen intact while
changing the default home app package.
Bug: 28536314
Change-Id: Ibebfd7dd33aa2cbd9ca28d2d611dd0a4a5971444
Diffstat (limited to 'src/com/android/launcher3/provider/ImportDataTask.java')
-rw-r--r-- | src/com/android/launcher3/provider/ImportDataTask.java | 451 |
1 files changed, 451 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 000000000..233c3edf1 --- /dev/null +++ b/src/com/android/launcher3/provider/ImportDataTask.java @@ -0,0 +1,451 @@ +/* + * 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 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.LongSparseArray; +import android.util.SparseBooleanArray; + +import com.android.launcher3.AutoInstallsLayout.LayoutParserCallback; +import com.android.launcher3.DefaultLayoutParser; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.LauncherAppWidgetInfo; +import com.android.launcher3.LauncherFiles; +import com.android.launcher3.LauncherSettings; +import com.android.launcher3.LauncherSettings.Favorites; +import com.android.launcher3.LauncherSettings.Settings; +import com.android.launcher3.LauncherSettings.WorkspaceScreens; +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.Workspace; +import com.android.launcher3.compat.UserHandleCompat; +import com.android.launcher3.compat.UserManagerCompat; +import com.android.launcher3.config.FeatureFlags; +import com.android.launcher3.config.ProviderConfig; +import com.android.launcher3.logging.FileLog; +import com.android.launcher3.model.GridSizeMigrationTask; +import com.android.launcher3.util.LongArrayMap; + +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.HashMap; +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 mOtherScreensUri; + private final Uri mOtherFavoritesUri; + + private int mHotseatSize; + private int mMaxGridSizeX; + private int mMaxGridSizeY; + + private ImportDataTask(Context context, String sourceAuthority) { + mContext = context; + mOtherScreensUri = Uri.parse("content://" + + sourceAuthority + "/" + WorkspaceScreens.TABLE_NAME); + mOtherFavoritesUri = Uri.parse("content://" + sourceAuthority + "/" + Favorites.TABLE_NAME); + } + + public boolean importWorkspace() throws Exception { + ArrayList<Long> allScreens = LauncherDbUtils.getScreenIdsFromCursor( + mContext.getContentResolver().query(mOtherScreensUri, null, null, null, + LauncherSettings.WorkspaceScreens.SCREEN_RANK)); + + // During import we reset the screen IDs to 0-indexed values. + if (allScreens.isEmpty()) { + // No thing to migrate + return false; + } + + mHotseatSize = mMaxGridSizeX = mMaxGridSizeY = 0; + + // Build screen update + ArrayList<ContentProviderOperation> screenOps = new ArrayList<>(); + int count = allScreens.size(); + LongSparseArray<Long> screenIdMap = new LongSparseArray<>(count); + for (int i = 0; i < count; i++) { + ContentValues v = new ContentValues(); + v.put(LauncherSettings.WorkspaceScreens._ID, i); + v.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, i); + screenIdMap.put(allScreens.get(i), (long) i); + screenOps.add(ContentProviderOperation.newInsert( + LauncherSettings.WorkspaceScreens.CONTENT_URI).withValues(v).build()); + } + mContext.getContentResolver().applyBatch(ProviderConfig.AUTHORITY, screenOps); + importWorkspaceItems(allScreens.get(0), screenIdMap); + + 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( + long firsetScreenId, LongSparseArray<Long> screenIdMap) throws Exception { + String profileId = Long.toString(UserManagerCompat.getInstance(mContext) + .getSerialNumberForUser(UserHandleCompat.myUserHandle())); + + boolean createEmptyRowOnFirstScreen = false; + if (FeatureFlags.QSB_ON_FIRST_SCREEN) { + try (Cursor c = mContext.getContentResolver().query(mOtherFavoritesUri, null, + // get items on the first row of the first screen + "profileId = ? AND container = -100 AND screen = ? AND cellY = 0", + new String[]{profileId, Long.toString(firsetScreenId)}, + null)) { + // First row of first screen is not empty + createEmptyRowOnFirstScreen = c.moveToNext(); + } + } + + ArrayList<ContentProviderOperation> insertOperations = new ArrayList<>(BATCH_INSERT_SIZE); + + // Set of package names present in hotseat + final HashSet<String> hotseatTargetApps = new HashSet<>(); + final LongArrayMap<Intent> hotseatItems = new LongArrayMap<>(); + 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)) { + + // 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(); + + 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); + + long screen = c.getLong(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: { + Long newScreenId = screenIdMap.get(screen); + if (newScreenId == null) { + FileLog.d(TAG, String.format("Skipping item %d, type %d not on a valid screen %d", id, type, screen)); + continue; + } + // Reset the screen to 0-index value + screen = newScreenId; + if (createEmptyRowOnFirstScreen && screen == Workspace.FIRST_SCREEN_ID) { + // Shift items by 1. + cellY++; + } + + mMaxGridSizeX = Math.max(mMaxGridSizeX, cellX + spanX); + mMaxGridSizeY = Math.max(mMaxGridSizeY, cellY + spanY); + break; + } + case Favorites.CONTAINER_HOTSEAT: { + mHotseatSize = Math.max(mHotseatSize, (int) 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 (Utilities.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()); + } + hotseatItems.put(screen, intent); + 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(ProviderConfig.AUTHORITY, + insertOperations); + insertOperations.clear(); + } + } + } + if (totalItemsOnWorkspace < MIN_ITEM_COUNT_FOR_SUCCESSFUL_MIGRATION) { + throw new Exception("Insufficient data"); + } + + int myHotseatCount = LauncherAppState.getInstance().getInvariantDeviceProfile().numHotseatIcons; + if (!FeatureFlags.NO_ALL_APPS_ICON) { + myHotseatCount--; + } + if (hotseatItems.size() < myHotseatCount) { + // Insufficient hotseat items. Add a few more. + HotseatParserCallback parserCallback = new HotseatParserCallback( + hotseatTargetApps, hotseatItems, insertOperations, maxId + 1); + new HotseatLayoutParser(mContext, + parserCallback).loadLayout(null, new ArrayList<Long>()); + mHotseatSize = (int) hotseatItems.keyAt(hotseatItems.size() - 1) + 1; + } + if (!insertOperations.isEmpty()) { + mContext.getContentResolver().applyBatch(ProviderConfig.AUTHORITY, + insertOperations); + } + } + + private static final 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; + } + + private static SharedPreferences getDevicePrefs(Context c) { + return c.getSharedPreferences(LauncherFiles.DEVICE_PREFERENCES_KEY, Context.MODE_PRIVATE); + } + + private static final int getMyHotseatLayoutId() { + return LauncherAppState.getInstance().getInvariantDeviceProfile().numHotseatIcons <= 5 + ? R.xml.dw_phone_hotseat + : R.xml.dw_tablet_hotseat; + } + + /** + * 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(), getMyHotseatLayoutId()); + } + + @Override + protected HashMap<String, TagParser> getLayoutElementsMap() { + // Only allow shortcut parsers + HashMap<String, TagParser> parsers = new HashMap<String, TagParser>(); + 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> mExisitingApps; + private final LongArrayMap<Intent> mExistingItems; + private final ArrayList<ContentProviderOperation> mOutOps; + private int mStartItemId; + + HotseatParserCallback( + HashSet<String> existingApps, LongArrayMap<Intent> existingItems, + ArrayList<ContentProviderOperation> outOps, int startItemId) { + mExisitingApps = existingApps; + mExistingItems = existingItems; + mOutOps = outOps; + mStartItemId = startItemId; + } + + @Override + public long generateNewItemId() { + return mStartItemId++; + } + + @Override + public long insertAndCheck(SQLiteDatabase db, ContentValues values) { + Intent intent; + try { + intent = Intent.parseUri(values.getAsString(Favorites.INTENT), 0); + } catch (URISyntaxException e) { + return 0; + } + String pkg = getPackage(intent); + if (pkg == null || mExisitingApps.contains(pkg)) { + // The item does not target an app or is already in hotseat. + return 0; + } + mExisitingApps.add(pkg); + + // find next vacant spot. + long 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; + } + } +} |