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