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