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