| /* |
| * 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.LauncherAppWidgetInfo; |
| 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.compat.UserManagerCompat; |
| import com.android.launcher3.config.FeatureFlags; |
| import com.android.launcher3.logging.FileLog; |
| import com.android.launcher3.model.GridSizeMigrationTask; |
| 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(UserManagerCompat.getInstance(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).numHotseatIcons; |
| 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; |
| } |
| } |
| } |