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