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