Files
Lawnchair/src/com/android/launcher3/provider/ImportDataTask.java
T
Sunny Goyal d70ef24233 Fixing the hotseat import logic
The import logic following the behavior: Improt everything and force
run GridMigrationTask to automatically remove broken icons.
This logic would fail for hotseat as the replacement happens before
the GridMigrationTask, which will not replace the broken targets
appropriately

The cl changes some logic only for hotseat import
> After import remove any broken icons/empty folders
> When adding default icons, only add as much icons as required. Since
GridMigrationTask uses weights, it sometimes removes imported icon, if
the hotseat size becomes too large.

Bug: 30909630
Change-Id: I6ca1f25dac81649794d81aaf82c3c38d1c918d91
2016-08-24 11:31:13 -07:00

463 lines
20 KiB
Java

/*
* 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<>();
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());
}
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");
}
if (!insertOperations.isEmpty()) {
mContext.getContentResolver().applyBatch(ProviderConfig.AUTHORITY,
insertOperations);
insertOperations.clear();
}
LongArrayMap<Object> hotseatItems = GridSizeMigrationTask.removeBrokenHotseatItems(mContext);
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, myHotseatCount);
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<Object> mExistingItems;
private final ArrayList<ContentProviderOperation> mOutOps;
private final int mRequiredSize;
private int mStartItemId;
HotseatParserCallback(
HashSet<String> existingApps, LongArrayMap<Object> existingItems,
ArrayList<ContentProviderOperation> outOps, int startItemId, int requiredSize) {
mExisitingApps = existingApps;
mExistingItems = existingItems;
mOutOps = outOps;
mRequiredSize = requiredSize;
mStartItemId = startItemId;
}
@Override
public long generateNewItemId() {
return mStartItemId++;
}
@Override
public long insertAndCheck(SQLiteDatabase db, ContentValues values) {
if (mExistingItems.size() >= mRequiredSize) {
// No need to add more items.
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 || 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;
}
}
}