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