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