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