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.ContentUris; 27 import android.content.ContentValues; 28 import android.content.Context; 29 import android.content.Intent; 30 import android.content.OperationApplicationException; 31 import android.content.SharedPreferences; 32 import android.content.pm.PackageManager.NameNotFoundException; 33 import android.content.res.Resources; 34 import android.database.Cursor; 35 import android.database.SQLException; 36 import android.database.sqlite.SQLiteDatabase; 37 import android.database.sqlite.SQLiteOpenHelper; 38 import android.database.sqlite.SQLiteQueryBuilder; 39 import android.database.sqlite.SQLiteStatement; 40 import android.net.Uri; 41 import android.os.Binder; 42 import android.os.Build; 43 import android.os.Bundle; 44 import android.os.Handler; 45 import android.os.Message; 46 import android.os.Process; 47 import android.os.Trace; 48 import android.os.UserManager; 49 import android.text.TextUtils; 50 import android.util.Log; 51 52 import com.android.launcher3.AutoInstallsLayout.LayoutParserCallback; 53 import com.android.launcher3.LauncherSettings.Favorites; 54 import com.android.launcher3.LauncherSettings.WorkspaceScreens; 55 import com.android.launcher3.compat.UserHandleCompat; 56 import com.android.launcher3.compat.UserManagerCompat; 57 import com.android.launcher3.config.FeatureFlags; 58 import com.android.launcher3.config.ProviderConfig; 59 import com.android.launcher3.dynamicui.ExtractionUtils; 60 import com.android.launcher3.provider.LauncherDbUtils; 61 import com.android.launcher3.provider.RestoreDbTask; 62 import com.android.launcher3.util.ManagedProfileHeuristic; 63 import com.android.launcher3.util.NoLocaleSqliteContext; 64 import com.android.launcher3.util.Preconditions; 65 import com.android.launcher3.util.Thunk; 66 67 import java.net.URISyntaxException; 68 import java.util.ArrayList; 69 import java.util.Collections; 70 import java.util.Locale; 71 72 public class LauncherProvider extends ContentProvider { 73 private static final String TAG = "LauncherProvider"; 74 private static final boolean LOGD = false; 75 76 private static final int DATABASE_VERSION = 27; 77 78 public static final String AUTHORITY = ProviderConfig.AUTHORITY; 79 80 static final String EMPTY_DATABASE_CREATED = "EMPTY_DATABASE_CREATED"; 81 82 private static final String RESTRICTION_PACKAGE_NAME = "workspace.configuration.package.name"; 83 84 private final ChangeListenerWrapper mListenerWrapper = new ChangeListenerWrapper(); 85 private Handler mListenerHandler; 86 87 protected DatabaseHelper mOpenHelper; 88 89 @Override 90 public boolean onCreate() { 91 if (ProviderConfig.IS_DOGFOOD_BUILD) { 92 Log.d(TAG, "Launcher process started"); 93 } 94 mListenerHandler = new Handler(mListenerWrapper); 95 96 LauncherAppState.setLauncherProvider(this); 97 return true; 98 } 99 100 /** 101 * Sets a provider listener. 102 */ 103 public void setLauncherProviderChangeListener(LauncherProviderChangeListener listener) { 104 Preconditions.assertUIThread(); 105 mListenerWrapper.mListener = listener; 106 } 107 108 @Override 109 public String getType(Uri uri) { 110 SqlArguments args = new SqlArguments(uri, null, null); 111 if (TextUtils.isEmpty(args.where)) { 112 return "vnd.android.cursor.dir/" + args.table; 113 } else { 114 return "vnd.android.cursor.item/" + args.table; 115 } 116 } 117 118 /** 119 * Overridden in tests 120 */ 121 protected synchronized void createDbIfNotExists() { 122 if (mOpenHelper == null) { 123 if (LauncherAppState.PROFILE_STARTUP) { 124 Trace.beginSection("Opening workspace DB"); 125 } 126 mOpenHelper = new DatabaseHelper(getContext(), mListenerHandler); 127 128 if (RestoreDbTask.isPending(getContext())) { 129 if (!RestoreDbTask.performRestore(mOpenHelper)) { 130 mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase()); 131 } 132 // Set is pending to false irrespective of the result, so that it doesn't get 133 // executed again. 134 RestoreDbTask.setPending(getContext(), false); 135 } 136 137 if (LauncherAppState.PROFILE_STARTUP) { 138 Trace.endSection(); 139 } 140 } 141 } 142 143 @Override 144 public Cursor query(Uri uri, String[] projection, String selection, 145 String[] selectionArgs, String sortOrder) { 146 createDbIfNotExists(); 147 148 SqlArguments args = new SqlArguments(uri, selection, selectionArgs); 149 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 150 qb.setTables(args.table); 151 152 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 153 Cursor result = qb.query(db, projection, args.where, args.args, null, null, sortOrder); 154 result.setNotificationUri(getContext().getContentResolver(), uri); 155 156 return result; 157 } 158 159 @Thunk static long dbInsertAndCheck(DatabaseHelper helper, 160 SQLiteDatabase db, String table, String nullColumnHack, ContentValues values) { 161 if (values == null) { 162 throw new RuntimeException("Error: attempting to insert null values"); 163 } 164 if (!values.containsKey(LauncherSettings.ChangeLogColumns._ID)) { 165 throw new RuntimeException("Error: attempting to add item without specifying an id"); 166 } 167 helper.checkId(table, values); 168 return db.insert(table, nullColumnHack, values); 169 } 170 171 private void reloadLauncherIfExternal() { 172 if (Utilities.ATLEAST_MARSHMALLOW && Binder.getCallingPid() != Process.myPid()) { 173 LauncherAppState app = LauncherAppState.getInstanceNoCreate(); 174 if (app != null) { 175 app.reloadWorkspace(); 176 } 177 } 178 } 179 180 @Override 181 public Uri insert(Uri uri, ContentValues initialValues) { 182 createDbIfNotExists(); 183 SqlArguments args = new SqlArguments(uri); 184 185 // In very limited cases, we support system|signature permission apps to modify the db. 186 if (Binder.getCallingPid() != Process.myPid()) { 187 if (!initializeExternalAdd(initialValues)) { 188 return null; 189 } 190 } 191 192 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 193 addModifiedTime(initialValues); 194 final long rowId = dbInsertAndCheck(mOpenHelper, db, args.table, null, initialValues); 195 if (rowId < 0) return null; 196 197 uri = ContentUris.withAppendedId(uri, rowId); 198 notifyListeners(); 199 200 if (Utilities.ATLEAST_MARSHMALLOW) { 201 reloadLauncherIfExternal(); 202 } else { 203 // Deprecated behavior to support legacy devices which rely on provider callbacks. 204 LauncherAppState app = LauncherAppState.getInstanceNoCreate(); 205 if (app != null && "true".equals(uri.getQueryParameter("isExternalAdd"))) { 206 app.reloadWorkspace(); 207 } 208 209 String notify = uri.getQueryParameter("notify"); 210 if (notify == null || "true".equals(notify)) { 211 getContext().getContentResolver().notifyChange(uri, null); 212 } 213 } 214 return uri; 215 } 216 217 private boolean initializeExternalAdd(ContentValues values) { 218 // 1. Ensure that externally added items have a valid item id 219 long id = mOpenHelper.generateNewItemId(); 220 values.put(LauncherSettings.Favorites._ID, id); 221 222 // 2. In the case of an app widget, and if no app widget id is specified, we 223 // attempt allocate and bind the widget. 224 Integer itemType = values.getAsInteger(LauncherSettings.Favorites.ITEM_TYPE); 225 if (itemType != null && 226 itemType.intValue() == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET && 227 !values.containsKey(LauncherSettings.Favorites.APPWIDGET_ID)) { 228 229 final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(getContext()); 230 ComponentName cn = ComponentName.unflattenFromString( 231 values.getAsString(Favorites.APPWIDGET_PROVIDER)); 232 233 if (cn != null) { 234 try { 235 int appWidgetId = new AppWidgetHost(getContext(), Launcher.APPWIDGET_HOST_ID) 236 .allocateAppWidgetId(); 237 values.put(LauncherSettings.Favorites.APPWIDGET_ID, appWidgetId); 238 if (!appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId,cn)) { 239 return false; 240 } 241 } catch (RuntimeException e) { 242 Log.e(TAG, "Failed to initialize external widget", e); 243 return false; 244 } 245 } else { 246 return false; 247 } 248 } 249 250 // Add screen id if not present 251 long screenId = values.getAsLong(LauncherSettings.Favorites.SCREEN); 252 SQLiteStatement stmp = null; 253 try { 254 stmp = mOpenHelper.getWritableDatabase().compileStatement( 255 "INSERT OR IGNORE INTO workspaceScreens (_id, screenRank) " + 256 "select ?, (ifnull(MAX(screenRank), -1)+1) from workspaceScreens"); 257 stmp.bindLong(1, screenId); 258 259 ContentValues valuesInserted = new ContentValues(); 260 valuesInserted.put(LauncherSettings.BaseLauncherColumns._ID, stmp.executeInsert()); 261 mOpenHelper.checkId(WorkspaceScreens.TABLE_NAME, valuesInserted); 262 return true; 263 } catch (Exception e) { 264 return false; 265 } finally { 266 Utilities.closeSilently(stmp); 267 } 268 } 269 270 @Override 271 public int bulkInsert(Uri uri, ContentValues[] values) { 272 createDbIfNotExists(); 273 SqlArguments args = new SqlArguments(uri); 274 275 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 276 db.beginTransaction(); 277 try { 278 int numValues = values.length; 279 for (int i = 0; i < numValues; i++) { 280 addModifiedTime(values[i]); 281 if (dbInsertAndCheck(mOpenHelper, db, args.table, null, values[i]) < 0) { 282 return 0; 283 } 284 } 285 db.setTransactionSuccessful(); 286 } finally { 287 db.endTransaction(); 288 } 289 290 notifyListeners(); 291 reloadLauncherIfExternal(); 292 return values.length; 293 } 294 295 @Override 296 public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) 297 throws OperationApplicationException { 298 createDbIfNotExists(); 299 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 300 db.beginTransaction(); 301 try { 302 ContentProviderResult[] result = super.applyBatch(operations); 303 db.setTransactionSuccessful(); 304 reloadLauncherIfExternal(); 305 return result; 306 } finally { 307 db.endTransaction(); 308 } 309 } 310 311 @Override 312 public int delete(Uri uri, String selection, String[] selectionArgs) { 313 createDbIfNotExists(); 314 SqlArguments args = new SqlArguments(uri, selection, selectionArgs); 315 316 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 317 318 if (Binder.getCallingPid() != Process.myPid() 319 && Favorites.TABLE_NAME.equalsIgnoreCase(args.table)) { 320 String widgetSelection = TextUtils.isEmpty(args.where) ? "1=1" : args.where; 321 widgetSelection = String.format(Locale.ENGLISH, "%1$s = %2$d AND ( %3$s )", 322 Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_APPWIDGET, widgetSelection); 323 try (Cursor c = db.query(Favorites.TABLE_NAME, new String[] { Favorites.APPWIDGET_ID }, 324 widgetSelection, args.args, null, null, null)) { 325 AppWidgetHost host = new AppWidgetHost(getContext(), Launcher.APPWIDGET_HOST_ID); 326 while (c.moveToNext()) { 327 int widgetId = c.getInt(0); 328 if (widgetId != AppWidgetManager.INVALID_APPWIDGET_ID) { 329 try { 330 host.deleteAppWidgetId(widgetId); 331 } catch (RuntimeException e) { 332 Log.e(TAG, "Error deleting widget id " + widgetId, e); 333 } 334 } 335 } 336 } 337 } 338 int count = db.delete(args.table, args.where, args.args); 339 if (count > 0) { 340 notifyListeners(); 341 reloadLauncherIfExternal(); 342 } 343 return count; 344 } 345 346 @Override 347 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 348 createDbIfNotExists(); 349 SqlArguments args = new SqlArguments(uri, selection, selectionArgs); 350 351 addModifiedTime(values); 352 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 353 int count = db.update(args.table, values, args.where, args.args); 354 if (count > 0) notifyListeners(); 355 356 reloadLauncherIfExternal(); 357 return count; 358 } 359 360 @Override 361 public Bundle call(String method, final String arg, final Bundle extras) { 362 if (Binder.getCallingUid() != Process.myUid()) { 363 return null; 364 } 365 createDbIfNotExists(); 366 367 switch (method) { 368 case LauncherSettings.Settings.METHOD_SET_EXTRACTED_COLORS_AND_WALLPAPER_ID: { 369 String extractedColors = extras.getString( 370 LauncherSettings.Settings.EXTRA_EXTRACTED_COLORS); 371 int wallpaperId = extras.getInt(LauncherSettings.Settings.EXTRA_WALLPAPER_ID); 372 Utilities.getPrefs(getContext()).edit() 373 .putString(ExtractionUtils.EXTRACTED_COLORS_PREFERENCE_KEY, extractedColors) 374 .putInt(ExtractionUtils.WALLPAPER_ID_PREFERENCE_KEY, wallpaperId) 375 .apply(); 376 mListenerHandler.sendEmptyMessage(ChangeListenerWrapper.MSG_EXTRACTED_COLORS_CHANGED); 377 Bundle result = new Bundle(); 378 result.putString(LauncherSettings.Settings.EXTRA_VALUE, extractedColors); 379 return result; 380 } 381 case LauncherSettings.Settings.METHOD_CLEAR_EMPTY_DB_FLAG: { 382 clearFlagEmptyDbCreated(); 383 return null; 384 } 385 case LauncherSettings.Settings.METHOD_WAS_EMPTY_DB_CREATED : { 386 Bundle result = new Bundle(); 387 result.putBoolean(LauncherSettings.Settings.EXTRA_VALUE, 388 Utilities.getPrefs(getContext()).getBoolean(EMPTY_DATABASE_CREATED, false)); 389 return result; 390 } 391 case LauncherSettings.Settings.METHOD_DELETE_EMPTY_FOLDERS: { 392 Bundle result = new Bundle(); 393 result.putSerializable(LauncherSettings.Settings.EXTRA_VALUE, deleteEmptyFolders()); 394 return result; 395 } 396 case LauncherSettings.Settings.METHOD_NEW_ITEM_ID: { 397 Bundle result = new Bundle(); 398 result.putLong(LauncherSettings.Settings.EXTRA_VALUE, mOpenHelper.generateNewItemId()); 399 return result; 400 } 401 case LauncherSettings.Settings.METHOD_NEW_SCREEN_ID: { 402 Bundle result = new Bundle(); 403 result.putLong(LauncherSettings.Settings.EXTRA_VALUE, mOpenHelper.generateNewScreenId()); 404 return result; 405 } 406 case LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB: { 407 createEmptyDB(); 408 return null; 409 } 410 case LauncherSettings.Settings.METHOD_LOAD_DEFAULT_FAVORITES: { 411 loadDefaultFavoritesIfNecessary(); 412 return null; 413 } 414 case LauncherSettings.Settings.METHOD_DELETE_DB: { 415 // Are you sure? (y/n) 416 mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase()); 417 return null; 418 } 419 } 420 return null; 421 } 422 423 /** 424 * Deletes any empty folder from the DB. 425 * @return Ids of deleted folders. 426 */ 427 private ArrayList<Long> deleteEmptyFolders() { 428 ArrayList<Long> folderIds = new ArrayList<>(); 429 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 430 db.beginTransaction(); 431 try { 432 // Select folders whose id do not match any container value. 433 String selection = LauncherSettings.Favorites.ITEM_TYPE + " = " 434 + LauncherSettings.Favorites.ITEM_TYPE_FOLDER + " AND " 435 + LauncherSettings.Favorites._ID + " NOT IN (SELECT " + 436 LauncherSettings.Favorites.CONTAINER + " FROM " 437 + Favorites.TABLE_NAME + ")"; 438 Cursor c = db.query(Favorites.TABLE_NAME, 439 new String[] {LauncherSettings.Favorites._ID}, 440 selection, null, null, null, null); 441 while (c.moveToNext()) { 442 folderIds.add(c.getLong(0)); 443 } 444 c.close(); 445 if (!folderIds.isEmpty()) { 446 db.delete(Favorites.TABLE_NAME, Utilities.createDbSelectionQuery( 447 LauncherSettings.Favorites._ID, folderIds), null); 448 } 449 db.setTransactionSuccessful(); 450 } catch (SQLException ex) { 451 Log.e(TAG, ex.getMessage(), ex); 452 folderIds.clear(); 453 } finally { 454 db.endTransaction(); 455 } 456 return folderIds; 457 } 458 459 /** 460 * Overridden in tests 461 */ 462 protected void notifyListeners() { 463 mListenerHandler.sendEmptyMessage(ChangeListenerWrapper.MSG_LAUNCHER_PROVIDER_CHANGED); 464 } 465 466 @Thunk static void addModifiedTime(ContentValues values) { 467 values.put(LauncherSettings.ChangeLogColumns.MODIFIED, System.currentTimeMillis()); 468 } 469 470 /** 471 * Clears all the data for a fresh start. 472 */ 473 synchronized private void createEmptyDB() { 474 mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase()); 475 } 476 477 private void clearFlagEmptyDbCreated() { 478 Utilities.getPrefs(getContext()).edit().remove(EMPTY_DATABASE_CREATED).commit(); 479 } 480 481 /** 482 * Loads the default workspace based on the following priority scheme: 483 * 1) From the app restrictions 484 * 2) From a package provided by play store 485 * 3) From a partner configuration APK, already in the system image 486 * 4) The default configuration for the particular device 487 */ 488 synchronized private void loadDefaultFavoritesIfNecessary() { 489 SharedPreferences sp = Utilities.getPrefs(getContext()); 490 491 if (sp.getBoolean(EMPTY_DATABASE_CREATED, false)) { 492 Log.d(TAG, "loading default workspace"); 493 494 AppWidgetHost widgetHost = new AppWidgetHost(getContext(), Launcher.APPWIDGET_HOST_ID); 495 AutoInstallsLayout loader = createWorkspaceLoaderFromAppRestriction(widgetHost); 496 if (loader == null) { 497 loader = AutoInstallsLayout.get(getContext(),widgetHost, mOpenHelper); 498 } 499 if (loader == null) { 500 final Partner partner = Partner.get(getContext().getPackageManager()); 501 if (partner != null && partner.hasDefaultLayout()) { 502 final Resources partnerRes = partner.getResources(); 503 int workspaceResId = partnerRes.getIdentifier(Partner.RES_DEFAULT_LAYOUT, 504 "xml", partner.getPackageName()); 505 if (workspaceResId != 0) { 506 loader = new DefaultLayoutParser(getContext(), widgetHost, 507 mOpenHelper, partnerRes, workspaceResId); 508 } 509 } 510 } 511 512 final boolean usingExternallyProvidedLayout = loader != null; 513 if (loader == null) { 514 loader = getDefaultLayoutParser(widgetHost); 515 } 516 517 // There might be some partially restored DB items, due to buggy restore logic in 518 // previous versions of launcher. 519 createEmptyDB(); 520 // Populate favorites table with initial favorites 521 if ((mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), loader) <= 0) 522 && usingExternallyProvidedLayout) { 523 // Unable to load external layout. Cleanup and load the internal layout. 524 createEmptyDB(); 525 mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), 526 getDefaultLayoutParser(widgetHost)); 527 } 528 clearFlagEmptyDbCreated(); 529 } 530 } 531 532 /** 533 * Creates workspace loader from an XML resource listed in the app restrictions. 534 * 535 * @return the loader if the restrictions are set and the resource exists; null otherwise. 536 */ 537 @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) 538 private AutoInstallsLayout createWorkspaceLoaderFromAppRestriction(AppWidgetHost widgetHost) { 539 // UserManager.getApplicationRestrictions() requires minSdkVersion >= 18 540 if (!Utilities.ATLEAST_JB_MR2) { 541 return null; 542 } 543 544 Context ctx = getContext(); 545 UserManager um = (UserManager) ctx.getSystemService(Context.USER_SERVICE); 546 Bundle bundle = um.getApplicationRestrictions(ctx.getPackageName()); 547 if (bundle == null) { 548 return null; 549 } 550 551 String packageName = bundle.getString(RESTRICTION_PACKAGE_NAME); 552 if (packageName != null) { 553 try { 554 Resources targetResources = ctx.getPackageManager() 555 .getResourcesForApplication(packageName); 556 return AutoInstallsLayout.get(ctx, packageName, targetResources, 557 widgetHost, mOpenHelper); 558 } catch (NameNotFoundException e) { 559 Log.e(TAG, "Target package for restricted profile not found", e); 560 return null; 561 } 562 } 563 return null; 564 } 565 566 private DefaultLayoutParser getDefaultLayoutParser(AppWidgetHost widgetHost) { 567 int defaultLayout = LauncherAppState.getInstance() 568 .getInvariantDeviceProfile().defaultLayoutId; 569 return new DefaultLayoutParser(getContext(), widgetHost, 570 mOpenHelper, getContext().getResources(), defaultLayout); 571 } 572 573 /** 574 * The class is subclassed in tests to create an in-memory db. 575 */ 576 public static class DatabaseHelper extends SQLiteOpenHelper implements LayoutParserCallback { 577 private final Handler mWidgetHostResetHandler; 578 private final Context mContext; 579 private long mMaxItemId = -1; 580 private long mMaxScreenId = -1; 581 582 DatabaseHelper(Context context, Handler widgetHostResetHandler) { 583 this(context, widgetHostResetHandler, LauncherFiles.LAUNCHER_DB); 584 // Table creation sometimes fails silently, which leads to a crash loop. 585 // This way, we will try to create a table every time after crash, so the device 586 // would eventually be able to recover. 587 if (!tableExists(Favorites.TABLE_NAME) || !tableExists(WorkspaceScreens.TABLE_NAME)) { 588 Log.e(TAG, "Tables are missing after onCreate has been called. Trying to recreate"); 589 // This operation is a no-op if the table already exists. 590 addFavoritesTable(getWritableDatabase(), true); 591 addWorkspacesTable(getWritableDatabase(), true); 592 } 593 594 initIds(); 595 } 596 597 /** 598 * Constructor used in tests and for restore. 599 */ 600 public DatabaseHelper( 601 Context context, Handler widgetHostResetHandler, String tableName) { 602 super(new NoLocaleSqliteContext(context), tableName, null, DATABASE_VERSION); 603 mContext = context; 604 mWidgetHostResetHandler = widgetHostResetHandler; 605 } 606 607 protected void initIds() { 608 // In the case where neither onCreate nor onUpgrade gets called, we read the maxId from 609 // the DB here 610 if (mMaxItemId == -1) { 611 mMaxItemId = initializeMaxItemId(getWritableDatabase()); 612 } 613 if (mMaxScreenId == -1) { 614 mMaxScreenId = initializeMaxScreenId(getWritableDatabase()); 615 } 616 } 617 618 private boolean tableExists(String tableName) { 619 Cursor c = getReadableDatabase().query( 620 true, "sqlite_master", new String[] {"tbl_name"}, 621 "tbl_name = ?", new String[] {tableName}, 622 null, null, null, null, null); 623 try { 624 return c.getCount() > 0; 625 } finally { 626 c.close(); 627 } 628 } 629 630 @Override 631 public void onCreate(SQLiteDatabase db) { 632 if (LOGD) Log.d(TAG, "creating new launcher database"); 633 634 mMaxItemId = 1; 635 mMaxScreenId = 0; 636 637 addFavoritesTable(db, false); 638 addWorkspacesTable(db, false); 639 640 // Fresh and clean launcher DB. 641 mMaxItemId = initializeMaxItemId(db); 642 onEmptyDbCreated(); 643 } 644 645 /** 646 * Overriden in tests. 647 */ 648 protected void onEmptyDbCreated() { 649 // Database was just created, so wipe any previous widgets 650 if (mWidgetHostResetHandler != null) { 651 new AppWidgetHost(mContext, Launcher.APPWIDGET_HOST_ID).deleteHost(); 652 mWidgetHostResetHandler.sendMessage(Message.obtain( 653 mWidgetHostResetHandler, 654 ChangeListenerWrapper.MSG_APP_WIDGET_HOST_RESET, 655 mContext 656 )); 657 } 658 659 // Set the flag for empty DB 660 Utilities.getPrefs(mContext).edit().putBoolean(EMPTY_DATABASE_CREATED, true).commit(); 661 662 // When a new DB is created, remove all previously stored managed profile information. 663 ManagedProfileHeuristic.processAllUsers(Collections.<UserHandleCompat>emptyList(), 664 mContext); 665 } 666 667 public long getDefaultUserSerial() { 668 return UserManagerCompat.getInstance(mContext).getSerialNumberForUser( 669 UserHandleCompat.myUserHandle()); 670 } 671 672 private void addFavoritesTable(SQLiteDatabase db, boolean optional) { 673 Favorites.addTableToDb(db, getDefaultUserSerial(), optional); 674 } 675 676 private void addWorkspacesTable(SQLiteDatabase db, boolean optional) { 677 String ifNotExists = optional ? " IF NOT EXISTS " : ""; 678 db.execSQL("CREATE TABLE " + ifNotExists + WorkspaceScreens.TABLE_NAME + " (" + 679 LauncherSettings.WorkspaceScreens._ID + " INTEGER PRIMARY KEY," + 680 LauncherSettings.WorkspaceScreens.SCREEN_RANK + " INTEGER," + 681 LauncherSettings.ChangeLogColumns.MODIFIED + " INTEGER NOT NULL DEFAULT 0" + 682 ");"); 683 } 684 685 private void removeOrphanedItems(SQLiteDatabase db) { 686 // Delete items directly on the workspace who's screen id doesn't exist 687 // "DELETE FROM favorites WHERE screen NOT IN (SELECT _id FROM workspaceScreens) 688 // AND container = -100" 689 String removeOrphanedDesktopItems = "DELETE FROM " + Favorites.TABLE_NAME + 690 " WHERE " + 691 LauncherSettings.Favorites.SCREEN + " NOT IN (SELECT " + 692 LauncherSettings.WorkspaceScreens._ID + " FROM " + WorkspaceScreens.TABLE_NAME + ")" + 693 " AND " + 694 LauncherSettings.Favorites.CONTAINER + " = " + 695 LauncherSettings.Favorites.CONTAINER_DESKTOP; 696 db.execSQL(removeOrphanedDesktopItems); 697 698 // Delete items contained in folders which no longer exist (after above statement) 699 // "DELETE FROM favorites WHERE container <> -100 AND container <> -101 AND container 700 // NOT IN (SELECT _id FROM favorites WHERE itemType = 2)" 701 String removeOrphanedFolderItems = "DELETE FROM " + Favorites.TABLE_NAME + 702 " WHERE " + 703 LauncherSettings.Favorites.CONTAINER + " <> " + 704 LauncherSettings.Favorites.CONTAINER_DESKTOP + 705 " AND " 706 + LauncherSettings.Favorites.CONTAINER + " <> " + 707 LauncherSettings.Favorites.CONTAINER_HOTSEAT + 708 " AND " 709 + LauncherSettings.Favorites.CONTAINER + " NOT IN (SELECT " + 710 LauncherSettings.Favorites._ID + " FROM " + Favorites.TABLE_NAME + 711 " WHERE " + LauncherSettings.Favorites.ITEM_TYPE + " = " + 712 LauncherSettings.Favorites.ITEM_TYPE_FOLDER + ")"; 713 db.execSQL(removeOrphanedFolderItems); 714 } 715 716 @Override 717 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 718 if (LOGD) Log.d(TAG, "onUpgrade triggered: " + oldVersion); 719 switch (oldVersion) { 720 // The version cannot be lower that 12, as Launcher3 never supported a lower 721 // version of the DB. 722 case 12: { 723 // With the new shrink-wrapped and re-orderable workspaces, it makes sense 724 // to persist workspace screens and their relative order. 725 mMaxScreenId = 0; 726 addWorkspacesTable(db, false); 727 } 728 case 13: { 729 db.beginTransaction(); 730 try { 731 // Insert new column for holding widget provider name 732 db.execSQL("ALTER TABLE favorites " + 733 "ADD COLUMN appWidgetProvider TEXT;"); 734 db.setTransactionSuccessful(); 735 } catch (SQLException ex) { 736 Log.e(TAG, ex.getMessage(), ex); 737 // Old version remains, which means we wipe old data 738 break; 739 } finally { 740 db.endTransaction(); 741 } 742 } 743 case 14: { 744 db.beginTransaction(); 745 try { 746 // Insert new column for holding update timestamp 747 db.execSQL("ALTER TABLE favorites " + 748 "ADD COLUMN modified INTEGER NOT NULL DEFAULT 0;"); 749 db.execSQL("ALTER TABLE workspaceScreens " + 750 "ADD COLUMN modified INTEGER NOT NULL DEFAULT 0;"); 751 db.setTransactionSuccessful(); 752 } catch (SQLException ex) { 753 Log.e(TAG, ex.getMessage(), ex); 754 // Old version remains, which means we wipe old data 755 break; 756 } finally { 757 db.endTransaction(); 758 } 759 } 760 case 15: { 761 if (!addIntegerColumn(db, Favorites.RESTORED, 0)) { 762 // Old version remains, which means we wipe old data 763 break; 764 } 765 } 766 case 16: { 767 // We use the db version upgrade here to identify users who may not have seen 768 // clings yet (because they weren't available), but for whom the clings are now 769 // available (tablet users). Because one of the possible cling flows (migration) 770 // is very destructive (wipes out workspaces), we want to prevent this from showing 771 // until clear data. We do so by marking that the clings have been shown. 772 LauncherClings.markFirstRunClingDismissed(mContext); 773 } 774 case 17: { 775 // No-op 776 } 777 case 18: { 778 // Due to a data loss bug, some users may have items associated with screen ids 779 // which no longer exist. Since this can cause other problems, and since the user 780 // will never see these items anyway, we use database upgrade as an opportunity to 781 // clean things up. 782 removeOrphanedItems(db); 783 } 784 case 19: { 785 // Add userId column 786 if (!addProfileColumn(db)) { 787 // Old version remains, which means we wipe old data 788 break; 789 } 790 } 791 case 20: 792 if (!updateFolderItemsRank(db, true)) { 793 break; 794 } 795 case 21: 796 // Recreate workspace table with screen id a primary key 797 if (!recreateWorkspaceTable(db)) { 798 break; 799 } 800 case 22: { 801 if (!addIntegerColumn(db, Favorites.OPTIONS, 0)) { 802 // Old version remains, which means we wipe old data 803 break; 804 } 805 } 806 case 23: 807 // No-op 808 case 24: 809 ManagedProfileHeuristic.markExistingUsersForNoFolderCreation(mContext); 810 case 25: 811 convertShortcutsToLauncherActivities(db); 812 case 26: 813 // QSB was moved to the grid. Clear the first row on screen 0. 814 if (FeatureFlags.QSB_ON_FIRST_SCREEN && 815 !LauncherDbUtils.prepareScreenZeroToHostQsb(db)) { 816 break; 817 } 818 case 27: { 819 // DB Upgraded successfully 820 return; 821 } 822 } 823 824 // DB was not upgraded 825 Log.w(TAG, "Destroying all old data."); 826 createEmptyDB(db); 827 } 828 829 @Override 830 public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { 831 // This shouldn't happen -- throw our hands up in the air and start over. 832 Log.w(TAG, "Database version downgrade from: " + oldVersion + " to " + newVersion + 833 ". Wiping databse."); 834 createEmptyDB(db); 835 } 836 837 /** 838 * Clears all the data for a fresh start. 839 */ 840 public void createEmptyDB(SQLiteDatabase db) { 841 db.execSQL("DROP TABLE IF EXISTS " + Favorites.TABLE_NAME); 842 db.execSQL("DROP TABLE IF EXISTS " + WorkspaceScreens.TABLE_NAME); 843 onCreate(db); 844 } 845 846 /** 847 * Replaces all shortcuts of type {@link Favorites#ITEM_TYPE_SHORTCUT} which have a valid 848 * launcher activity target with {@link Favorites#ITEM_TYPE_APPLICATION}. 849 */ 850 @Thunk void convertShortcutsToLauncherActivities(SQLiteDatabase db) { 851 db.beginTransaction(); 852 Cursor c = null; 853 SQLiteStatement updateStmt = null; 854 855 try { 856 // Only consider the primary user as other users can't have a shortcut. 857 long userSerial = getDefaultUserSerial(); 858 c = db.query(Favorites.TABLE_NAME, new String[] { 859 Favorites._ID, 860 Favorites.INTENT, 861 }, "itemType=" + Favorites.ITEM_TYPE_SHORTCUT + " AND profileId=" + userSerial, 862 null, null, null, null); 863 864 updateStmt = db.compileStatement("UPDATE favorites SET itemType=" 865 + Favorites.ITEM_TYPE_APPLICATION + " WHERE _id=?"); 866 867 final int idIndex = c.getColumnIndexOrThrow(Favorites._ID); 868 final int intentIndex = c.getColumnIndexOrThrow(Favorites.INTENT); 869 870 while (c.moveToNext()) { 871 String intentDescription = c.getString(intentIndex); 872 Intent intent; 873 try { 874 intent = Intent.parseUri(intentDescription, 0); 875 } catch (URISyntaxException e) { 876 Log.e(TAG, "Unable to parse intent", e); 877 continue; 878 } 879 880 if (!Utilities.isLauncherAppTarget(intent)) { 881 continue; 882 } 883 884 long id = c.getLong(idIndex); 885 updateStmt.bindLong(1, id); 886 updateStmt.executeUpdateDelete(); 887 } 888 db.setTransactionSuccessful(); 889 } catch (SQLException ex) { 890 Log.w(TAG, "Error deduping shortcuts", ex); 891 } finally { 892 db.endTransaction(); 893 if (c != null) { 894 c.close(); 895 } 896 if (updateStmt != null) { 897 updateStmt.close(); 898 } 899 } 900 } 901 902 /** 903 * Recreates workspace table and migrates data to the new table. 904 */ 905 public boolean recreateWorkspaceTable(SQLiteDatabase db) { 906 db.beginTransaction(); 907 try { 908 Cursor c = db.query(WorkspaceScreens.TABLE_NAME, 909 new String[] {LauncherSettings.WorkspaceScreens._ID}, 910 null, null, null, null, 911 LauncherSettings.WorkspaceScreens.SCREEN_RANK); 912 ArrayList<Long> sortedIDs = new ArrayList<Long>(); 913 long maxId = 0; 914 try { 915 while (c.moveToNext()) { 916 Long id = c.getLong(0); 917 if (!sortedIDs.contains(id)) { 918 sortedIDs.add(id); 919 maxId = Math.max(maxId, id); 920 } 921 } 922 } finally { 923 c.close(); 924 } 925 926 db.execSQL("DROP TABLE IF EXISTS " + WorkspaceScreens.TABLE_NAME); 927 addWorkspacesTable(db, false); 928 929 // Add all screen ids back 930 int total = sortedIDs.size(); 931 for (int i = 0; i < total; i++) { 932 ContentValues values = new ContentValues(); 933 values.put(LauncherSettings.WorkspaceScreens._ID, sortedIDs.get(i)); 934 values.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, i); 935 addModifiedTime(values); 936 db.insertOrThrow(WorkspaceScreens.TABLE_NAME, null, values); 937 } 938 db.setTransactionSuccessful(); 939 mMaxScreenId = maxId; 940 } catch (SQLException ex) { 941 // Old version remains, which means we wipe old data 942 Log.e(TAG, ex.getMessage(), ex); 943 return false; 944 } finally { 945 db.endTransaction(); 946 } 947 return true; 948 } 949 950 @Thunk boolean updateFolderItemsRank(SQLiteDatabase db, boolean addRankColumn) { 951 db.beginTransaction(); 952 try { 953 if (addRankColumn) { 954 // Insert new column for holding rank 955 db.execSQL("ALTER TABLE favorites ADD COLUMN rank INTEGER NOT NULL DEFAULT 0;"); 956 } 957 958 // Get a map for folder ID to folder width 959 Cursor c = db.rawQuery("SELECT container, MAX(cellX) FROM favorites" 960 + " WHERE container IN (SELECT _id FROM favorites WHERE itemType = ?)" 961 + " GROUP BY container;", 962 new String[] {Integer.toString(LauncherSettings.Favorites.ITEM_TYPE_FOLDER)}); 963 964 while (c.moveToNext()) { 965 db.execSQL("UPDATE favorites SET rank=cellX+(cellY*?) WHERE " 966 + "container=? AND cellX IS NOT NULL AND cellY IS NOT NULL;", 967 new Object[] {c.getLong(1) + 1, c.getLong(0)}); 968 } 969 970 c.close(); 971 db.setTransactionSuccessful(); 972 } catch (SQLException ex) { 973 // Old version remains, which means we wipe old data 974 Log.e(TAG, ex.getMessage(), ex); 975 return false; 976 } finally { 977 db.endTransaction(); 978 } 979 return true; 980 } 981 982 private boolean addProfileColumn(SQLiteDatabase db) { 983 return addIntegerColumn(db, Favorites.PROFILE_ID, getDefaultUserSerial()); 984 } 985 986 private boolean addIntegerColumn(SQLiteDatabase db, String columnName, long defaultValue) { 987 db.beginTransaction(); 988 try { 989 db.execSQL("ALTER TABLE favorites ADD COLUMN " 990 + columnName + " INTEGER NOT NULL DEFAULT " + defaultValue + ";"); 991 db.setTransactionSuccessful(); 992 } catch (SQLException ex) { 993 Log.e(TAG, ex.getMessage(), ex); 994 return false; 995 } finally { 996 db.endTransaction(); 997 } 998 return true; 999 } 1000 1001 // Generates a new ID to use for an object in your database. This method should be only 1002 // called from the main UI thread. As an exception, we do call it when we call the 1003 // constructor from the worker thread; however, this doesn't extend until after the 1004 // constructor is called, and we only pass a reference to LauncherProvider to LauncherApp 1005 // after that point 1006 @Override 1007 public long generateNewItemId() { 1008 if (mMaxItemId < 0) { 1009 throw new RuntimeException("Error: max item id was not initialized"); 1010 } 1011 mMaxItemId += 1; 1012 return mMaxItemId; 1013 } 1014 1015 @Override 1016 public long insertAndCheck(SQLiteDatabase db, ContentValues values) { 1017 return dbInsertAndCheck(this, db, Favorites.TABLE_NAME, null, values); 1018 } 1019 1020 public void checkId(String table, ContentValues values) { 1021 long id = values.getAsLong(LauncherSettings.BaseLauncherColumns._ID); 1022 if (table == WorkspaceScreens.TABLE_NAME) { 1023 mMaxScreenId = Math.max(id, mMaxScreenId); 1024 } else { 1025 mMaxItemId = Math.max(id, mMaxItemId); 1026 } 1027 } 1028 1029 private long initializeMaxItemId(SQLiteDatabase db) { 1030 return getMaxId(db, Favorites.TABLE_NAME); 1031 } 1032 1033 // Generates a new ID to use for an workspace screen in your database. This method 1034 // should be only called from the main UI thread. As an exception, we do call it when we 1035 // call the constructor from the worker thread; however, this doesn't extend until after the 1036 // constructor is called, and we only pass a reference to LauncherProvider to LauncherApp 1037 // after that point 1038 public long generateNewScreenId() { 1039 if (mMaxScreenId < 0) { 1040 throw new RuntimeException("Error: max screen id was not initialized"); 1041 } 1042 mMaxScreenId += 1; 1043 return mMaxScreenId; 1044 } 1045 1046 private long initializeMaxScreenId(SQLiteDatabase db) { 1047 return getMaxId(db, WorkspaceScreens.TABLE_NAME); 1048 } 1049 1050 @Thunk int loadFavorites(SQLiteDatabase db, AutoInstallsLayout loader) { 1051 ArrayList<Long> screenIds = new ArrayList<Long>(); 1052 // TODO: Use multiple loaders with fall-back and transaction. 1053 int count = loader.loadLayout(db, screenIds); 1054 1055 // Add the screens specified by the items above 1056 Collections.sort(screenIds); 1057 int rank = 0; 1058 ContentValues values = new ContentValues(); 1059 for (Long id : screenIds) { 1060 values.clear(); 1061 values.put(LauncherSettings.WorkspaceScreens._ID, id); 1062 values.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, rank); 1063 if (dbInsertAndCheck(this, db, WorkspaceScreens.TABLE_NAME, null, values) < 0) { 1064 throw new RuntimeException("Failed initialize screen table" 1065 + "from default layout"); 1066 } 1067 rank++; 1068 } 1069 1070 // Ensure that the max ids are initialized 1071 mMaxItemId = initializeMaxItemId(db); 1072 mMaxScreenId = initializeMaxScreenId(db); 1073 1074 return count; 1075 } 1076 } 1077 1078 /** 1079 * @return the max _id in the provided table. 1080 */ 1081 @Thunk static long getMaxId(SQLiteDatabase db, String table) { 1082 Cursor c = db.rawQuery("SELECT MAX(_id) FROM " + table, null); 1083 // get the result 1084 long id = -1; 1085 if (c != null && c.moveToNext()) { 1086 id = c.getLong(0); 1087 } 1088 if (c != null) { 1089 c.close(); 1090 } 1091 1092 if (id == -1) { 1093 throw new RuntimeException("Error: could not query max id in " + table); 1094 } 1095 1096 return id; 1097 } 1098 1099 static class SqlArguments { 1100 public final String table; 1101 public final String where; 1102 public final String[] args; 1103 1104 SqlArguments(Uri url, String where, String[] args) { 1105 if (url.getPathSegments().size() == 1) { 1106 this.table = url.getPathSegments().get(0); 1107 this.where = where; 1108 this.args = args; 1109 } else if (url.getPathSegments().size() != 2) { 1110 throw new IllegalArgumentException("Invalid URI: " + url); 1111 } else if (!TextUtils.isEmpty(where)) { 1112 throw new UnsupportedOperationException("WHERE clause not supported: " + url); 1113 } else { 1114 this.table = url.getPathSegments().get(0); 1115 this.where = "_id=" + ContentUris.parseId(url); 1116 this.args = null; 1117 } 1118 } 1119 1120 SqlArguments(Uri url) { 1121 if (url.getPathSegments().size() == 1) { 1122 table = url.getPathSegments().get(0); 1123 where = null; 1124 args = null; 1125 } else { 1126 throw new IllegalArgumentException("Invalid URI: " + url); 1127 } 1128 } 1129 } 1130 1131 private static class ChangeListenerWrapper implements Handler.Callback { 1132 1133 private static final int MSG_LAUNCHER_PROVIDER_CHANGED = 1; 1134 private static final int MSG_EXTRACTED_COLORS_CHANGED = 2; 1135 private static final int MSG_APP_WIDGET_HOST_RESET = 3; 1136 1137 private LauncherProviderChangeListener mListener; 1138 1139 @Override 1140 public boolean handleMessage(Message msg) { 1141 if (mListener != null) { 1142 switch (msg.what) { 1143 case MSG_LAUNCHER_PROVIDER_CHANGED: 1144 mListener.onLauncherProviderChange(); 1145 break; 1146 case MSG_EXTRACTED_COLORS_CHANGED: 1147 mListener.onExtractedColorsChanged(); 1148 break; 1149 case MSG_APP_WIDGET_HOST_RESET: 1150 Context context = (Context) msg.obj; 1151 if (context != null) { 1152 context.sendBroadcast(new Intent(Launcher.ACTION_APPWIDGET_HOST_RESET) 1153 .setPackage(context.getPackageName())); 1154 } 1155 break; 1156 } 1157 } 1158 return true; 1159 } 1160 } 1161 } 1162