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