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