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