LauncherProvider.java revision 14334bda039b0f70980e8782e379c5df059a5d6e
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 = 21; 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 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 // Populate favorites table with initial favorites 301 if ((mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), loader) <= 0) 302 && usingExternallyProvidedLayout) { 303 // Unable to load external layout. Cleanup and load the internal layout. 304 createEmptyDB(); 305 mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), 306 getDefaultLayoutParser()); 307 } 308 clearFlagEmptyDbCreated(); 309 } 310 } 311 312 private DefaultLayoutParser getDefaultLayoutParser() { 313 int defaultLayout = LauncherAppState.getInstance() 314 .getDynamicGrid().getDeviceProfile().defaultLayoutId; 315 return new DefaultLayoutParser(getContext(), mOpenHelper.mAppWidgetHost, 316 mOpenHelper, getContext().getResources(), defaultLayout); 317 } 318 319 public void migrateLauncher2Shortcuts() { 320 mOpenHelper.migrateLauncher2Shortcuts(mOpenHelper.getWritableDatabase(), 321 Uri.parse(getContext().getString(R.string.old_launcher_provider_uri))); 322 } 323 324 public void updateFolderItemsRank() { 325 mOpenHelper.updateFolderItemsRank(mOpenHelper.getWritableDatabase(), false); 326 } 327 328 public void deleteDatabase() { 329 // Are you sure? (y/n) 330 final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 331 final File dbFile = new File(db.getPath()); 332 mOpenHelper.close(); 333 if (dbFile.exists()) { 334 SQLiteDatabase.deleteDatabase(dbFile); 335 } 336 mOpenHelper = new DatabaseHelper(getContext()); 337 } 338 339 private static class DatabaseHelper extends SQLiteOpenHelper implements LayoutParserCallback { 340 private final Context mContext; 341 private final AppWidgetHost mAppWidgetHost; 342 private long mMaxItemId = -1; 343 private long mMaxScreenId = -1; 344 345 private boolean mNewDbCreated = false; 346 347 DatabaseHelper(Context context) { 348 super(context, LauncherFiles.LAUNCHER_DB, null, DATABASE_VERSION); 349 mContext = context; 350 mAppWidgetHost = new AppWidgetHost(context, Launcher.APPWIDGET_HOST_ID); 351 352 // In the case where neither onCreate nor onUpgrade gets called, we read the maxId from 353 // the DB here 354 if (mMaxItemId == -1) { 355 mMaxItemId = initializeMaxItemId(getWritableDatabase()); 356 } 357 if (mMaxScreenId == -1) { 358 mMaxScreenId = initializeMaxScreenId(getWritableDatabase()); 359 } 360 } 361 362 public boolean wasNewDbCreated() { 363 return mNewDbCreated; 364 } 365 366 /** 367 * Send notification that we've deleted the {@link AppWidgetHost}, 368 * probably as part of the initial database creation. The receiver may 369 * want to re-call {@link AppWidgetHost#startListening()} to ensure 370 * callbacks are correctly set. 371 */ 372 private void sendAppWidgetResetNotify() { 373 final ContentResolver resolver = mContext.getContentResolver(); 374 resolver.notifyChange(CONTENT_APPWIDGET_RESET_URI, null); 375 } 376 377 @Override 378 public void onCreate(SQLiteDatabase db) { 379 if (LOGD) Log.d(TAG, "creating new launcher database"); 380 381 mMaxItemId = 1; 382 mMaxScreenId = 0; 383 mNewDbCreated = true; 384 385 UserManagerCompat userManager = UserManagerCompat.getInstance(mContext); 386 long userSerialNumber = userManager.getSerialNumberForUser( 387 UserHandleCompat.myUserHandle()); 388 389 db.execSQL("CREATE TABLE favorites (" + 390 "_id INTEGER PRIMARY KEY," + 391 "title TEXT," + 392 "intent TEXT," + 393 "container INTEGER," + 394 "screen INTEGER," + 395 "cellX INTEGER," + 396 "cellY INTEGER," + 397 "spanX INTEGER," + 398 "spanY INTEGER," + 399 "itemType INTEGER," + 400 "appWidgetId INTEGER NOT NULL DEFAULT -1," + 401 "isShortcut INTEGER," + 402 "iconType INTEGER," + 403 "iconPackage TEXT," + 404 "iconResource TEXT," + 405 "icon BLOB," + 406 "uri TEXT," + 407 "displayMode INTEGER," + 408 "appWidgetProvider TEXT," + 409 "modified INTEGER NOT NULL DEFAULT 0," + 410 "restored INTEGER NOT NULL DEFAULT 0," + 411 "profileId INTEGER DEFAULT " + userSerialNumber + "," + 412 "rank INTEGER NOT NULL DEFAULT 0" + 413 ");"); 414 addWorkspacesTable(db); 415 416 // Database was just created, so wipe any previous widgets 417 if (mAppWidgetHost != null) { 418 mAppWidgetHost.deleteHost(); 419 sendAppWidgetResetNotify(); 420 } 421 422 // Fresh and clean launcher DB. 423 mMaxItemId = initializeMaxItemId(db); 424 setFlagEmptyDbCreated(); 425 } 426 427 private void addWorkspacesTable(SQLiteDatabase db) { 428 db.execSQL("CREATE TABLE " + TABLE_WORKSPACE_SCREENS + " (" + 429 LauncherSettings.WorkspaceScreens._ID + " INTEGER," + 430 LauncherSettings.WorkspaceScreens.SCREEN_RANK + " INTEGER," + 431 LauncherSettings.ChangeLogColumns.MODIFIED + " INTEGER NOT NULL DEFAULT 0" + 432 ");"); 433 } 434 435 private void removeOrphanedItems(SQLiteDatabase db) { 436 // Delete items directly on the workspace who's screen id doesn't exist 437 // "DELETE FROM favorites WHERE screen NOT IN (SELECT _id FROM workspaceScreens) 438 // AND container = -100" 439 String removeOrphanedDesktopItems = "DELETE FROM " + TABLE_FAVORITES + 440 " WHERE " + 441 LauncherSettings.Favorites.SCREEN + " NOT IN (SELECT " + 442 LauncherSettings.WorkspaceScreens._ID + " FROM " + TABLE_WORKSPACE_SCREENS + ")" + 443 " AND " + 444 LauncherSettings.Favorites.CONTAINER + " = " + 445 LauncherSettings.Favorites.CONTAINER_DESKTOP; 446 db.execSQL(removeOrphanedDesktopItems); 447 448 // Delete items contained in folders which no longer exist (after above statement) 449 // "DELETE FROM favorites WHERE container <> -100 AND container <> -101 AND container 450 // NOT IN (SELECT _id FROM favorites WHERE itemType = 2)" 451 String removeOrphanedFolderItems = "DELETE FROM " + TABLE_FAVORITES + 452 " WHERE " + 453 LauncherSettings.Favorites.CONTAINER + " <> " + 454 LauncherSettings.Favorites.CONTAINER_DESKTOP + 455 " AND " 456 + LauncherSettings.Favorites.CONTAINER + " <> " + 457 LauncherSettings.Favorites.CONTAINER_HOTSEAT + 458 " AND " 459 + LauncherSettings.Favorites.CONTAINER + " NOT IN (SELECT " + 460 LauncherSettings.Favorites._ID + " FROM " + TABLE_FAVORITES + 461 " WHERE " + LauncherSettings.Favorites.ITEM_TYPE + " = " + 462 LauncherSettings.Favorites.ITEM_TYPE_FOLDER + ")"; 463 db.execSQL(removeOrphanedFolderItems); 464 } 465 466 private void setFlagJustLoadedOldDb() { 467 String spKey = LauncherAppState.getSharedPreferencesKey(); 468 SharedPreferences sp = mContext.getSharedPreferences(spKey, Context.MODE_PRIVATE); 469 sp.edit().putBoolean(EMPTY_DATABASE_CREATED, false).commit(); 470 } 471 472 private void setFlagEmptyDbCreated() { 473 String spKey = LauncherAppState.getSharedPreferencesKey(); 474 SharedPreferences sp = mContext.getSharedPreferences(spKey, Context.MODE_PRIVATE); 475 sp.edit().putBoolean(EMPTY_DATABASE_CREATED, true).commit(); 476 } 477 478 @Override 479 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 480 if (LOGD) Log.d(TAG, "onUpgrade triggered: " + oldVersion); 481 switch (oldVersion) { 482 // The version cannot be lower that 12, as Launcher3 never supported a lower 483 // version of the DB. 484 case 12: { 485 // With the new shrink-wrapped and re-orderable workspaces, it makes sense 486 // to persist workspace screens and their relative order. 487 mMaxScreenId = 0; 488 addWorkspacesTable(db); 489 } 490 case 13: { 491 db.beginTransaction(); 492 try { 493 // Insert new column for holding widget provider name 494 db.execSQL("ALTER TABLE favorites " + 495 "ADD COLUMN appWidgetProvider TEXT;"); 496 db.setTransactionSuccessful(); 497 } catch (SQLException ex) { 498 Log.e(TAG, ex.getMessage(), ex); 499 // Old version remains, which means we wipe old data 500 break; 501 } finally { 502 db.endTransaction(); 503 } 504 } 505 case 14: { 506 db.beginTransaction(); 507 try { 508 // Insert new column for holding update timestamp 509 db.execSQL("ALTER TABLE favorites " + 510 "ADD COLUMN modified INTEGER NOT NULL DEFAULT 0;"); 511 db.execSQL("ALTER TABLE workspaceScreens " + 512 "ADD COLUMN modified INTEGER NOT NULL DEFAULT 0;"); 513 db.setTransactionSuccessful(); 514 } catch (SQLException ex) { 515 Log.e(TAG, ex.getMessage(), ex); 516 // Old version remains, which means we wipe old data 517 break; 518 } finally { 519 db.endTransaction(); 520 } 521 } 522 case 15: { 523 db.beginTransaction(); 524 try { 525 // Insert new column for holding restore status 526 db.execSQL("ALTER TABLE favorites " + 527 "ADD COLUMN restored INTEGER NOT NULL DEFAULT 0;"); 528 db.setTransactionSuccessful(); 529 } catch (SQLException ex) { 530 Log.e(TAG, ex.getMessage(), ex); 531 // Old version remains, which means we wipe old data 532 break; 533 } finally { 534 db.endTransaction(); 535 } 536 } 537 case 16: { 538 // We use the db version upgrade here to identify users who may not have seen 539 // clings yet (because they weren't available), but for whom the clings are now 540 // available (tablet users). Because one of the possible cling flows (migration) 541 // is very destructive (wipes out workspaces), we want to prevent this from showing 542 // until clear data. We do so by marking that the clings have been shown. 543 LauncherClings.synchonouslyMarkFirstRunClingDismissed(mContext); 544 } 545 case 17: { 546 // No-op 547 } 548 case 18: { 549 // Due to a data loss bug, some users may have items associated with screen ids 550 // which no longer exist. Since this can cause other problems, and since the user 551 // will never see these items anyway, we use database upgrade as an opportunity to 552 // clean things up. 553 removeOrphanedItems(db); 554 } 555 case 19: { 556 // Add userId column 557 if (!addProfileColumn(db)) { 558 // Old version remains, which means we wipe old data 559 break; 560 } 561 } 562 case 20: 563 if (!updateFolderItemsRank(db, true)) { 564 break; 565 } 566 case 21: { 567 // DB Upgraded successfully 568 return; 569 } 570 } 571 572 // DB was not upgraded 573 Log.w(TAG, "Destroying all old data."); 574 createEmptyDB(db); 575 } 576 577 @Override 578 public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { 579 // This shouldn't happen -- throw our hands up in the air and start over. 580 Log.w(TAG, "Database version downgrade from: " + oldVersion + " to " + newVersion + 581 ". Wiping databse."); 582 createEmptyDB(db); 583 } 584 585 586 /** 587 * Clears all the data for a fresh start. 588 */ 589 public void createEmptyDB(SQLiteDatabase db) { 590 db.execSQL("DROP TABLE IF EXISTS " + TABLE_FAVORITES); 591 db.execSQL("DROP TABLE IF EXISTS " + TABLE_WORKSPACE_SCREENS); 592 onCreate(db); 593 } 594 595 private boolean updateFolderItemsRank(SQLiteDatabase db, boolean addRankColumn) { 596 db.beginTransaction(); 597 try { 598 if (addRankColumn) { 599 // Insert new column for holding rank 600 db.execSQL("ALTER TABLE favorites ADD COLUMN rank INTEGER NOT NULL DEFAULT 0;"); 601 } 602 603 // Get a map for folder ID to folder width 604 Cursor c = db.rawQuery("SELECT container, MAX(cellX) FROM favorites" 605 + " WHERE container IN (SELECT _id FROM favorites WHERE itemType = ?)" 606 + " GROUP BY container;", 607 new String[] {Integer.toString(LauncherSettings.Favorites.ITEM_TYPE_FOLDER)}); 608 609 while (c.moveToNext()) { 610 db.execSQL("UPDATE favorites SET rank=cellX+(cellY*?) WHERE " 611 + "container=? AND cellX IS NOT NULL AND cellY IS NOT NULL;", 612 new Object[] {c.getLong(1) + 1, c.getLong(0)}); 613 } 614 615 c.close(); 616 db.setTransactionSuccessful(); 617 } catch (SQLException ex) { 618 // Old version remains, which means we wipe old data 619 Log.e(TAG, ex.getMessage(), ex); 620 return false; 621 } finally { 622 db.endTransaction(); 623 } 624 return true; 625 } 626 627 private boolean addProfileColumn(SQLiteDatabase db) { 628 db.beginTransaction(); 629 try { 630 UserManagerCompat userManager = UserManagerCompat.getInstance(mContext); 631 // Default to the serial number of this user, for older 632 // shortcuts. 633 long userSerialNumber = userManager.getSerialNumberForUser( 634 UserHandleCompat.myUserHandle()); 635 // Insert new column for holding user serial number 636 db.execSQL("ALTER TABLE favorites " + 637 "ADD COLUMN profileId INTEGER DEFAULT " 638 + userSerialNumber + ";"); 639 db.setTransactionSuccessful(); 640 } catch (SQLException ex) { 641 // Old version remains, which means we wipe old data 642 Log.e(TAG, ex.getMessage(), ex); 643 return false; 644 } finally { 645 db.endTransaction(); 646 } 647 return true; 648 } 649 650 // Generates a new ID to use for an object in your database. This method should be only 651 // called from the main UI thread. As an exception, we do call it when we call the 652 // constructor from the worker thread; however, this doesn't extend until after the 653 // constructor is called, and we only pass a reference to LauncherProvider to LauncherApp 654 // after that point 655 @Override 656 public long generateNewItemId() { 657 if (mMaxItemId < 0) { 658 throw new RuntimeException("Error: max item id was not initialized"); 659 } 660 mMaxItemId += 1; 661 return mMaxItemId; 662 } 663 664 @Override 665 public long insertAndCheck(SQLiteDatabase db, ContentValues values) { 666 return dbInsertAndCheck(this, db, TABLE_FAVORITES, null, values); 667 } 668 669 public void updateMaxItemId(long id) { 670 mMaxItemId = id + 1; 671 } 672 673 public void checkId(String table, ContentValues values) { 674 long id = values.getAsLong(LauncherSettings.BaseLauncherColumns._ID); 675 if (table == LauncherProvider.TABLE_WORKSPACE_SCREENS) { 676 mMaxScreenId = Math.max(id, mMaxScreenId); 677 } else { 678 mMaxItemId = Math.max(id, mMaxItemId); 679 } 680 } 681 682 private long initializeMaxItemId(SQLiteDatabase db) { 683 Cursor c = db.rawQuery("SELECT MAX(_id) FROM favorites", null); 684 685 // get the result 686 final int maxIdIndex = 0; 687 long id = -1; 688 if (c != null && c.moveToNext()) { 689 id = c.getLong(maxIdIndex); 690 } 691 if (c != null) { 692 c.close(); 693 } 694 695 if (id == -1) { 696 throw new RuntimeException("Error: could not query max item id"); 697 } 698 699 return id; 700 } 701 702 // Generates a new ID to use for an workspace screen in your database. This method 703 // should be only called from the main UI thread. As an exception, we do call it when we 704 // call the constructor from the worker thread; however, this doesn't extend until after the 705 // constructor is called, and we only pass a reference to LauncherProvider to LauncherApp 706 // after that point 707 public long generateNewScreenId() { 708 if (mMaxScreenId < 0) { 709 throw new RuntimeException("Error: max screen id was not initialized"); 710 } 711 mMaxScreenId += 1; 712 // Log to disk 713 Launcher.addDumpLog(TAG, "11683562 - generateNewScreenId(): " + mMaxScreenId, true); 714 return mMaxScreenId; 715 } 716 717 private long initializeMaxScreenId(SQLiteDatabase db) { 718 Cursor c = db.rawQuery("SELECT MAX(" + LauncherSettings.WorkspaceScreens._ID + ") FROM " + TABLE_WORKSPACE_SCREENS, null); 719 720 // get the result 721 final int maxIdIndex = 0; 722 long id = -1; 723 if (c != null && c.moveToNext()) { 724 id = c.getLong(maxIdIndex); 725 } 726 if (c != null) { 727 c.close(); 728 } 729 730 if (id == -1) { 731 throw new RuntimeException("Error: could not query max screen id"); 732 } 733 734 // Log to disk 735 Launcher.addDumpLog(TAG, "11683562 - initializeMaxScreenId(): " + id, true); 736 return id; 737 } 738 739 private boolean initializeExternalAdd(ContentValues values) { 740 // 1. Ensure that externally added items have a valid item id 741 long id = generateNewItemId(); 742 values.put(LauncherSettings.Favorites._ID, id); 743 744 // 2. In the case of an app widget, and if no app widget id is specified, we 745 // attempt allocate and bind the widget. 746 Integer itemType = values.getAsInteger(LauncherSettings.Favorites.ITEM_TYPE); 747 if (itemType != null && 748 itemType.intValue() == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET && 749 !values.containsKey(LauncherSettings.Favorites.APPWIDGET_ID)) { 750 751 final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext); 752 ComponentName cn = ComponentName.unflattenFromString( 753 values.getAsString(Favorites.APPWIDGET_PROVIDER)); 754 755 if (cn != null) { 756 try { 757 int appWidgetId = mAppWidgetHost.allocateAppWidgetId(); 758 values.put(LauncherSettings.Favorites.APPWIDGET_ID, appWidgetId); 759 if (!appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId,cn)) { 760 return false; 761 } 762 } catch (RuntimeException e) { 763 Log.e(TAG, "Failed to initialize external widget", e); 764 return false; 765 } 766 } else { 767 return false; 768 } 769 } 770 771 // Add screen id if not present 772 long screenId = values.getAsLong(LauncherSettings.Favorites.SCREEN); 773 if (!addScreenIdIfNecessary(screenId)) { 774 return false; 775 } 776 return true; 777 } 778 779 // Returns true of screen id exists, or if successfully added 780 private boolean addScreenIdIfNecessary(long screenId) { 781 if (!hasScreenId(screenId)) { 782 int rank = getMaxScreenRank() + 1; 783 784 ContentValues v = new ContentValues(); 785 v.put(LauncherSettings.WorkspaceScreens._ID, screenId); 786 v.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, rank); 787 if (dbInsertAndCheck(this, getWritableDatabase(), 788 TABLE_WORKSPACE_SCREENS, null, v) < 0) { 789 return false; 790 } 791 } 792 return true; 793 } 794 795 private boolean hasScreenId(long screenId) { 796 SQLiteDatabase db = getWritableDatabase(); 797 Cursor c = db.rawQuery("SELECT * FROM " + TABLE_WORKSPACE_SCREENS + " WHERE " 798 + LauncherSettings.WorkspaceScreens._ID + " = " + screenId, null); 799 if (c != null) { 800 int count = c.getCount(); 801 c.close(); 802 return count > 0; 803 } else { 804 return false; 805 } 806 } 807 808 private int getMaxScreenRank() { 809 SQLiteDatabase db = getWritableDatabase(); 810 Cursor c = db.rawQuery("SELECT MAX(" + LauncherSettings.WorkspaceScreens.SCREEN_RANK 811 + ") FROM " + TABLE_WORKSPACE_SCREENS, null); 812 813 // get the result 814 final int maxRankIndex = 0; 815 int rank = -1; 816 if (c != null && c.moveToNext()) { 817 rank = c.getInt(maxRankIndex); 818 } 819 if (c != null) { 820 c.close(); 821 } 822 823 return rank; 824 } 825 826 private int loadFavorites(SQLiteDatabase db, AutoInstallsLayout loader) { 827 ArrayList<Long> screenIds = new ArrayList<Long>(); 828 // TODO: Use multiple loaders with fall-back and transaction. 829 int count = loader.loadLayout(db, screenIds); 830 831 // Add the screens specified by the items above 832 Collections.sort(screenIds); 833 int rank = 0; 834 ContentValues values = new ContentValues(); 835 for (Long id : screenIds) { 836 values.clear(); 837 values.put(LauncherSettings.WorkspaceScreens._ID, id); 838 values.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, rank); 839 if (dbInsertAndCheck(this, db, TABLE_WORKSPACE_SCREENS, null, values) < 0) { 840 throw new RuntimeException("Failed initialize screen table" 841 + "from default layout"); 842 } 843 rank++; 844 } 845 846 // Ensure that the max ids are initialized 847 mMaxItemId = initializeMaxItemId(db); 848 mMaxScreenId = initializeMaxScreenId(db); 849 850 return count; 851 } 852 853 private void migrateLauncher2Shortcuts(SQLiteDatabase db, Uri uri) { 854 final ContentResolver resolver = mContext.getContentResolver(); 855 Cursor c = null; 856 int count = 0; 857 int curScreen = 0; 858 859 try { 860 c = resolver.query(uri, null, null, null, "title ASC"); 861 } catch (Exception e) { 862 // Ignore 863 } 864 865 // We already have a favorites database in the old provider 866 if (c != null) { 867 try { 868 if (c.getCount() > 0) { 869 final int idIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID); 870 final int intentIndex 871 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.INTENT); 872 final int titleIndex 873 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.TITLE); 874 final int iconTypeIndex 875 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_TYPE); 876 final int iconIndex 877 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON); 878 final int iconPackageIndex 879 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_PACKAGE); 880 final int iconResourceIndex 881 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_RESOURCE); 882 final int containerIndex 883 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CONTAINER); 884 final int itemTypeIndex 885 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE); 886 final int screenIndex 887 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SCREEN); 888 final int cellXIndex 889 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLX); 890 final int cellYIndex 891 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLY); 892 final int uriIndex 893 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.URI); 894 final int displayModeIndex 895 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.DISPLAY_MODE); 896 final int profileIndex 897 = c.getColumnIndex(LauncherSettings.Favorites.PROFILE_ID); 898 899 int i = 0; 900 int curX = 0; 901 int curY = 0; 902 903 final LauncherAppState app = LauncherAppState.getInstance(); 904 final DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); 905 final int width = (int) grid.numColumns; 906 final int height = (int) grid.numRows; 907 final int hotseatWidth = (int) grid.numHotseatIcons; 908 909 final HashSet<String> seenIntents = new HashSet<String>(c.getCount()); 910 911 final ArrayList<ContentValues> shortcuts = new ArrayList<ContentValues>(); 912 final ArrayList<ContentValues> folders = new ArrayList<ContentValues>(); 913 final SparseArray<ContentValues> hotseat = new SparseArray<ContentValues>(); 914 915 while (c.moveToNext()) { 916 final int itemType = c.getInt(itemTypeIndex); 917 if (itemType != Favorites.ITEM_TYPE_APPLICATION 918 && itemType != Favorites.ITEM_TYPE_SHORTCUT 919 && itemType != Favorites.ITEM_TYPE_FOLDER) { 920 continue; 921 } 922 923 final int cellX = c.getInt(cellXIndex); 924 final int cellY = c.getInt(cellYIndex); 925 final int screen = c.getInt(screenIndex); 926 int container = c.getInt(containerIndex); 927 final String intentStr = c.getString(intentIndex); 928 929 UserManagerCompat userManager = UserManagerCompat.getInstance(mContext); 930 UserHandleCompat userHandle; 931 final long userSerialNumber; 932 if (profileIndex != -1 && !c.isNull(profileIndex)) { 933 userSerialNumber = c.getInt(profileIndex); 934 userHandle = userManager.getUserForSerialNumber(userSerialNumber); 935 } else { 936 // Default to the serial number of this user, for older 937 // shortcuts. 938 userHandle = UserHandleCompat.myUserHandle(); 939 userSerialNumber = userManager.getSerialNumberForUser(userHandle); 940 } 941 942 if (userHandle == null) { 943 Launcher.addDumpLog(TAG, "skipping deleted user", true); 944 continue; 945 } 946 947 Launcher.addDumpLog(TAG, "migrating \"" 948 + c.getString(titleIndex) + "\" (" 949 + cellX + "," + cellY + "@" 950 + LauncherSettings.Favorites.containerToString(container) 951 + "/" + screen 952 + "): " + intentStr, true); 953 954 if (itemType != Favorites.ITEM_TYPE_FOLDER) { 955 956 final Intent intent; 957 final ComponentName cn; 958 try { 959 intent = Intent.parseUri(intentStr, 0); 960 } catch (URISyntaxException e) { 961 // bogus intent? 962 Launcher.addDumpLog(TAG, 963 "skipping invalid intent uri", true); 964 continue; 965 } 966 967 cn = intent.getComponent(); 968 if (TextUtils.isEmpty(intentStr)) { 969 // no intent? no icon 970 Launcher.addDumpLog(TAG, "skipping empty intent", true); 971 continue; 972 } else if (cn != null && 973 !LauncherModel.isValidPackageActivity(mContext, cn, 974 userHandle)) { 975 // component no longer exists. 976 Launcher.addDumpLog(TAG, "skipping item whose component " + 977 "no longer exists.", true); 978 continue; 979 } else if (container == 980 LauncherSettings.Favorites.CONTAINER_DESKTOP) { 981 // Dedupe icons directly on the workspace 982 983 // Canonicalize 984 // the Play Store sets the package parameter, but Launcher 985 // does not, so we clear that out to keep them the same. 986 // Also ignore intent flags for the purposes of deduping. 987 intent.setPackage(null); 988 int flags = intent.getFlags(); 989 intent.setFlags(0); 990 final String key = intent.toUri(0); 991 intent.setFlags(flags); 992 if (seenIntents.contains(key)) { 993 Launcher.addDumpLog(TAG, "skipping duplicate", true); 994 continue; 995 } else { 996 seenIntents.add(key); 997 } 998 } 999 } 1000 1001 ContentValues values = new ContentValues(c.getColumnCount()); 1002 values.put(LauncherSettings.Favorites._ID, c.getInt(idIndex)); 1003 values.put(LauncherSettings.Favorites.INTENT, intentStr); 1004 values.put(LauncherSettings.Favorites.TITLE, c.getString(titleIndex)); 1005 values.put(LauncherSettings.Favorites.ICON_TYPE, 1006 c.getInt(iconTypeIndex)); 1007 values.put(LauncherSettings.Favorites.ICON, c.getBlob(iconIndex)); 1008 values.put(LauncherSettings.Favorites.ICON_PACKAGE, 1009 c.getString(iconPackageIndex)); 1010 values.put(LauncherSettings.Favorites.ICON_RESOURCE, 1011 c.getString(iconResourceIndex)); 1012 values.put(LauncherSettings.Favorites.ITEM_TYPE, itemType); 1013 values.put(LauncherSettings.Favorites.APPWIDGET_ID, -1); 1014 values.put(LauncherSettings.Favorites.URI, c.getString(uriIndex)); 1015 values.put(LauncherSettings.Favorites.DISPLAY_MODE, 1016 c.getInt(displayModeIndex)); 1017 values.put(LauncherSettings.Favorites.PROFILE_ID, userSerialNumber); 1018 1019 if (container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) { 1020 hotseat.put(screen, values); 1021 } 1022 1023 if (container != LauncherSettings.Favorites.CONTAINER_DESKTOP) { 1024 // In a folder or in the hotseat, preserve position 1025 values.put(LauncherSettings.Favorites.SCREEN, screen); 1026 values.put(LauncherSettings.Favorites.CELLX, cellX); 1027 values.put(LauncherSettings.Favorites.CELLY, cellY); 1028 } else { 1029 // For items contained directly on one of the workspace screen, 1030 // we'll determine their location (screen, x, y) in a second pass. 1031 } 1032 1033 values.put(LauncherSettings.Favorites.CONTAINER, container); 1034 1035 if (itemType != Favorites.ITEM_TYPE_FOLDER) { 1036 shortcuts.add(values); 1037 } else { 1038 folders.add(values); 1039 } 1040 } 1041 1042 // Now that we have all the hotseat icons, let's go through them left-right 1043 // and assign valid locations for them in the new hotseat 1044 final int N = hotseat.size(); 1045 for (int idx=0; idx<N; idx++) { 1046 int hotseatX = hotseat.keyAt(idx); 1047 ContentValues values = hotseat.valueAt(idx); 1048 1049 if (hotseatX == grid.hotseatAllAppsRank) { 1050 // let's drop this in the next available hole in the hotseat 1051 while (++hotseatX < hotseatWidth) { 1052 if (hotseat.get(hotseatX) == null) { 1053 // found a spot! move it here 1054 values.put(LauncherSettings.Favorites.SCREEN, 1055 hotseatX); 1056 break; 1057 } 1058 } 1059 } 1060 if (hotseatX >= hotseatWidth) { 1061 // no room for you in the hotseat? it's off to the desktop with you 1062 values.put(LauncherSettings.Favorites.CONTAINER, 1063 Favorites.CONTAINER_DESKTOP); 1064 } 1065 } 1066 1067 final ArrayList<ContentValues> allItems = new ArrayList<ContentValues>(); 1068 // Folders first 1069 allItems.addAll(folders); 1070 // Then shortcuts 1071 allItems.addAll(shortcuts); 1072 1073 // Layout all the folders 1074 for (ContentValues values: allItems) { 1075 if (values.getAsInteger(LauncherSettings.Favorites.CONTAINER) != 1076 LauncherSettings.Favorites.CONTAINER_DESKTOP) { 1077 // Hotseat items and folder items have already had their 1078 // location information set. Nothing to be done here. 1079 continue; 1080 } 1081 values.put(LauncherSettings.Favorites.SCREEN, curScreen); 1082 values.put(LauncherSettings.Favorites.CELLX, curX); 1083 values.put(LauncherSettings.Favorites.CELLY, curY); 1084 curX = (curX + 1) % width; 1085 if (curX == 0) { 1086 curY = (curY + 1); 1087 } 1088 // Leave the last row of icons blank on every screen 1089 if (curY == height - 1) { 1090 curScreen = (int) generateNewScreenId(); 1091 curY = 0; 1092 } 1093 } 1094 1095 if (allItems.size() > 0) { 1096 db.beginTransaction(); 1097 try { 1098 for (ContentValues row: allItems) { 1099 if (row == null) continue; 1100 if (dbInsertAndCheck(this, db, TABLE_FAVORITES, null, row) 1101 < 0) { 1102 return; 1103 } else { 1104 count++; 1105 } 1106 } 1107 db.setTransactionSuccessful(); 1108 } finally { 1109 db.endTransaction(); 1110 } 1111 } 1112 1113 db.beginTransaction(); 1114 try { 1115 for (i=0; i<=curScreen; i++) { 1116 final ContentValues values = new ContentValues(); 1117 values.put(LauncherSettings.WorkspaceScreens._ID, i); 1118 values.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, i); 1119 if (dbInsertAndCheck(this, db, TABLE_WORKSPACE_SCREENS, null, values) 1120 < 0) { 1121 return; 1122 } 1123 } 1124 db.setTransactionSuccessful(); 1125 } finally { 1126 db.endTransaction(); 1127 } 1128 1129 updateFolderItemsRank(db, false); 1130 } 1131 } finally { 1132 c.close(); 1133 } 1134 } 1135 1136 Launcher.addDumpLog(TAG, "migrated " + count + " icons from Launcher2 into " 1137 + (curScreen+1) + " screens", true); 1138 1139 // ensure that new screens are created to hold these icons 1140 setFlagJustLoadedOldDb(); 1141 1142 // Update max IDs; very important since we just grabbed IDs from another database 1143 mMaxItemId = initializeMaxItemId(db); 1144 mMaxScreenId = initializeMaxScreenId(db); 1145 if (LOGD) Log.d(TAG, "mMaxItemId: " + mMaxItemId + " mMaxScreenId: " + mMaxScreenId); 1146 } 1147 } 1148 1149 static class SqlArguments { 1150 public final String table; 1151 public final String where; 1152 public final String[] args; 1153 1154 SqlArguments(Uri url, String where, String[] args) { 1155 if (url.getPathSegments().size() == 1) { 1156 this.table = url.getPathSegments().get(0); 1157 this.where = where; 1158 this.args = args; 1159 } else if (url.getPathSegments().size() != 2) { 1160 throw new IllegalArgumentException("Invalid URI: " + url); 1161 } else if (!TextUtils.isEmpty(where)) { 1162 throw new UnsupportedOperationException("WHERE clause not supported: " + url); 1163 } else { 1164 this.table = url.getPathSegments().get(0); 1165 this.where = "_id=" + ContentUris.parseId(url); 1166 this.args = null; 1167 } 1168 } 1169 1170 SqlArguments(Uri url) { 1171 if (url.getPathSegments().size() == 1) { 1172 table = url.getPathSegments().get(0); 1173 where = null; 1174 args = null; 1175 } else { 1176 throw new IllegalArgumentException("Invalid URI: " + url); 1177 } 1178 } 1179 } 1180} 1181