1 /* 2 * Copyright (C) 2008 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; 18 19 import android.annotation.TargetApi; 20 import android.appwidget.AppWidgetHost; 21 import android.appwidget.AppWidgetManager; 22 import android.content.ComponentName; 23 import android.content.ContentProvider; 24 import android.content.ContentProviderOperation; 25 import android.content.ContentProviderResult; 26 import android.content.ContentResolver; 27 import android.content.ContentUris; 28 import android.content.ContentValues; 29 import android.content.Context; 30 import android.content.Intent; 31 import android.content.OperationApplicationException; 32 import android.content.SharedPreferences; 33 import android.content.pm.PackageManager.NameNotFoundException; 34 import android.content.res.Resources; 35 import android.database.Cursor; 36 import android.database.SQLException; 37 import android.database.sqlite.SQLiteDatabase; 38 import android.database.sqlite.SQLiteOpenHelper; 39 import android.database.sqlite.SQLiteQueryBuilder; 40 import android.database.sqlite.SQLiteStatement; 41 import android.net.Uri; 42 import android.os.Binder; 43 import android.os.Build; 44 import android.os.Bundle; 45 import android.os.Process; 46 import android.os.StrictMode; 47 import android.os.UserManager; 48 import android.text.TextUtils; 49 import android.util.Log; 50 import android.util.SparseArray; 51 52 import com.android.launcher3.AutoInstallsLayout.LayoutParserCallback; 53 import com.android.launcher3.LauncherSettings.Favorites; 54 import com.android.launcher3.compat.UserHandleCompat; 55 import com.android.launcher3.compat.UserManagerCompat; 56 import com.android.launcher3.config.ProviderConfig; 57 import com.android.launcher3.util.ManagedProfileHeuristic; 58 import com.android.launcher3.util.Thunk; 59 60 import java.net.URISyntaxException; 61 import java.util.ArrayList; 62 import java.util.Collections; 63 import java.util.HashSet; 64 import java.util.List; 65 66 public class LauncherProvider extends ContentProvider { 67 private static final String TAG = "LauncherProvider"; 68 private static final boolean LOGD = false; 69 70 private static final int DATABASE_VERSION = 26; 71 72 public static final String AUTHORITY = ProviderConfig.AUTHORITY; 73 74 static final String TABLE_FAVORITES = LauncherSettings.Favorites.TABLE_NAME; 75 static final String TABLE_WORKSPACE_SCREENS = LauncherSettings.WorkspaceScreens.TABLE_NAME; 76 static final String EMPTY_DATABASE_CREATED = "EMPTY_DATABASE_CREATED"; 77 78 private static final String RESTRICTION_PACKAGE_NAME = "workspace.configuration.package.name"; 79 80 @Thunk LauncherProviderChangeListener mListener; 81 protected DatabaseHelper mOpenHelper; 82 83 @Override onCreate()84 public boolean onCreate() { 85 final Context context = getContext(); 86 // The content provider exists for the entire duration of the launcher main process and 87 // is the first component to get created. Initializing application context here ensures 88 // that LauncherAppState always exists in the main process. 89 LauncherAppState.setApplicationContext(context.getApplicationContext()); 90 StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites(); 91 mOpenHelper = new DatabaseHelper(context); 92 StrictMode.setThreadPolicy(oldPolicy); 93 LauncherAppState.setLauncherProvider(this); 94 return true; 95 } 96 wasNewDbCreated()97 public boolean wasNewDbCreated() { 98 return mOpenHelper.wasNewDbCreated(); 99 } 100 setLauncherProviderChangeListener(LauncherProviderChangeListener listener)101 public void setLauncherProviderChangeListener(LauncherProviderChangeListener listener) { 102 mListener = listener; 103 mOpenHelper.mListener = mListener; 104 } 105 106 @Override getType(Uri uri)107 public String getType(Uri uri) { 108 SqlArguments args = new SqlArguments(uri, null, null); 109 if (TextUtils.isEmpty(args.where)) { 110 return "vnd.android.cursor.dir/" + args.table; 111 } else { 112 return "vnd.android.cursor.item/" + args.table; 113 } 114 } 115 116 /** 117 * Overridden in tests 118 */ createDbIfNotExists()119 protected synchronized void createDbIfNotExists() { 120 if (mOpenHelper == null) { 121 mOpenHelper = new DatabaseHelper(getContext()); 122 } 123 } 124 125 @Override query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)126 public Cursor query(Uri uri, String[] projection, String selection, 127 String[] selectionArgs, String sortOrder) { 128 129 SqlArguments args = new SqlArguments(uri, selection, selectionArgs); 130 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 131 qb.setTables(args.table); 132 133 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 134 Cursor result = qb.query(db, projection, args.where, args.args, null, null, sortOrder); 135 result.setNotificationUri(getContext().getContentResolver(), uri); 136 137 return result; 138 } 139 dbInsertAndCheck(DatabaseHelper helper, SQLiteDatabase db, String table, String nullColumnHack, ContentValues values)140 @Thunk static long dbInsertAndCheck(DatabaseHelper helper, 141 SQLiteDatabase db, String table, String nullColumnHack, ContentValues values) { 142 if (values == null) { 143 throw new RuntimeException("Error: attempting to insert null values"); 144 } 145 if (!values.containsKey(LauncherSettings.ChangeLogColumns._ID)) { 146 throw new RuntimeException("Error: attempting to add item without specifying an id"); 147 } 148 helper.checkId(table, values); 149 return db.insert(table, nullColumnHack, values); 150 } 151 reloadLauncherIfExternal()152 private void reloadLauncherIfExternal() { 153 if (Utilities.ATLEAST_MARSHMALLOW && Binder.getCallingPid() != Process.myPid()) { 154 LauncherAppState app = LauncherAppState.getInstanceNoCreate(); 155 if (app != null) { 156 app.reloadWorkspace(); 157 } 158 } 159 } 160 161 @Override insert(Uri uri, ContentValues initialValues)162 public Uri insert(Uri uri, ContentValues initialValues) { 163 SqlArguments args = new SqlArguments(uri); 164 165 // In very limited cases, we support system|signature permission apps to modify the db. 166 if (Binder.getCallingPid() != Process.myPid()) { 167 if (!mOpenHelper.initializeExternalAdd(initialValues)) { 168 return null; 169 } 170 } 171 172 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 173 addModifiedTime(initialValues); 174 final long rowId = dbInsertAndCheck(mOpenHelper, db, args.table, null, initialValues); 175 if (rowId < 0) return null; 176 177 uri = ContentUris.withAppendedId(uri, rowId); 178 notifyListeners(); 179 180 if (Utilities.ATLEAST_MARSHMALLOW) { 181 reloadLauncherIfExternal(); 182 } else { 183 // Deprecated behavior to support legacy devices which rely on provider callbacks. 184 LauncherAppState app = LauncherAppState.getInstanceNoCreate(); 185 if (app != null && "true".equals(uri.getQueryParameter("isExternalAdd"))) { 186 app.reloadWorkspace(); 187 } 188 189 String notify = uri.getQueryParameter("notify"); 190 if (notify == null || "true".equals(notify)) { 191 getContext().getContentResolver().notifyChange(uri, null); 192 } 193 } 194 return uri; 195 } 196 197 198 @Override bulkInsert(Uri uri, ContentValues[] values)199 public int bulkInsert(Uri uri, ContentValues[] values) { 200 SqlArguments args = new SqlArguments(uri); 201 202 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 203 db.beginTransaction(); 204 try { 205 int numValues = values.length; 206 for (int i = 0; i < numValues; i++) { 207 addModifiedTime(values[i]); 208 if (dbInsertAndCheck(mOpenHelper, db, args.table, null, values[i]) < 0) { 209 return 0; 210 } 211 } 212 db.setTransactionSuccessful(); 213 } finally { 214 db.endTransaction(); 215 } 216 217 notifyListeners(); 218 reloadLauncherIfExternal(); 219 return values.length; 220 } 221 222 @Override applyBatch(ArrayList<ContentProviderOperation> operations)223 public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) 224 throws OperationApplicationException { 225 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 226 db.beginTransaction(); 227 try { 228 ContentProviderResult[] result = super.applyBatch(operations); 229 db.setTransactionSuccessful(); 230 reloadLauncherIfExternal(); 231 return result; 232 } finally { 233 db.endTransaction(); 234 } 235 } 236 237 @Override delete(Uri uri, String selection, String[] selectionArgs)238 public int delete(Uri uri, String selection, String[] selectionArgs) { 239 SqlArguments args = new SqlArguments(uri, selection, selectionArgs); 240 241 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 242 int count = db.delete(args.table, args.where, args.args); 243 if (count > 0) notifyListeners(); 244 245 reloadLauncherIfExternal(); 246 return count; 247 } 248 249 @Override update(Uri uri, ContentValues values, String selection, String[] selectionArgs)250 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 251 SqlArguments args = new SqlArguments(uri, selection, selectionArgs); 252 253 addModifiedTime(values); 254 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 255 int count = db.update(args.table, values, args.where, args.args); 256 if (count > 0) notifyListeners(); 257 258 reloadLauncherIfExternal(); 259 return count; 260 } 261 262 @Override call(String method, String arg, Bundle extras)263 public Bundle call(String method, String arg, Bundle extras) { 264 if (Binder.getCallingUid() != Process.myUid()) { 265 return null; 266 } 267 268 switch (method) { 269 case LauncherSettings.Settings.METHOD_GET_BOOLEAN: { 270 Bundle result = new Bundle(); 271 if (Utilities.ALLOW_ROTATION_PREFERENCE_KEY.equals(arg)) { 272 result.putBoolean(LauncherSettings.Settings.EXTRA_VALUE, 273 Utilities.isAllowRotationPrefEnabled(getContext())); 274 } else { 275 result.putBoolean(LauncherSettings.Settings.EXTRA_VALUE, 276 Utilities.getPrefs(getContext()).getBoolean(arg, extras.getBoolean( 277 LauncherSettings.Settings.EXTRA_DEFAULT_VALUE))); 278 } 279 return result; 280 } 281 case LauncherSettings.Settings.METHOD_SET_BOOLEAN: { 282 boolean value = extras.getBoolean(LauncherSettings.Settings.EXTRA_VALUE); 283 Utilities.getPrefs(getContext()).edit().putBoolean(arg, value).apply(); 284 if (mListener != null) { 285 mListener.onSettingsChanged(arg, value); 286 } 287 if (extras.getBoolean(LauncherSettings.Settings.NOTIFY_BACKUP)) { 288 LauncherBackupAgentHelper.dataChanged(getContext()); 289 } 290 Bundle result = new Bundle(); 291 result.putBoolean(LauncherSettings.Settings.EXTRA_VALUE, value); 292 return result; 293 } 294 } 295 return null; 296 } 297 298 /** 299 * Deletes any empty folder from the DB. 300 * @return Ids of deleted folders. 301 */ deleteEmptyFolders()302 public List<Long> deleteEmptyFolders() { 303 ArrayList<Long> folderIds = new ArrayList<Long>(); 304 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 305 db.beginTransaction(); 306 try { 307 // Select folders whose id do not match any container value. 308 String selection = LauncherSettings.Favorites.ITEM_TYPE + " = " 309 + LauncherSettings.Favorites.ITEM_TYPE_FOLDER + " AND " 310 + LauncherSettings.Favorites._ID + " NOT IN (SELECT " + 311 LauncherSettings.Favorites.CONTAINER + " FROM " 312 + TABLE_FAVORITES + ")"; 313 Cursor c = db.query(TABLE_FAVORITES, 314 new String[] {LauncherSettings.Favorites._ID}, 315 selection, null, null, null, null); 316 while (c.moveToNext()) { 317 folderIds.add(c.getLong(0)); 318 } 319 c.close(); 320 if (folderIds.size() > 0) { 321 db.delete(TABLE_FAVORITES, Utilities.createDbSelectionQuery( 322 LauncherSettings.Favorites._ID, folderIds), null); 323 } 324 db.setTransactionSuccessful(); 325 } catch (SQLException ex) { 326 Log.e(TAG, ex.getMessage(), ex); 327 folderIds.clear(); 328 } finally { 329 db.endTransaction(); 330 } 331 return folderIds; 332 } 333 334 /** 335 * Overridden in tests 336 */ notifyListeners()337 protected void notifyListeners() { 338 // always notify the backup agent 339 LauncherBackupAgentHelper.dataChanged(getContext()); 340 if (mListener != null) { 341 mListener.onLauncherProviderChange(); 342 } 343 } 344 addModifiedTime(ContentValues values)345 @Thunk static void addModifiedTime(ContentValues values) { 346 values.put(LauncherSettings.ChangeLogColumns.MODIFIED, System.currentTimeMillis()); 347 } 348 generateNewItemId()349 public long generateNewItemId() { 350 return mOpenHelper.generateNewItemId(); 351 } 352 generateNewScreenId()353 public long generateNewScreenId() { 354 return mOpenHelper.generateNewScreenId(); 355 } 356 357 /** 358 * Clears all the data for a fresh start. 359 */ createEmptyDB()360 synchronized public void createEmptyDB() { 361 mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase()); 362 } 363 clearFlagEmptyDbCreated()364 public void clearFlagEmptyDbCreated() { 365 Utilities.getPrefs(getContext()).edit().remove(EMPTY_DATABASE_CREATED).commit(); 366 } 367 368 /** 369 * Loads the default workspace based on the following priority scheme: 370 * 1) From the app restrictions 371 * 2) From a package provided by play store 372 * 3) From a partner configuration APK, already in the system image 373 * 4) The default configuration for the particular device 374 */ loadDefaultFavoritesIfNecessary()375 synchronized public void loadDefaultFavoritesIfNecessary() { 376 SharedPreferences sp = Utilities.getPrefs(getContext()); 377 378 if (sp.getBoolean(EMPTY_DATABASE_CREATED, false)) { 379 Log.d(TAG, "loading default workspace"); 380 381 AutoInstallsLayout loader = createWorkspaceLoaderFromAppRestriction(); 382 if (loader == null) { 383 loader = AutoInstallsLayout.get(getContext(), 384 mOpenHelper.mAppWidgetHost, mOpenHelper); 385 } 386 if (loader == null) { 387 final Partner partner = Partner.get(getContext().getPackageManager()); 388 if (partner != null && partner.hasDefaultLayout()) { 389 final Resources partnerRes = partner.getResources(); 390 int workspaceResId = partnerRes.getIdentifier(Partner.RES_DEFAULT_LAYOUT, 391 "xml", partner.getPackageName()); 392 if (workspaceResId != 0) { 393 loader = new DefaultLayoutParser(getContext(), mOpenHelper.mAppWidgetHost, 394 mOpenHelper, partnerRes, workspaceResId); 395 } 396 } 397 } 398 399 final boolean usingExternallyProvidedLayout = loader != null; 400 if (loader == null) { 401 loader = getDefaultLayoutParser(); 402 } 403 404 // There might be some partially restored DB items, due to buggy restore logic in 405 // previous versions of launcher. 406 createEmptyDB(); 407 // Populate favorites table with initial favorites 408 if ((mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), loader) <= 0) 409 && usingExternallyProvidedLayout) { 410 // Unable to load external layout. Cleanup and load the internal layout. 411 createEmptyDB(); 412 mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), 413 getDefaultLayoutParser()); 414 } 415 clearFlagEmptyDbCreated(); 416 } 417 } 418 419 /** 420 * Creates workspace loader from an XML resource listed in the app restrictions. 421 * 422 * @return the loader if the restrictions are set and the resource exists; null otherwise. 423 */ 424 @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) createWorkspaceLoaderFromAppRestriction()425 private AutoInstallsLayout createWorkspaceLoaderFromAppRestriction() { 426 // UserManager.getApplicationRestrictions() requires minSdkVersion >= 18 427 if (!Utilities.ATLEAST_JB_MR2) { 428 return null; 429 } 430 431 Context ctx = getContext(); 432 UserManager um = (UserManager) ctx.getSystemService(Context.USER_SERVICE); 433 Bundle bundle = um.getApplicationRestrictions(ctx.getPackageName()); 434 if (bundle == null) { 435 return null; 436 } 437 438 String packageName = bundle.getString(RESTRICTION_PACKAGE_NAME); 439 if (packageName != null) { 440 try { 441 Resources targetResources = ctx.getPackageManager() 442 .getResourcesForApplication(packageName); 443 return AutoInstallsLayout.get(ctx, packageName, targetResources, 444 mOpenHelper.mAppWidgetHost, mOpenHelper); 445 } catch (NameNotFoundException e) { 446 Log.e(TAG, "Target package for restricted profile not found", e); 447 return null; 448 } 449 } 450 return null; 451 } 452 getDefaultLayoutParser()453 private DefaultLayoutParser getDefaultLayoutParser() { 454 int defaultLayout = LauncherAppState.getInstance() 455 .getInvariantDeviceProfile().defaultLayoutId; 456 return new DefaultLayoutParser(getContext(), mOpenHelper.mAppWidgetHost, 457 mOpenHelper, getContext().getResources(), defaultLayout); 458 } 459 migrateLauncher2Shortcuts()460 public void migrateLauncher2Shortcuts() { 461 mOpenHelper.migrateLauncher2Shortcuts(mOpenHelper.getWritableDatabase(), 462 Uri.parse(getContext().getString(R.string.old_launcher_provider_uri))); 463 } 464 updateFolderItemsRank()465 public void updateFolderItemsRank() { 466 mOpenHelper.updateFolderItemsRank(mOpenHelper.getWritableDatabase(), false); 467 } 468 convertShortcutsToLauncherActivities()469 public void convertShortcutsToLauncherActivities() { 470 mOpenHelper.convertShortcutsToLauncherActivities(mOpenHelper.getWritableDatabase()); 471 } 472 473 deleteDatabase()474 public void deleteDatabase() { 475 // Are you sure? (y/n) 476 mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase()); 477 } 478 479 /** 480 * The class is subclassed in tests to create an in-memory db. 481 */ 482 protected static class DatabaseHelper extends SQLiteOpenHelper implements LayoutParserCallback { 483 private final Context mContext; 484 @Thunk final AppWidgetHost mAppWidgetHost; 485 private long mMaxItemId = -1; 486 private long mMaxScreenId = -1; 487 488 private boolean mNewDbCreated = false; 489 490 @Thunk LauncherProviderChangeListener mListener; 491 DatabaseHelper(Context context)492 DatabaseHelper(Context context) { 493 super(context, LauncherFiles.LAUNCHER_DB, null, DATABASE_VERSION); 494 mContext = context; 495 mAppWidgetHost = new AppWidgetHost(context, Launcher.APPWIDGET_HOST_ID); 496 497 // Table creation sometimes fails silently, which leads to a crash loop. 498 // This way, we will try to create a table every time after crash, so the device 499 // would eventually be able to recover. 500 if (!tableExists(TABLE_FAVORITES) || !tableExists(TABLE_WORKSPACE_SCREENS)) { 501 Log.e(TAG, "Tables are missing after onCreate has been called. Trying to recreate"); 502 // This operation is a no-op if the table already exists. 503 addFavoritesTable(getWritableDatabase(), true); 504 addWorkspacesTable(getWritableDatabase(), true); 505 } 506 507 // In the case where neither onCreate nor onUpgrade gets called, we read the maxId from 508 // the DB here 509 if (mMaxItemId == -1) { 510 mMaxItemId = initializeMaxItemId(getWritableDatabase()); 511 } 512 if (mMaxScreenId == -1) { 513 mMaxScreenId = initializeMaxScreenId(getWritableDatabase()); 514 } 515 } 516 517 /** 518 * Constructor used only in tests. 519 */ DatabaseHelper(Context context, String tableName)520 public DatabaseHelper(Context context, String tableName) { 521 super(context, tableName, null, DATABASE_VERSION); 522 mContext = context; 523 524 mAppWidgetHost = null; 525 mMaxItemId = initializeMaxItemId(getWritableDatabase()); 526 mMaxScreenId = initializeMaxScreenId(getWritableDatabase()); 527 } 528 tableExists(String tableName)529 private boolean tableExists(String tableName) { 530 Cursor c = getReadableDatabase().query( 531 true, "sqlite_master", new String[] {"tbl_name"}, 532 "tbl_name = ?", new String[] {tableName}, 533 null, null, null, null, null); 534 try { 535 return c.getCount() > 0; 536 } finally { 537 c.close(); 538 } 539 } 540 wasNewDbCreated()541 public boolean wasNewDbCreated() { 542 return mNewDbCreated; 543 } 544 545 @Override onCreate(SQLiteDatabase db)546 public void onCreate(SQLiteDatabase db) { 547 if (LOGD) Log.d(TAG, "creating new launcher database"); 548 549 mMaxItemId = 1; 550 mMaxScreenId = 0; 551 mNewDbCreated = true; 552 553 addFavoritesTable(db, false); 554 addWorkspacesTable(db, false); 555 556 // Database was just created, so wipe any previous widgets 557 if (mAppWidgetHost != null) { 558 mAppWidgetHost.deleteHost(); 559 560 /** 561 * Send notification that we've deleted the {@link AppWidgetHost}, 562 * probably as part of the initial database creation. The receiver may 563 * want to re-call {@link AppWidgetHost#startListening()} to ensure 564 * callbacks are correctly set. 565 */ 566 new MainThreadExecutor().execute(new Runnable() { 567 568 @Override 569 public void run() { 570 if (mListener != null) { 571 mListener.onAppWidgetHostReset(); 572 } 573 } 574 }); 575 } 576 577 // Fresh and clean launcher DB. 578 mMaxItemId = initializeMaxItemId(db); 579 onEmptyDbCreated(); 580 } 581 582 /** 583 * Overriden in tests. 584 */ onEmptyDbCreated()585 protected void onEmptyDbCreated() { 586 // Set the flag for empty DB 587 Utilities.getPrefs(mContext).edit().putBoolean(EMPTY_DATABASE_CREATED, true).commit(); 588 589 // When a new DB is created, remove all previously stored managed profile information. 590 ManagedProfileHeuristic.processAllUsers(Collections.<UserHandleCompat>emptyList(), 591 mContext); 592 } 593 getDefaultUserSerial()594 protected long getDefaultUserSerial() { 595 return UserManagerCompat.getInstance(mContext).getSerialNumberForUser( 596 UserHandleCompat.myUserHandle()); 597 } 598 addFavoritesTable(SQLiteDatabase db, boolean optional)599 private void addFavoritesTable(SQLiteDatabase db, boolean optional) { 600 String ifNotExists = optional ? " IF NOT EXISTS " : ""; 601 db.execSQL("CREATE TABLE " + ifNotExists + TABLE_FAVORITES + " (" + 602 "_id INTEGER PRIMARY KEY," + 603 "title TEXT," + 604 "intent TEXT," + 605 "container INTEGER," + 606 "screen INTEGER," + 607 "cellX INTEGER," + 608 "cellY INTEGER," + 609 "spanX INTEGER," + 610 "spanY INTEGER," + 611 "itemType INTEGER," + 612 "appWidgetId INTEGER NOT NULL DEFAULT -1," + 613 "isShortcut INTEGER," + 614 "iconType INTEGER," + 615 "iconPackage TEXT," + 616 "iconResource TEXT," + 617 "icon BLOB," + 618 "uri TEXT," + 619 "displayMode INTEGER," + 620 "appWidgetProvider TEXT," + 621 "modified INTEGER NOT NULL DEFAULT 0," + 622 "restored INTEGER NOT NULL DEFAULT 0," + 623 "profileId INTEGER DEFAULT " + getDefaultUserSerial() + "," + 624 "rank INTEGER NOT NULL DEFAULT 0," + 625 "options INTEGER NOT NULL DEFAULT 0" + 626 ");"); 627 } 628 addWorkspacesTable(SQLiteDatabase db, boolean optional)629 private void addWorkspacesTable(SQLiteDatabase db, boolean optional) { 630 String ifNotExists = optional ? " IF NOT EXISTS " : ""; 631 db.execSQL("CREATE TABLE " + ifNotExists + TABLE_WORKSPACE_SCREENS + " (" + 632 LauncherSettings.WorkspaceScreens._ID + " INTEGER PRIMARY KEY," + 633 LauncherSettings.WorkspaceScreens.SCREEN_RANK + " INTEGER," + 634 LauncherSettings.ChangeLogColumns.MODIFIED + " INTEGER NOT NULL DEFAULT 0" + 635 ");"); 636 } 637 removeOrphanedItems(SQLiteDatabase db)638 private void removeOrphanedItems(SQLiteDatabase db) { 639 // Delete items directly on the workspace who's screen id doesn't exist 640 // "DELETE FROM favorites WHERE screen NOT IN (SELECT _id FROM workspaceScreens) 641 // AND container = -100" 642 String removeOrphanedDesktopItems = "DELETE FROM " + TABLE_FAVORITES + 643 " WHERE " + 644 LauncherSettings.Favorites.SCREEN + " NOT IN (SELECT " + 645 LauncherSettings.WorkspaceScreens._ID + " FROM " + TABLE_WORKSPACE_SCREENS + ")" + 646 " AND " + 647 LauncherSettings.Favorites.CONTAINER + " = " + 648 LauncherSettings.Favorites.CONTAINER_DESKTOP; 649 db.execSQL(removeOrphanedDesktopItems); 650 651 // Delete items contained in folders which no longer exist (after above statement) 652 // "DELETE FROM favorites WHERE container <> -100 AND container <> -101 AND container 653 // NOT IN (SELECT _id FROM favorites WHERE itemType = 2)" 654 String removeOrphanedFolderItems = "DELETE FROM " + TABLE_FAVORITES + 655 " WHERE " + 656 LauncherSettings.Favorites.CONTAINER + " <> " + 657 LauncherSettings.Favorites.CONTAINER_DESKTOP + 658 " AND " 659 + LauncherSettings.Favorites.CONTAINER + " <> " + 660 LauncherSettings.Favorites.CONTAINER_HOTSEAT + 661 " AND " 662 + LauncherSettings.Favorites.CONTAINER + " NOT IN (SELECT " + 663 LauncherSettings.Favorites._ID + " FROM " + TABLE_FAVORITES + 664 " WHERE " + LauncherSettings.Favorites.ITEM_TYPE + " = " + 665 LauncherSettings.Favorites.ITEM_TYPE_FOLDER + ")"; 666 db.execSQL(removeOrphanedFolderItems); 667 } 668 setFlagJustLoadedOldDb()669 private void setFlagJustLoadedOldDb() { 670 Utilities.getPrefs(mContext).edit().putBoolean(EMPTY_DATABASE_CREATED, false).commit(); 671 } 672 673 @Override onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)674 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 675 if (LOGD) Log.d(TAG, "onUpgrade triggered: " + oldVersion); 676 switch (oldVersion) { 677 // The version cannot be lower that 12, as Launcher3 never supported a lower 678 // version of the DB. 679 case 12: { 680 // With the new shrink-wrapped and re-orderable workspaces, it makes sense 681 // to persist workspace screens and their relative order. 682 mMaxScreenId = 0; 683 addWorkspacesTable(db, false); 684 } 685 case 13: { 686 db.beginTransaction(); 687 try { 688 // Insert new column for holding widget provider name 689 db.execSQL("ALTER TABLE favorites " + 690 "ADD COLUMN appWidgetProvider TEXT;"); 691 db.setTransactionSuccessful(); 692 } catch (SQLException ex) { 693 Log.e(TAG, ex.getMessage(), ex); 694 // Old version remains, which means we wipe old data 695 break; 696 } finally { 697 db.endTransaction(); 698 } 699 } 700 case 14: { 701 db.beginTransaction(); 702 try { 703 // Insert new column for holding update timestamp 704 db.execSQL("ALTER TABLE favorites " + 705 "ADD COLUMN modified INTEGER NOT NULL DEFAULT 0;"); 706 db.execSQL("ALTER TABLE workspaceScreens " + 707 "ADD COLUMN modified INTEGER NOT NULL DEFAULT 0;"); 708 db.setTransactionSuccessful(); 709 } catch (SQLException ex) { 710 Log.e(TAG, ex.getMessage(), ex); 711 // Old version remains, which means we wipe old data 712 break; 713 } finally { 714 db.endTransaction(); 715 } 716 } 717 case 15: { 718 if (!addIntegerColumn(db, Favorites.RESTORED, 0)) { 719 // Old version remains, which means we wipe old data 720 break; 721 } 722 } 723 case 16: { 724 // We use the db version upgrade here to identify users who may not have seen 725 // clings yet (because they weren't available), but for whom the clings are now 726 // available (tablet users). Because one of the possible cling flows (migration) 727 // is very destructive (wipes out workspaces), we want to prevent this from showing 728 // until clear data. We do so by marking that the clings have been shown. 729 LauncherClings.markFirstRunClingDismissed(mContext); 730 } 731 case 17: { 732 // No-op 733 } 734 case 18: { 735 // Due to a data loss bug, some users may have items associated with screen ids 736 // which no longer exist. Since this can cause other problems, and since the user 737 // will never see these items anyway, we use database upgrade as an opportunity to 738 // clean things up. 739 removeOrphanedItems(db); 740 } 741 case 19: { 742 // Add userId column 743 if (!addProfileColumn(db)) { 744 // Old version remains, which means we wipe old data 745 break; 746 } 747 } 748 case 20: 749 if (!updateFolderItemsRank(db, true)) { 750 break; 751 } 752 case 21: 753 // Recreate workspace table with screen id a primary key 754 if (!recreateWorkspaceTable(db)) { 755 break; 756 } 757 case 22: { 758 if (!addIntegerColumn(db, Favorites.OPTIONS, 0)) { 759 // Old version remains, which means we wipe old data 760 break; 761 } 762 } 763 case 23: 764 // No-op 765 case 24: 766 ManagedProfileHeuristic.markExistingUsersForNoFolderCreation(mContext); 767 case 25: 768 convertShortcutsToLauncherActivities(db); 769 case 26: { 770 // DB Upgraded successfully 771 return; 772 } 773 } 774 775 // DB was not upgraded 776 Log.w(TAG, "Destroying all old data."); 777 createEmptyDB(db); 778 } 779 780 @Override onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion)781 public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { 782 // This shouldn't happen -- throw our hands up in the air and start over. 783 Log.w(TAG, "Database version downgrade from: " + oldVersion + " to " + newVersion + 784 ". Wiping databse."); 785 createEmptyDB(db); 786 } 787 788 /** 789 * Clears all the data for a fresh start. 790 */ createEmptyDB(SQLiteDatabase db)791 public void createEmptyDB(SQLiteDatabase db) { 792 db.execSQL("DROP TABLE IF EXISTS " + TABLE_FAVORITES); 793 db.execSQL("DROP TABLE IF EXISTS " + TABLE_WORKSPACE_SCREENS); 794 onCreate(db); 795 } 796 797 /** 798 * Replaces all shortcuts of type {@link Favorites#ITEM_TYPE_SHORTCUT} which have a valid 799 * launcher activity target with {@link Favorites#ITEM_TYPE_APPLICATION}. 800 */ convertShortcutsToLauncherActivities(SQLiteDatabase db)801 @Thunk void convertShortcutsToLauncherActivities(SQLiteDatabase db) { 802 db.beginTransaction(); 803 Cursor c = null; 804 SQLiteStatement updateStmt = null; 805 806 try { 807 // Only consider the primary user as other users can't have a shortcut. 808 long userSerial = UserManagerCompat.getInstance(mContext) 809 .getSerialNumberForUser(UserHandleCompat.myUserHandle()); 810 c = db.query(TABLE_FAVORITES, new String[] { 811 Favorites._ID, 812 Favorites.INTENT, 813 }, "itemType=" + Favorites.ITEM_TYPE_SHORTCUT + " AND profileId=" + userSerial, 814 null, null, null, null); 815 816 updateStmt = db.compileStatement("UPDATE favorites SET itemType=" 817 + Favorites.ITEM_TYPE_APPLICATION + " WHERE _id=?"); 818 819 final int idIndex = c.getColumnIndexOrThrow(Favorites._ID); 820 final int intentIndex = c.getColumnIndexOrThrow(Favorites.INTENT); 821 822 while (c.moveToNext()) { 823 String intentDescription = c.getString(intentIndex); 824 Intent intent; 825 try { 826 intent = Intent.parseUri(intentDescription, 0); 827 } catch (URISyntaxException e) { 828 Log.e(TAG, "Unable to parse intent", e); 829 continue; 830 } 831 832 if (!Utilities.isLauncherAppTarget(intent)) { 833 continue; 834 } 835 836 long id = c.getLong(idIndex); 837 updateStmt.bindLong(1, id); 838 updateStmt.executeUpdateDelete(); 839 } 840 db.setTransactionSuccessful(); 841 } catch (SQLException ex) { 842 Log.w(TAG, "Error deduping shortcuts", ex); 843 } finally { 844 db.endTransaction(); 845 if (c != null) { 846 c.close(); 847 } 848 if (updateStmt != null) { 849 updateStmt.close(); 850 } 851 } 852 } 853 854 /** 855 * Recreates workspace table and migrates data to the new table. 856 */ recreateWorkspaceTable(SQLiteDatabase db)857 public boolean recreateWorkspaceTable(SQLiteDatabase db) { 858 db.beginTransaction(); 859 try { 860 Cursor c = db.query(TABLE_WORKSPACE_SCREENS, 861 new String[] {LauncherSettings.WorkspaceScreens._ID}, 862 null, null, null, null, 863 LauncherSettings.WorkspaceScreens.SCREEN_RANK); 864 ArrayList<Long> sortedIDs = new ArrayList<Long>(); 865 long maxId = 0; 866 try { 867 while (c.moveToNext()) { 868 Long id = c.getLong(0); 869 if (!sortedIDs.contains(id)) { 870 sortedIDs.add(id); 871 maxId = Math.max(maxId, id); 872 } 873 } 874 } finally { 875 c.close(); 876 } 877 878 db.execSQL("DROP TABLE IF EXISTS " + TABLE_WORKSPACE_SCREENS); 879 addWorkspacesTable(db, false); 880 881 // Add all screen ids back 882 int total = sortedIDs.size(); 883 for (int i = 0; i < total; i++) { 884 ContentValues values = new ContentValues(); 885 values.put(LauncherSettings.WorkspaceScreens._ID, sortedIDs.get(i)); 886 values.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, i); 887 addModifiedTime(values); 888 db.insertOrThrow(TABLE_WORKSPACE_SCREENS, null, values); 889 } 890 db.setTransactionSuccessful(); 891 mMaxScreenId = maxId; 892 } catch (SQLException ex) { 893 // Old version remains, which means we wipe old data 894 Log.e(TAG, ex.getMessage(), ex); 895 return false; 896 } finally { 897 db.endTransaction(); 898 } 899 return true; 900 } 901 updateFolderItemsRank(SQLiteDatabase db, boolean addRankColumn)902 @Thunk boolean updateFolderItemsRank(SQLiteDatabase db, boolean addRankColumn) { 903 db.beginTransaction(); 904 try { 905 if (addRankColumn) { 906 // Insert new column for holding rank 907 db.execSQL("ALTER TABLE favorites ADD COLUMN rank INTEGER NOT NULL DEFAULT 0;"); 908 } 909 910 // Get a map for folder ID to folder width 911 Cursor c = db.rawQuery("SELECT container, MAX(cellX) FROM favorites" 912 + " WHERE container IN (SELECT _id FROM favorites WHERE itemType = ?)" 913 + " GROUP BY container;", 914 new String[] {Integer.toString(LauncherSettings.Favorites.ITEM_TYPE_FOLDER)}); 915 916 while (c.moveToNext()) { 917 db.execSQL("UPDATE favorites SET rank=cellX+(cellY*?) WHERE " 918 + "container=? AND cellX IS NOT NULL AND cellY IS NOT NULL;", 919 new Object[] {c.getLong(1) + 1, c.getLong(0)}); 920 } 921 922 c.close(); 923 db.setTransactionSuccessful(); 924 } catch (SQLException ex) { 925 // Old version remains, which means we wipe old data 926 Log.e(TAG, ex.getMessage(), ex); 927 return false; 928 } finally { 929 db.endTransaction(); 930 } 931 return true; 932 } 933 addProfileColumn(SQLiteDatabase db)934 private boolean addProfileColumn(SQLiteDatabase db) { 935 UserManagerCompat userManager = UserManagerCompat.getInstance(mContext); 936 // Default to the serial number of this user, for older 937 // shortcuts. 938 long userSerialNumber = userManager.getSerialNumberForUser( 939 UserHandleCompat.myUserHandle()); 940 return addIntegerColumn(db, Favorites.PROFILE_ID, userSerialNumber); 941 } 942 addIntegerColumn(SQLiteDatabase db, String columnName, long defaultValue)943 private boolean addIntegerColumn(SQLiteDatabase db, String columnName, long defaultValue) { 944 db.beginTransaction(); 945 try { 946 db.execSQL("ALTER TABLE favorites ADD COLUMN " 947 + columnName + " INTEGER NOT NULL DEFAULT " + defaultValue + ";"); 948 db.setTransactionSuccessful(); 949 } catch (SQLException ex) { 950 Log.e(TAG, ex.getMessage(), ex); 951 return false; 952 } finally { 953 db.endTransaction(); 954 } 955 return true; 956 } 957 958 // Generates a new ID to use for an object in your database. This method should be only 959 // called from the main UI thread. As an exception, we do call it when we call the 960 // constructor from the worker thread; however, this doesn't extend until after the 961 // constructor is called, and we only pass a reference to LauncherProvider to LauncherApp 962 // after that point 963 @Override generateNewItemId()964 public long generateNewItemId() { 965 if (mMaxItemId < 0) { 966 throw new RuntimeException("Error: max item id was not initialized"); 967 } 968 mMaxItemId += 1; 969 return mMaxItemId; 970 } 971 972 @Override insertAndCheck(SQLiteDatabase db, ContentValues values)973 public long insertAndCheck(SQLiteDatabase db, ContentValues values) { 974 return dbInsertAndCheck(this, db, TABLE_FAVORITES, null, values); 975 } 976 checkId(String table, ContentValues values)977 public void checkId(String table, ContentValues values) { 978 long id = values.getAsLong(LauncherSettings.BaseLauncherColumns._ID); 979 if (table == LauncherProvider.TABLE_WORKSPACE_SCREENS) { 980 mMaxScreenId = Math.max(id, mMaxScreenId); 981 } else { 982 mMaxItemId = Math.max(id, mMaxItemId); 983 } 984 } 985 initializeMaxItemId(SQLiteDatabase db)986 private long initializeMaxItemId(SQLiteDatabase db) { 987 return getMaxId(db, TABLE_FAVORITES); 988 } 989 990 // Generates a new ID to use for an workspace screen in your database. This method 991 // should be only called from the main UI thread. As an exception, we do call it when we 992 // call the constructor from the worker thread; however, this doesn't extend until after the 993 // constructor is called, and we only pass a reference to LauncherProvider to LauncherApp 994 // after that point generateNewScreenId()995 public long generateNewScreenId() { 996 if (mMaxScreenId < 0) { 997 throw new RuntimeException("Error: max screen id was not initialized"); 998 } 999 mMaxScreenId += 1; 1000 return mMaxScreenId; 1001 } 1002 initializeMaxScreenId(SQLiteDatabase db)1003 private long initializeMaxScreenId(SQLiteDatabase db) { 1004 return getMaxId(db, TABLE_WORKSPACE_SCREENS); 1005 } 1006 initializeExternalAdd(ContentValues values)1007 @Thunk boolean initializeExternalAdd(ContentValues values) { 1008 // 1. Ensure that externally added items have a valid item id 1009 long id = generateNewItemId(); 1010 values.put(LauncherSettings.Favorites._ID, id); 1011 1012 // 2. In the case of an app widget, and if no app widget id is specified, we 1013 // attempt allocate and bind the widget. 1014 Integer itemType = values.getAsInteger(LauncherSettings.Favorites.ITEM_TYPE); 1015 if (itemType != null && 1016 itemType.intValue() == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET && 1017 !values.containsKey(LauncherSettings.Favorites.APPWIDGET_ID)) { 1018 1019 final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext); 1020 ComponentName cn = ComponentName.unflattenFromString( 1021 values.getAsString(Favorites.APPWIDGET_PROVIDER)); 1022 1023 if (cn != null) { 1024 try { 1025 int appWidgetId = mAppWidgetHost.allocateAppWidgetId(); 1026 values.put(LauncherSettings.Favorites.APPWIDGET_ID, appWidgetId); 1027 if (!appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId,cn)) { 1028 return false; 1029 } 1030 } catch (RuntimeException e) { 1031 Log.e(TAG, "Failed to initialize external widget", e); 1032 return false; 1033 } 1034 } else { 1035 return false; 1036 } 1037 } 1038 1039 // Add screen id if not present 1040 long screenId = values.getAsLong(LauncherSettings.Favorites.SCREEN); 1041 if (!addScreenIdIfNecessary(screenId)) { 1042 return false; 1043 } 1044 return true; 1045 } 1046 1047 // Returns true of screen id exists, or if successfully added addScreenIdIfNecessary(long screenId)1048 private boolean addScreenIdIfNecessary(long screenId) { 1049 if (!hasScreenId(screenId)) { 1050 int rank = getMaxScreenRank() + 1; 1051 1052 ContentValues v = new ContentValues(); 1053 v.put(LauncherSettings.WorkspaceScreens._ID, screenId); 1054 v.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, rank); 1055 if (dbInsertAndCheck(this, getWritableDatabase(), 1056 TABLE_WORKSPACE_SCREENS, null, v) < 0) { 1057 return false; 1058 } 1059 } 1060 return true; 1061 } 1062 hasScreenId(long screenId)1063 private boolean hasScreenId(long screenId) { 1064 SQLiteDatabase db = getWritableDatabase(); 1065 Cursor c = db.rawQuery("SELECT * FROM " + TABLE_WORKSPACE_SCREENS + " WHERE " 1066 + LauncherSettings.WorkspaceScreens._ID + " = " + screenId, null); 1067 if (c != null) { 1068 int count = c.getCount(); 1069 c.close(); 1070 return count > 0; 1071 } else { 1072 return false; 1073 } 1074 } 1075 getMaxScreenRank()1076 private int getMaxScreenRank() { 1077 SQLiteDatabase db = getWritableDatabase(); 1078 Cursor c = db.rawQuery("SELECT MAX(" + LauncherSettings.WorkspaceScreens.SCREEN_RANK 1079 + ") FROM " + TABLE_WORKSPACE_SCREENS, null); 1080 1081 // get the result 1082 final int maxRankIndex = 0; 1083 int rank = -1; 1084 if (c != null && c.moveToNext()) { 1085 rank = c.getInt(maxRankIndex); 1086 } 1087 if (c != null) { 1088 c.close(); 1089 } 1090 1091 return rank; 1092 } 1093 loadFavorites(SQLiteDatabase db, AutoInstallsLayout loader)1094 @Thunk int loadFavorites(SQLiteDatabase db, AutoInstallsLayout loader) { 1095 ArrayList<Long> screenIds = new ArrayList<Long>(); 1096 // TODO: Use multiple loaders with fall-back and transaction. 1097 int count = loader.loadLayout(db, screenIds); 1098 1099 // Add the screens specified by the items above 1100 Collections.sort(screenIds); 1101 int rank = 0; 1102 ContentValues values = new ContentValues(); 1103 for (Long id : screenIds) { 1104 values.clear(); 1105 values.put(LauncherSettings.WorkspaceScreens._ID, id); 1106 values.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, rank); 1107 if (dbInsertAndCheck(this, db, TABLE_WORKSPACE_SCREENS, null, values) < 0) { 1108 throw new RuntimeException("Failed initialize screen table" 1109 + "from default layout"); 1110 } 1111 rank++; 1112 } 1113 1114 // Ensure that the max ids are initialized 1115 mMaxItemId = initializeMaxItemId(db); 1116 mMaxScreenId = initializeMaxScreenId(db); 1117 1118 return count; 1119 } 1120 migrateLauncher2Shortcuts(SQLiteDatabase db, Uri uri)1121 @Thunk void migrateLauncher2Shortcuts(SQLiteDatabase db, Uri uri) { 1122 final ContentResolver resolver = mContext.getContentResolver(); 1123 Cursor c = null; 1124 int count = 0; 1125 int curScreen = 0; 1126 1127 try { 1128 c = resolver.query(uri, null, null, null, "title ASC"); 1129 } catch (Exception e) { 1130 // Ignore 1131 } 1132 1133 // We already have a favorites database in the old provider 1134 if (c != null) { 1135 try { 1136 if (c.getCount() > 0) { 1137 final int idIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID); 1138 final int intentIndex 1139 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.INTENT); 1140 final int titleIndex 1141 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.TITLE); 1142 final int iconTypeIndex 1143 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_TYPE); 1144 final int iconIndex 1145 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON); 1146 final int iconPackageIndex 1147 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_PACKAGE); 1148 final int iconResourceIndex 1149 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_RESOURCE); 1150 final int containerIndex 1151 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CONTAINER); 1152 final int itemTypeIndex 1153 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE); 1154 final int screenIndex 1155 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SCREEN); 1156 final int cellXIndex 1157 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLX); 1158 final int cellYIndex 1159 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLY); 1160 final int uriIndex 1161 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.URI); 1162 final int displayModeIndex 1163 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.DISPLAY_MODE); 1164 final int profileIndex 1165 = c.getColumnIndex(LauncherSettings.Favorites.PROFILE_ID); 1166 1167 int i = 0; 1168 int curX = 0; 1169 int curY = 0; 1170 1171 final LauncherAppState app = LauncherAppState.getInstance(); 1172 final InvariantDeviceProfile profile = app.getInvariantDeviceProfile(); 1173 final int width = (int) profile.numColumns; 1174 final int height = (int) profile.numRows; 1175 final int hotseatWidth = (int) profile.numHotseatIcons; 1176 1177 final HashSet<String> seenIntents = new HashSet<String>(c.getCount()); 1178 1179 final ArrayList<ContentValues> shortcuts = new ArrayList<ContentValues>(); 1180 final ArrayList<ContentValues> folders = new ArrayList<ContentValues>(); 1181 final SparseArray<ContentValues> hotseat = new SparseArray<ContentValues>(); 1182 1183 while (c.moveToNext()) { 1184 final int itemType = c.getInt(itemTypeIndex); 1185 if (itemType != Favorites.ITEM_TYPE_APPLICATION 1186 && itemType != Favorites.ITEM_TYPE_SHORTCUT 1187 && itemType != Favorites.ITEM_TYPE_FOLDER) { 1188 continue; 1189 } 1190 1191 final int cellX = c.getInt(cellXIndex); 1192 final int cellY = c.getInt(cellYIndex); 1193 final int screen = c.getInt(screenIndex); 1194 int container = c.getInt(containerIndex); 1195 final String intentStr = c.getString(intentIndex); 1196 1197 UserManagerCompat userManager = UserManagerCompat.getInstance(mContext); 1198 UserHandleCompat userHandle; 1199 final long userSerialNumber; 1200 if (profileIndex != -1 && !c.isNull(profileIndex)) { 1201 userSerialNumber = c.getInt(profileIndex); 1202 userHandle = userManager.getUserForSerialNumber(userSerialNumber); 1203 } else { 1204 // Default to the serial number of this user, for older 1205 // shortcuts. 1206 userHandle = UserHandleCompat.myUserHandle(); 1207 userSerialNumber = userManager.getSerialNumberForUser(userHandle); 1208 } 1209 1210 if (userHandle == null) { 1211 Launcher.addDumpLog(TAG, "skipping deleted user", true); 1212 continue; 1213 } 1214 1215 Launcher.addDumpLog(TAG, "migrating \"" 1216 + c.getString(titleIndex) + "\" (" 1217 + cellX + "," + cellY + "@" 1218 + LauncherSettings.Favorites.containerToString(container) 1219 + "/" + screen 1220 + "): " + intentStr, true); 1221 1222 if (itemType != Favorites.ITEM_TYPE_FOLDER) { 1223 1224 final Intent intent; 1225 final ComponentName cn; 1226 try { 1227 intent = Intent.parseUri(intentStr, 0); 1228 } catch (URISyntaxException e) { 1229 // bogus intent? 1230 Launcher.addDumpLog(TAG, 1231 "skipping invalid intent uri", true); 1232 continue; 1233 } 1234 1235 cn = intent.getComponent(); 1236 if (TextUtils.isEmpty(intentStr)) { 1237 // no intent? no icon 1238 Launcher.addDumpLog(TAG, "skipping empty intent", true); 1239 continue; 1240 } else if (cn != null && 1241 !LauncherModel.isValidPackageActivity(mContext, cn, 1242 userHandle)) { 1243 // component no longer exists. 1244 Launcher.addDumpLog(TAG, "skipping item whose component " + 1245 "no longer exists.", true); 1246 continue; 1247 } else if (container == 1248 LauncherSettings.Favorites.CONTAINER_DESKTOP) { 1249 // Dedupe icons directly on the workspace 1250 1251 // Canonicalize 1252 // the Play Store sets the package parameter, but Launcher 1253 // does not, so we clear that out to keep them the same. 1254 // Also ignore intent flags for the purposes of deduping. 1255 intent.setPackage(null); 1256 int flags = intent.getFlags(); 1257 intent.setFlags(0); 1258 final String key = intent.toUri(0); 1259 intent.setFlags(flags); 1260 if (seenIntents.contains(key)) { 1261 Launcher.addDumpLog(TAG, "skipping duplicate", true); 1262 continue; 1263 } else { 1264 seenIntents.add(key); 1265 } 1266 } 1267 } 1268 1269 ContentValues values = new ContentValues(c.getColumnCount()); 1270 values.put(LauncherSettings.Favorites._ID, c.getInt(idIndex)); 1271 values.put(LauncherSettings.Favorites.INTENT, intentStr); 1272 values.put(LauncherSettings.Favorites.TITLE, c.getString(titleIndex)); 1273 values.put(LauncherSettings.Favorites.ICON_TYPE, 1274 c.getInt(iconTypeIndex)); 1275 values.put(LauncherSettings.Favorites.ICON, c.getBlob(iconIndex)); 1276 values.put(LauncherSettings.Favorites.ICON_PACKAGE, 1277 c.getString(iconPackageIndex)); 1278 values.put(LauncherSettings.Favorites.ICON_RESOURCE, 1279 c.getString(iconResourceIndex)); 1280 values.put(LauncherSettings.Favorites.ITEM_TYPE, itemType); 1281 values.put(LauncherSettings.Favorites.APPWIDGET_ID, -1); 1282 values.put(LauncherSettings.Favorites.URI, c.getString(uriIndex)); 1283 values.put(LauncherSettings.Favorites.DISPLAY_MODE, 1284 c.getInt(displayModeIndex)); 1285 values.put(LauncherSettings.Favorites.PROFILE_ID, userSerialNumber); 1286 1287 if (container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) { 1288 hotseat.put(screen, values); 1289 } 1290 1291 if (container != LauncherSettings.Favorites.CONTAINER_DESKTOP) { 1292 // In a folder or in the hotseat, preserve position 1293 values.put(LauncherSettings.Favorites.SCREEN, screen); 1294 values.put(LauncherSettings.Favorites.CELLX, cellX); 1295 values.put(LauncherSettings.Favorites.CELLY, cellY); 1296 } else { 1297 // For items contained directly on one of the workspace screen, 1298 // we'll determine their location (screen, x, y) in a second pass. 1299 } 1300 1301 values.put(LauncherSettings.Favorites.CONTAINER, container); 1302 1303 if (itemType != Favorites.ITEM_TYPE_FOLDER) { 1304 shortcuts.add(values); 1305 } else { 1306 folders.add(values); 1307 } 1308 } 1309 1310 // Now that we have all the hotseat icons, let's go through them left-right 1311 // and assign valid locations for them in the new hotseat 1312 final int N = hotseat.size(); 1313 for (int idx=0; idx<N; idx++) { 1314 int hotseatX = hotseat.keyAt(idx); 1315 ContentValues values = hotseat.valueAt(idx); 1316 1317 if (hotseatX == profile.hotseatAllAppsRank) { 1318 // let's drop this in the next available hole in the hotseat 1319 while (++hotseatX < hotseatWidth) { 1320 if (hotseat.get(hotseatX) == null) { 1321 // found a spot! move it here 1322 values.put(LauncherSettings.Favorites.SCREEN, 1323 hotseatX); 1324 break; 1325 } 1326 } 1327 } 1328 if (hotseatX >= hotseatWidth) { 1329 // no room for you in the hotseat? it's off to the desktop with you 1330 values.put(LauncherSettings.Favorites.CONTAINER, 1331 Favorites.CONTAINER_DESKTOP); 1332 } 1333 } 1334 1335 final ArrayList<ContentValues> allItems = new ArrayList<ContentValues>(); 1336 // Folders first 1337 allItems.addAll(folders); 1338 // Then shortcuts 1339 allItems.addAll(shortcuts); 1340 1341 // Layout all the folders 1342 for (ContentValues values: allItems) { 1343 if (values.getAsInteger(LauncherSettings.Favorites.CONTAINER) != 1344 LauncherSettings.Favorites.CONTAINER_DESKTOP) { 1345 // Hotseat items and folder items have already had their 1346 // location information set. Nothing to be done here. 1347 continue; 1348 } 1349 values.put(LauncherSettings.Favorites.SCREEN, curScreen); 1350 values.put(LauncherSettings.Favorites.CELLX, curX); 1351 values.put(LauncherSettings.Favorites.CELLY, curY); 1352 curX = (curX + 1) % width; 1353 if (curX == 0) { 1354 curY = (curY + 1); 1355 } 1356 // Leave the last row of icons blank on every screen 1357 if (curY == height - 1) { 1358 curScreen = (int) generateNewScreenId(); 1359 curY = 0; 1360 } 1361 } 1362 1363 if (allItems.size() > 0) { 1364 db.beginTransaction(); 1365 try { 1366 for (ContentValues row: allItems) { 1367 if (row == null) continue; 1368 if (dbInsertAndCheck(this, db, TABLE_FAVORITES, null, row) 1369 < 0) { 1370 return; 1371 } else { 1372 count++; 1373 } 1374 } 1375 db.setTransactionSuccessful(); 1376 } finally { 1377 db.endTransaction(); 1378 } 1379 } 1380 1381 db.beginTransaction(); 1382 try { 1383 for (i=0; i<=curScreen; i++) { 1384 final ContentValues values = new ContentValues(); 1385 values.put(LauncherSettings.WorkspaceScreens._ID, i); 1386 values.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, i); 1387 if (dbInsertAndCheck(this, db, TABLE_WORKSPACE_SCREENS, null, values) 1388 < 0) { 1389 return; 1390 } 1391 } 1392 db.setTransactionSuccessful(); 1393 } finally { 1394 db.endTransaction(); 1395 } 1396 1397 updateFolderItemsRank(db, false); 1398 } 1399 } finally { 1400 c.close(); 1401 } 1402 } 1403 1404 Launcher.addDumpLog(TAG, "migrated " + count + " icons from Launcher2 into " 1405 + (curScreen+1) + " screens", true); 1406 1407 // ensure that new screens are created to hold these icons 1408 setFlagJustLoadedOldDb(); 1409 1410 // Update max IDs; very important since we just grabbed IDs from another database 1411 mMaxItemId = initializeMaxItemId(db); 1412 mMaxScreenId = initializeMaxScreenId(db); 1413 if (LOGD) Log.d(TAG, "mMaxItemId: " + mMaxItemId + " mMaxScreenId: " + mMaxScreenId); 1414 } 1415 } 1416 1417 /** 1418 * @return the max _id in the provided table. 1419 */ getMaxId(SQLiteDatabase db, String table)1420 @Thunk static long getMaxId(SQLiteDatabase db, String table) { 1421 Cursor c = db.rawQuery("SELECT MAX(_id) FROM " + table, null); 1422 // get the result 1423 long id = -1; 1424 if (c != null && c.moveToNext()) { 1425 id = c.getLong(0); 1426 } 1427 if (c != null) { 1428 c.close(); 1429 } 1430 1431 if (id == -1) { 1432 throw new RuntimeException("Error: could not query max id in " + table); 1433 } 1434 1435 return id; 1436 } 1437 1438 static class SqlArguments { 1439 public final String table; 1440 public final String where; 1441 public final String[] args; 1442 SqlArguments(Uri url, String where, String[] args)1443 SqlArguments(Uri url, String where, String[] args) { 1444 if (url.getPathSegments().size() == 1) { 1445 this.table = url.getPathSegments().get(0); 1446 this.where = where; 1447 this.args = args; 1448 } else if (url.getPathSegments().size() != 2) { 1449 throw new IllegalArgumentException("Invalid URI: " + url); 1450 } else if (!TextUtils.isEmpty(where)) { 1451 throw new UnsupportedOperationException("WHERE clause not supported: " + url); 1452 } else { 1453 this.table = url.getPathSegments().get(0); 1454 this.where = "_id=" + ContentUris.parseId(url); 1455 this.args = null; 1456 } 1457 } 1458 SqlArguments(Uri url)1459 SqlArguments(Uri url) { 1460 if (url.getPathSegments().size() == 1) { 1461 table = url.getPathSegments().get(0); 1462 where = null; 1463 args = null; 1464 } else { 1465 throw new IllegalArgumentException("Invalid URI: " + url); 1466 } 1467 } 1468 } 1469 } 1470