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