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