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