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