2 * Copyright (C) 2008 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.launcher3;
19 import android.appwidget.AppWidgetHost;
20 import android.appwidget.AppWidgetManager;
21 import android.content.ComponentName;
22 import android.content.ContentProvider;
23 import android.content.ContentProviderOperation;
24 import android.content.ContentProviderResult;
25 import android.content.ContentUris;
26 import android.content.ContentValues;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.content.OperationApplicationException;
30 import android.content.SharedPreferences;
31 import android.content.pm.PackageManager.NameNotFoundException;
32 import android.content.res.Resources;
33 import android.database.Cursor;
34 import android.database.SQLException;
35 import android.database.sqlite.SQLiteDatabase;
36 import android.database.sqlite.SQLiteOpenHelper;
37 import android.database.sqlite.SQLiteQueryBuilder;
38 import android.database.sqlite.SQLiteStatement;
39 import android.net.Uri;
40 import android.os.Binder;
41 import android.os.Bundle;
42 import android.os.Handler;
43 import android.os.Message;
44 import android.os.Process;
45 import android.os.Trace;
46 import android.os.UserHandle;
47 import android.os.UserManager;
48 import android.text.TextUtils;
49 import android.util.Log;
51 import com.android.launcher3.AutoInstallsLayout.LayoutParserCallback;
52 import com.android.launcher3.LauncherSettings.Favorites;
53 import com.android.launcher3.LauncherSettings.WorkspaceScreens;
54 import com.android.launcher3.compat.UserManagerCompat;
55 import com.android.launcher3.config.FeatureFlags;
56 import com.android.launcher3.config.ProviderConfig;
57 import com.android.launcher3.dynamicui.ExtractionUtils;
58 import com.android.launcher3.graphics.IconShapeOverride;
59 import com.android.launcher3.logging.FileLog;
60 import com.android.launcher3.provider.LauncherDbUtils;
61 import com.android.launcher3.provider.RestoreDbTask;
62 import com.android.launcher3.util.ManagedProfileHeuristic;
63 import com.android.launcher3.util.NoLocaleSqliteContext;
64 import com.android.launcher3.util.Preconditions;
65 import com.android.launcher3.util.Thunk;
67 import java.io.FileDescriptor;
68 import java.io.PrintWriter;
69 import java.lang.reflect.Method;
70 import java.net.URISyntaxException;
71 import java.util.ArrayList;
72 import java.util.Collections;
73 import java.util.HashSet;
75 public class LauncherProvider extends ContentProvider {
76 private static final String TAG = "LauncherProvider";
77 private static final boolean LOGD = false;
80 * Represents the schema of the database. Changes in scheme need not be backwards compatible.
82 private static final int SCHEMA_VERSION = 27;
84 * Represents the actual data. It could include additional validations and normalizations added
85 * overtime. These must be backwards compatible, else we risk breaking old devices during
86 * restore or binary version downgrade.
88 private static final int DATA_VERSION = 3;
90 private static final String PREF_KEY_DATA_VERISON = "provider_data_version";
92 public static final String AUTHORITY = ProviderConfig.AUTHORITY;
94 static final String EMPTY_DATABASE_CREATED = "EMPTY_DATABASE_CREATED";
96 private static final String RESTRICTION_PACKAGE_NAME = "workspace.configuration.package.name";
98 private final ChangeListenerWrapper mListenerWrapper = new ChangeListenerWrapper();
99 private Handler mListenerHandler;
101 protected DatabaseHelper mOpenHelper;
104 * $ adb shell dumpsys activity provider com.android.launcher3
107 public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
108 LauncherAppState appState = LauncherAppState.getInstanceNoCreate();
109 if (appState == null || !appState.getModel().isModelLoaded()) {
112 appState.getModel().dumpState("", fd, writer, args);
116 public boolean onCreate() {
117 if (ProviderConfig.IS_DOGFOOD_BUILD) {
118 Log.d(TAG, "Launcher process started");
120 mListenerHandler = new Handler(mListenerWrapper);
122 // The content provider exists for the entire duration of the launcher main process and
123 // is the first component to get created. Initializing FileLog here ensures that it's
124 // always available in the main process.
125 FileLog.setDir(getContext().getApplicationContext().getFilesDir());
126 IconShapeOverride.apply(getContext());
131 * Sets a provider listener.
133 public void setLauncherProviderChangeListener(LauncherProviderChangeListener listener) {
134 Preconditions.assertUIThread();
135 mListenerWrapper.mListener = listener;
139 public String getType(Uri uri) {
140 SqlArguments args = new SqlArguments(uri, null, null);
141 if (TextUtils.isEmpty(args.where)) {
142 return "vnd.android.cursor.dir/" + args.table;
144 return "vnd.android.cursor.item/" + args.table;
149 * Overridden in tests
151 protected synchronized void createDbIfNotExists() {
152 if (mOpenHelper == null) {
153 if (LauncherAppState.PROFILE_STARTUP) {
154 Trace.beginSection("Opening workspace DB");
156 mOpenHelper = new DatabaseHelper(getContext(), mListenerHandler);
158 if (RestoreDbTask.isPending(getContext())) {
159 if (!RestoreDbTask.performRestore(mOpenHelper)) {
160 mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
162 // Set is pending to false irrespective of the result, so that it doesn't get
164 RestoreDbTask.setPending(getContext(), false);
167 if (LauncherAppState.PROFILE_STARTUP) {
174 public Cursor query(Uri uri, String[] projection, String selection,
175 String[] selectionArgs, String sortOrder) {
176 createDbIfNotExists();
178 SqlArguments args = new SqlArguments(uri, selection, selectionArgs);
179 SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
180 qb.setTables(args.table);
182 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
183 Cursor result = qb.query(db, projection, args.where, args.args, null, null, sortOrder);
184 result.setNotificationUri(getContext().getContentResolver(), uri);
189 @Thunk static long dbInsertAndCheck(DatabaseHelper helper,
190 SQLiteDatabase db, String table, String nullColumnHack, ContentValues values) {
191 if (values == null) {
192 throw new RuntimeException("Error: attempting to insert null values");
194 if (!values.containsKey(LauncherSettings.ChangeLogColumns._ID)) {
195 throw new RuntimeException("Error: attempting to add item without specifying an id");
197 helper.checkId(table, values);
198 return db.insert(table, nullColumnHack, values);
201 private void reloadLauncherIfExternal() {
202 if (Utilities.ATLEAST_MARSHMALLOW && Binder.getCallingPid() != Process.myPid()) {
203 LauncherAppState app = LauncherAppState.getInstanceNoCreate();
205 app.getModel().forceReload();
211 public Uri insert(Uri uri, ContentValues initialValues) {
212 createDbIfNotExists();
213 SqlArguments args = new SqlArguments(uri);
215 // In very limited cases, we support system|signature permission apps to modify the db.
216 if (Binder.getCallingPid() != Process.myPid()) {
217 if (!initializeExternalAdd(initialValues)) {
222 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
223 addModifiedTime(initialValues);
224 final long rowId = dbInsertAndCheck(mOpenHelper, db, args.table, null, initialValues);
225 if (rowId < 0) return null;
227 uri = ContentUris.withAppendedId(uri, rowId);
230 if (Utilities.ATLEAST_MARSHMALLOW) {
231 reloadLauncherIfExternal();
233 // Deprecated behavior to support legacy devices which rely on provider callbacks.
234 LauncherAppState app = LauncherAppState.getInstanceNoCreate();
235 if (app != null && "true".equals(uri.getQueryParameter("isExternalAdd"))) {
236 app.getModel().forceReload();
239 String notify = uri.getQueryParameter("notify");
240 if (notify == null || "true".equals(notify)) {
241 getContext().getContentResolver().notifyChange(uri, null);
247 private boolean initializeExternalAdd(ContentValues values) {
248 // 1. Ensure that externally added items have a valid item id
249 long id = mOpenHelper.generateNewItemId();
250 values.put(LauncherSettings.Favorites._ID, id);
252 // 2. In the case of an app widget, and if no app widget id is specified, we
253 // attempt allocate and bind the widget.
254 Integer itemType = values.getAsInteger(LauncherSettings.Favorites.ITEM_TYPE);
255 if (itemType != null &&
256 itemType.intValue() == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET &&
257 !values.containsKey(LauncherSettings.Favorites.APPWIDGET_ID)) {
259 final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(getContext());
260 ComponentName cn = ComponentName.unflattenFromString(
261 values.getAsString(Favorites.APPWIDGET_PROVIDER));
265 AppWidgetHost widgetHost = mOpenHelper.newLauncherWidgetHost();
266 int appWidgetId = widgetHost.allocateAppWidgetId();
267 values.put(LauncherSettings.Favorites.APPWIDGET_ID, appWidgetId);
268 if (!appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId,cn)) {
269 widgetHost.deleteAppWidgetId(appWidgetId);
272 } catch (RuntimeException e) {
273 Log.e(TAG, "Failed to initialize external widget", e);
281 // Add screen id if not present
282 long screenId = values.getAsLong(LauncherSettings.Favorites.SCREEN);
283 SQLiteStatement stmp = null;
285 stmp = mOpenHelper.getWritableDatabase().compileStatement(
286 "INSERT OR IGNORE INTO workspaceScreens (_id, screenRank) " +
287 "select ?, (ifnull(MAX(screenRank), -1)+1) from workspaceScreens");
288 stmp.bindLong(1, screenId);
290 ContentValues valuesInserted = new ContentValues();
291 valuesInserted.put(LauncherSettings.BaseLauncherColumns._ID, stmp.executeInsert());
292 mOpenHelper.checkId(WorkspaceScreens.TABLE_NAME, valuesInserted);
294 } catch (Exception e) {
297 Utilities.closeSilently(stmp);
302 public int bulkInsert(Uri uri, ContentValues[] values) {
303 createDbIfNotExists();
304 SqlArguments args = new SqlArguments(uri);
306 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
307 db.beginTransaction();
309 int numValues = values.length;
310 for (int i = 0; i < numValues; i++) {
311 addModifiedTime(values[i]);
312 if (dbInsertAndCheck(mOpenHelper, db, args.table, null, values[i]) < 0) {
316 db.setTransactionSuccessful();
322 reloadLauncherIfExternal();
323 return values.length;
327 public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
328 throws OperationApplicationException {
329 createDbIfNotExists();
330 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
331 db.beginTransaction();
333 ContentProviderResult[] result = super.applyBatch(operations);
334 db.setTransactionSuccessful();
335 reloadLauncherIfExternal();
343 public int delete(Uri uri, String selection, String[] selectionArgs) {
344 createDbIfNotExists();
345 SqlArguments args = new SqlArguments(uri, selection, selectionArgs);
347 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
349 if (Binder.getCallingPid() != Process.myPid()
350 && Favorites.TABLE_NAME.equalsIgnoreCase(args.table)) {
351 mOpenHelper.removeGhostWidgets(mOpenHelper.getWritableDatabase());
353 int count = db.delete(args.table, args.where, args.args);
356 reloadLauncherIfExternal();
362 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
363 createDbIfNotExists();
364 SqlArguments args = new SqlArguments(uri, selection, selectionArgs);
366 addModifiedTime(values);
367 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
368 int count = db.update(args.table, values, args.where, args.args);
369 if (count > 0) notifyListeners();
371 reloadLauncherIfExternal();
376 public Bundle call(String method, final String arg, final Bundle extras) {
377 if (Binder.getCallingUid() != Process.myUid()) {
380 createDbIfNotExists();
383 case LauncherSettings.Settings.METHOD_SET_EXTRACTED_COLORS_AND_WALLPAPER_ID: {
384 String extractedColors = extras.getString(
385 LauncherSettings.Settings.EXTRA_EXTRACTED_COLORS);
386 int wallpaperId = extras.getInt(LauncherSettings.Settings.EXTRA_WALLPAPER_ID);
387 Utilities.getPrefs(getContext()).edit()
388 .putString(ExtractionUtils.EXTRACTED_COLORS_PREFERENCE_KEY, extractedColors)
389 .putInt(ExtractionUtils.WALLPAPER_ID_PREFERENCE_KEY, wallpaperId)
391 mListenerHandler.sendEmptyMessage(ChangeListenerWrapper.MSG_EXTRACTED_COLORS_CHANGED);
392 Bundle result = new Bundle();
393 result.putString(LauncherSettings.Settings.EXTRA_VALUE, extractedColors);
396 case LauncherSettings.Settings.METHOD_CLEAR_EMPTY_DB_FLAG: {
397 clearFlagEmptyDbCreated();
400 case LauncherSettings.Settings.METHOD_WAS_EMPTY_DB_CREATED : {
401 Bundle result = new Bundle();
402 result.putBoolean(LauncherSettings.Settings.EXTRA_VALUE,
403 Utilities.getPrefs(getContext()).getBoolean(EMPTY_DATABASE_CREATED, false));
406 case LauncherSettings.Settings.METHOD_DELETE_EMPTY_FOLDERS: {
407 Bundle result = new Bundle();
408 result.putSerializable(LauncherSettings.Settings.EXTRA_VALUE, deleteEmptyFolders());
411 case LauncherSettings.Settings.METHOD_NEW_ITEM_ID: {
412 Bundle result = new Bundle();
413 result.putLong(LauncherSettings.Settings.EXTRA_VALUE, mOpenHelper.generateNewItemId());
416 case LauncherSettings.Settings.METHOD_NEW_SCREEN_ID: {
417 Bundle result = new Bundle();
418 result.putLong(LauncherSettings.Settings.EXTRA_VALUE, mOpenHelper.generateNewScreenId());
421 case LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB: {
422 mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
425 case LauncherSettings.Settings.METHOD_LOAD_DEFAULT_FAVORITES: {
426 loadDefaultFavoritesIfNecessary();
429 case LauncherSettings.Settings.METHOD_REMOVE_GHOST_WIDGETS: {
430 mOpenHelper.removeGhostWidgets(mOpenHelper.getWritableDatabase());
438 * Deletes any empty folder from the DB.
439 * @return Ids of deleted folders.
441 private ArrayList<Long> deleteEmptyFolders() {
442 ArrayList<Long> folderIds = new ArrayList<>();
443 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
444 db.beginTransaction();
446 // Select folders whose id do not match any container value.
447 String selection = LauncherSettings.Favorites.ITEM_TYPE + " = "
448 + LauncherSettings.Favorites.ITEM_TYPE_FOLDER + " AND "
449 + LauncherSettings.Favorites._ID + " NOT IN (SELECT " +
450 LauncherSettings.Favorites.CONTAINER + " FROM "
451 + Favorites.TABLE_NAME + ")";
452 Cursor c = db.query(Favorites.TABLE_NAME,
453 new String[] {LauncherSettings.Favorites._ID},
454 selection, null, null, null, null);
455 while (c.moveToNext()) {
456 folderIds.add(c.getLong(0));
459 if (!folderIds.isEmpty()) {
460 db.delete(Favorites.TABLE_NAME, Utilities.createDbSelectionQuery(
461 LauncherSettings.Favorites._ID, folderIds), null);
463 db.setTransactionSuccessful();
464 } catch (SQLException ex) {
465 Log.e(TAG, ex.getMessage(), ex);
474 * Overridden in tests
476 protected void notifyListeners() {
477 mListenerHandler.sendEmptyMessage(ChangeListenerWrapper.MSG_LAUNCHER_PROVIDER_CHANGED);
480 @Thunk static void addModifiedTime(ContentValues values) {
481 values.put(LauncherSettings.ChangeLogColumns.MODIFIED, System.currentTimeMillis());
484 private void clearFlagEmptyDbCreated() {
485 Utilities.getPrefs(getContext()).edit().remove(EMPTY_DATABASE_CREATED).commit();
489 * Loads the default workspace based on the following priority scheme:
490 * 1) From the app restrictions
491 * 2) From a package provided by play store
492 * 3) From a partner configuration APK, already in the system image
493 * 4) The default configuration for the particular device
495 synchronized private void loadDefaultFavoritesIfNecessary() {
496 SharedPreferences sp = Utilities.getPrefs(getContext());
498 if (sp.getBoolean(EMPTY_DATABASE_CREATED, false)) {
499 Log.d(TAG, "loading default workspace");
501 AppWidgetHost widgetHost = mOpenHelper.newLauncherWidgetHost();
502 AutoInstallsLayout loader = createWorkspaceLoaderFromAppRestriction(widgetHost);
503 if (loader == null) {
504 loader = AutoInstallsLayout.get(getContext(),widgetHost, mOpenHelper);
506 if (loader == null) {
507 final Partner partner = Partner.get(getContext().getPackageManager());
508 if (partner != null && partner.hasDefaultLayout()) {
509 final Resources partnerRes = partner.getResources();
510 int workspaceResId = partnerRes.getIdentifier(Partner.RES_DEFAULT_LAYOUT,
511 "xml", partner.getPackageName());
512 if (workspaceResId != 0) {
513 loader = new DefaultLayoutParser(getContext(), widgetHost,
514 mOpenHelper, partnerRes, workspaceResId);
519 final boolean usingExternallyProvidedLayout = loader != null;
520 if (loader == null) {
521 loader = getDefaultLayoutParser(widgetHost);
524 // There might be some partially restored DB items, due to buggy restore logic in
525 // previous versions of launcher.
526 mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
527 // Populate favorites table with initial favorites
528 if ((mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), loader) <= 0)
529 && usingExternallyProvidedLayout) {
530 // Unable to load external layout. Cleanup and load the internal layout.
531 mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
532 mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(),
533 getDefaultLayoutParser(widgetHost));
535 clearFlagEmptyDbCreated();
540 * Creates workspace loader from an XML resource listed in the app restrictions.
542 * @return the loader if the restrictions are set and the resource exists; null otherwise.
544 private AutoInstallsLayout createWorkspaceLoaderFromAppRestriction(AppWidgetHost widgetHost) {
545 Context ctx = getContext();
546 UserManager um = (UserManager) ctx.getSystemService(Context.USER_SERVICE);
547 Bundle bundle = um.getApplicationRestrictions(ctx.getPackageName());
548 if (bundle == null) {
552 String packageName = bundle.getString(RESTRICTION_PACKAGE_NAME);
553 if (packageName != null) {
555 Resources targetResources = ctx.getPackageManager()
556 .getResourcesForApplication(packageName);
557 return AutoInstallsLayout.get(ctx, packageName, targetResources,
558 widgetHost, mOpenHelper);
559 } catch (NameNotFoundException e) {
560 Log.e(TAG, "Target package for restricted profile not found", e);
567 private DefaultLayoutParser getDefaultLayoutParser(AppWidgetHost widgetHost) {
568 int defaultLayout = LauncherAppState.getIDP(getContext()).defaultLayoutId;
569 return new DefaultLayoutParser(getContext(), widgetHost,
570 mOpenHelper, getContext().getResources(), defaultLayout);
574 * The class is subclassed in tests to create an in-memory db.
576 public static class DatabaseHelper extends SQLiteOpenHelper implements LayoutParserCallback {
577 private final Handler mWidgetHostResetHandler;
578 private final Context mContext;
579 private long mMaxItemId = -1;
580 private long mMaxScreenId = -1;
582 DatabaseHelper(Context context, Handler widgetHostResetHandler) {
583 this(context, widgetHostResetHandler, LauncherFiles.LAUNCHER_DB);
584 // Table creation sometimes fails silently, which leads to a crash loop.
585 // This way, we will try to create a table every time after crash, so the device
586 // would eventually be able to recover.
587 if (!tableExists(Favorites.TABLE_NAME) || !tableExists(WorkspaceScreens.TABLE_NAME)) {
588 Log.e(TAG, "Tables are missing after onCreate has been called. Trying to recreate");
589 // This operation is a no-op if the table already exists.
590 addFavoritesTable(getWritableDatabase(), true);
591 addWorkspacesTable(getWritableDatabase(), true);
598 * Constructor used in tests and for restore.
600 public DatabaseHelper(
601 Context context, Handler widgetHostResetHandler, String tableName) {
602 super(new NoLocaleSqliteContext(context), tableName, null, SCHEMA_VERSION);
604 mWidgetHostResetHandler = widgetHostResetHandler;
607 protected void initIds() {
608 // In the case where neither onCreate nor onUpgrade gets called, we read the maxId from
610 if (mMaxItemId == -1) {
611 mMaxItemId = initializeMaxItemId(getWritableDatabase());
613 if (mMaxScreenId == -1) {
614 mMaxScreenId = initializeMaxScreenId(getWritableDatabase());
618 private boolean tableExists(String tableName) {
619 Cursor c = getReadableDatabase().query(
620 true, "sqlite_master", new String[] {"tbl_name"},
621 "tbl_name = ?", new String[] {tableName},
622 null, null, null, null, null);
624 return c.getCount() > 0;
631 public void onCreate(SQLiteDatabase db) {
632 if (LOGD) Log.d(TAG, "creating new launcher database");
637 addFavoritesTable(db, false);
638 addWorkspacesTable(db, false);
640 // Fresh and clean launcher DB.
641 mMaxItemId = initializeMaxItemId(db);
646 * Overriden in tests.
648 protected void onEmptyDbCreated() {
649 // Database was just created, so wipe any previous widgets
650 if (mWidgetHostResetHandler != null) {
651 newLauncherWidgetHost().deleteHost();
652 mWidgetHostResetHandler.sendEmptyMessage(
653 ChangeListenerWrapper.MSG_APP_WIDGET_HOST_RESET);
656 // Set the flag for empty DB
657 Utilities.getPrefs(mContext).edit().putBoolean(EMPTY_DATABASE_CREATED, true).commit();
659 // When a new DB is created, remove all previously stored managed profile information.
660 ManagedProfileHeuristic.processAllUsers(Collections.<UserHandle>emptyList(),
664 public long getDefaultUserSerial() {
665 return UserManagerCompat.getInstance(mContext).getSerialNumberForUser(
666 Process.myUserHandle());
669 private void addFavoritesTable(SQLiteDatabase db, boolean optional) {
670 Favorites.addTableToDb(db, getDefaultUserSerial(), optional);
673 private void addWorkspacesTable(SQLiteDatabase db, boolean optional) {
674 String ifNotExists = optional ? " IF NOT EXISTS " : "";
675 db.execSQL("CREATE TABLE " + ifNotExists + WorkspaceScreens.TABLE_NAME + " (" +
676 LauncherSettings.WorkspaceScreens._ID + " INTEGER PRIMARY KEY," +
677 LauncherSettings.WorkspaceScreens.SCREEN_RANK + " INTEGER," +
678 LauncherSettings.ChangeLogColumns.MODIFIED + " INTEGER NOT NULL DEFAULT 0" +
682 private void removeOrphanedItems(SQLiteDatabase db) {
683 // Delete items directly on the workspace who's screen id doesn't exist
684 // "DELETE FROM favorites WHERE screen NOT IN (SELECT _id FROM workspaceScreens)
685 // AND container = -100"
686 String removeOrphanedDesktopItems = "DELETE FROM " + Favorites.TABLE_NAME +
688 LauncherSettings.Favorites.SCREEN + " NOT IN (SELECT " +
689 LauncherSettings.WorkspaceScreens._ID + " FROM " + WorkspaceScreens.TABLE_NAME + ")" +
691 LauncherSettings.Favorites.CONTAINER + " = " +
692 LauncherSettings.Favorites.CONTAINER_DESKTOP;
693 db.execSQL(removeOrphanedDesktopItems);
695 // Delete items contained in folders which no longer exist (after above statement)
696 // "DELETE FROM favorites WHERE container <> -100 AND container <> -101 AND container
697 // NOT IN (SELECT _id FROM favorites WHERE itemType = 2)"
698 String removeOrphanedFolderItems = "DELETE FROM " + Favorites.TABLE_NAME +
700 LauncherSettings.Favorites.CONTAINER + " <> " +
701 LauncherSettings.Favorites.CONTAINER_DESKTOP +
703 + LauncherSettings.Favorites.CONTAINER + " <> " +
704 LauncherSettings.Favorites.CONTAINER_HOTSEAT +
706 + LauncherSettings.Favorites.CONTAINER + " NOT IN (SELECT " +
707 LauncherSettings.Favorites._ID + " FROM " + Favorites.TABLE_NAME +
708 " WHERE " + LauncherSettings.Favorites.ITEM_TYPE + " = " +
709 LauncherSettings.Favorites.ITEM_TYPE_FOLDER + ")";
710 db.execSQL(removeOrphanedFolderItems);
714 public void onOpen(SQLiteDatabase db) {
716 SharedPreferences prefs = mContext
717 .getSharedPreferences(LauncherFiles.DEVICE_PREFERENCES_KEY, 0);
718 int oldVersion = prefs.getInt(PREF_KEY_DATA_VERISON, 0);
719 if (oldVersion != DATA_VERSION) {
720 // Only run the data upgrade path for an existing db.
721 if (!Utilities.getPrefs(mContext).getBoolean(EMPTY_DATABASE_CREATED, false)) {
722 db.beginTransaction();
724 onDataUpgrade(db, oldVersion);
725 db.setTransactionSuccessful();
726 } catch (Exception e) {
727 Log.d(TAG, "Error updating data version, ignoring", e);
733 prefs.edit().putInt(PREF_KEY_DATA_VERISON, DATA_VERSION).apply();
738 * Called when the data is updated as part of app update. It can be called multiple times
739 * with old version, even though it had been run before. The changes made here must be
740 * backwards compatible, else we risk breaking old devices during restore or binary
743 protected void onDataUpgrade(SQLiteDatabase db, int oldVersion) {
744 switch (oldVersion) {
747 // Remove "profile extra"
748 UserManagerCompat um = UserManagerCompat.getInstance(mContext);
749 for (UserHandle user : um.getUserProfiles()) {
750 long serial = um.getSerialNumberForUser(user);
751 String sql = "update favorites set intent = replace(intent, "
752 + "';l.profile=" + serial + ";', ';') where itemType = 0;";
757 removeGhostWidgets(db);
765 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
766 if (LOGD) Log.d(TAG, "onUpgrade triggered: " + oldVersion);
767 switch (oldVersion) {
768 // The version cannot be lower that 12, as Launcher3 never supported a lower
769 // version of the DB.
771 // With the new shrink-wrapped and re-orderable workspaces, it makes sense
772 // to persist workspace screens and their relative order.
774 addWorkspacesTable(db, false);
777 db.beginTransaction();
779 // Insert new column for holding widget provider name
780 db.execSQL("ALTER TABLE favorites " +
781 "ADD COLUMN appWidgetProvider TEXT;");
782 db.setTransactionSuccessful();
783 } catch (SQLException ex) {
784 Log.e(TAG, ex.getMessage(), ex);
785 // Old version remains, which means we wipe old data
792 db.beginTransaction();
794 // Insert new column for holding update timestamp
795 db.execSQL("ALTER TABLE favorites " +
796 "ADD COLUMN modified INTEGER NOT NULL DEFAULT 0;");
797 db.execSQL("ALTER TABLE workspaceScreens " +
798 "ADD COLUMN modified INTEGER NOT NULL DEFAULT 0;");
799 db.setTransactionSuccessful();
800 } catch (SQLException ex) {
801 Log.e(TAG, ex.getMessage(), ex);
802 // Old version remains, which means we wipe old data
809 if (!addIntegerColumn(db, Favorites.RESTORED, 0)) {
810 // Old version remains, which means we wipe old data
821 // Due to a data loss bug, some users may have items associated with screen ids
822 // which no longer exist. Since this can cause other problems, and since the user
823 // will never see these items anyway, we use database upgrade as an opportunity to
825 removeOrphanedItems(db);
829 if (!addProfileColumn(db)) {
830 // Old version remains, which means we wipe old data
835 if (!updateFolderItemsRank(db, true)) {
839 // Recreate workspace table with screen id a primary key
840 if (!recreateWorkspaceTable(db)) {
844 if (!addIntegerColumn(db, Favorites.OPTIONS, 0)) {
845 // Old version remains, which means we wipe old data
852 ManagedProfileHeuristic.markExistingUsersForNoFolderCreation(mContext);
854 convertShortcutsToLauncherActivities(db);
856 // QSB was moved to the grid. Clear the first row on screen 0.
857 if (FeatureFlags.QSB_ON_FIRST_SCREEN &&
858 !LauncherDbUtils.prepareScreenZeroToHostQsb(mContext, db)) {
862 // DB Upgraded successfully
866 // DB was not upgraded
867 Log.w(TAG, "Destroying all old data.");
872 public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
873 if (oldVersion == 28 && newVersion == 27) {
874 // TODO: remove this check. This is only applicable for internal development/testing
875 // and for any released version of Launcher.
878 // This shouldn't happen -- throw our hands up in the air and start over.
879 Log.w(TAG, "Database version downgrade from: " + oldVersion + " to " + newVersion +
880 ". Wiping databse.");
885 * Clears all the data for a fresh start.
887 public void createEmptyDB(SQLiteDatabase db) {
888 db.beginTransaction();
890 db.execSQL("DROP TABLE IF EXISTS " + Favorites.TABLE_NAME);
891 db.execSQL("DROP TABLE IF EXISTS " + WorkspaceScreens.TABLE_NAME);
893 db.setTransactionSuccessful();
900 * Removes widgets which are registered to the Launcher's host, but are not present
903 public void removeGhostWidgets(SQLiteDatabase db) {
904 // Get all existing widget ids.
905 final AppWidgetHost host = newLauncherWidgetHost();
906 final int[] allWidgets;
908 Method getter = AppWidgetHost.class.getDeclaredMethod("getAppWidgetIds");
909 getter.setAccessible(true);
910 allWidgets = (int[]) getter.invoke(host);
911 } catch (Exception e) {
912 Log.e(TAG, "getAppWidgetIds not supported", e);
916 Cursor c = db.query(Favorites.TABLE_NAME,
917 new String[] {Favorites.APPWIDGET_ID },
918 "itemType=" + Favorites.ITEM_TYPE_APPWIDGET, null, null, null, null);
919 HashSet<Integer> validWidgets = new HashSet<>();
920 while (c.moveToNext()) {
921 validWidgets.add(c.getInt(0));
925 for (int widgetId : allWidgets) {
926 if (!validWidgets.contains(widgetId)) {
928 FileLog.d(TAG, "Deleting invalid widget " + widgetId);
929 host.deleteAppWidgetId(widgetId);
930 } catch (RuntimeException e) {
935 } catch (SQLException ex) {
936 Log.w(TAG, "Error getting widgets list", ex);
941 * Replaces all shortcuts of type {@link Favorites#ITEM_TYPE_SHORTCUT} which have a valid
942 * launcher activity target with {@link Favorites#ITEM_TYPE_APPLICATION}.
944 @Thunk void convertShortcutsToLauncherActivities(SQLiteDatabase db) {
945 db.beginTransaction();
947 SQLiteStatement updateStmt = null;
950 // Only consider the primary user as other users can't have a shortcut.
951 long userSerial = getDefaultUserSerial();
952 c = db.query(Favorites.TABLE_NAME, new String[] {
955 }, "itemType=" + Favorites.ITEM_TYPE_SHORTCUT + " AND profileId=" + userSerial,
956 null, null, null, null);
958 updateStmt = db.compileStatement("UPDATE favorites SET itemType="
959 + Favorites.ITEM_TYPE_APPLICATION + " WHERE _id=?");
961 final int idIndex = c.getColumnIndexOrThrow(Favorites._ID);
962 final int intentIndex = c.getColumnIndexOrThrow(Favorites.INTENT);
964 while (c.moveToNext()) {
965 String intentDescription = c.getString(intentIndex);
968 intent = Intent.parseUri(intentDescription, 0);
969 } catch (URISyntaxException e) {
970 Log.e(TAG, "Unable to parse intent", e);
974 if (!Utilities.isLauncherAppTarget(intent)) {
978 long id = c.getLong(idIndex);
979 updateStmt.bindLong(1, id);
980 updateStmt.executeUpdateDelete();
982 db.setTransactionSuccessful();
983 } catch (SQLException ex) {
984 Log.w(TAG, "Error deduping shortcuts", ex);
990 if (updateStmt != null) {
997 * Recreates workspace table and migrates data to the new table.
999 public boolean recreateWorkspaceTable(SQLiteDatabase db) {
1000 db.beginTransaction();
1002 Cursor c = db.query(WorkspaceScreens.TABLE_NAME,
1003 new String[] {LauncherSettings.WorkspaceScreens._ID},
1004 null, null, null, null,
1005 LauncherSettings.WorkspaceScreens.SCREEN_RANK);
1006 ArrayList<Long> sortedIDs = new ArrayList<Long>();
1009 while (c.moveToNext()) {
1010 Long id = c.getLong(0);
1011 if (!sortedIDs.contains(id)) {
1013 maxId = Math.max(maxId, id);
1020 db.execSQL("DROP TABLE IF EXISTS " + WorkspaceScreens.TABLE_NAME);
1021 addWorkspacesTable(db, false);
1023 // Add all screen ids back
1024 int total = sortedIDs.size();
1025 for (int i = 0; i < total; i++) {
1026 ContentValues values = new ContentValues();
1027 values.put(LauncherSettings.WorkspaceScreens._ID, sortedIDs.get(i));
1028 values.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, i);
1029 addModifiedTime(values);
1030 db.insertOrThrow(WorkspaceScreens.TABLE_NAME, null, values);
1032 db.setTransactionSuccessful();
1033 mMaxScreenId = maxId;
1034 } catch (SQLException ex) {
1035 // Old version remains, which means we wipe old data
1036 Log.e(TAG, ex.getMessage(), ex);
1039 db.endTransaction();
1044 @Thunk boolean updateFolderItemsRank(SQLiteDatabase db, boolean addRankColumn) {
1045 db.beginTransaction();
1047 if (addRankColumn) {
1048 // Insert new column for holding rank
1049 db.execSQL("ALTER TABLE favorites ADD COLUMN rank INTEGER NOT NULL DEFAULT 0;");
1052 // Get a map for folder ID to folder width
1053 Cursor c = db.rawQuery("SELECT container, MAX(cellX) FROM favorites"
1054 + " WHERE container IN (SELECT _id FROM favorites WHERE itemType = ?)"
1055 + " GROUP BY container;",
1056 new String[] {Integer.toString(LauncherSettings.Favorites.ITEM_TYPE_FOLDER)});
1058 while (c.moveToNext()) {
1059 db.execSQL("UPDATE favorites SET rank=cellX+(cellY*?) WHERE "
1060 + "container=? AND cellX IS NOT NULL AND cellY IS NOT NULL;",
1061 new Object[] {c.getLong(1) + 1, c.getLong(0)});
1065 db.setTransactionSuccessful();
1066 } catch (SQLException ex) {
1067 // Old version remains, which means we wipe old data
1068 Log.e(TAG, ex.getMessage(), ex);
1071 db.endTransaction();
1076 private boolean addProfileColumn(SQLiteDatabase db) {
1077 return addIntegerColumn(db, Favorites.PROFILE_ID, getDefaultUserSerial());
1080 private boolean addIntegerColumn(SQLiteDatabase db, String columnName, long defaultValue) {
1081 db.beginTransaction();
1083 db.execSQL("ALTER TABLE favorites ADD COLUMN "
1084 + columnName + " INTEGER NOT NULL DEFAULT " + defaultValue + ";");
1085 db.setTransactionSuccessful();
1086 } catch (SQLException ex) {
1087 Log.e(TAG, ex.getMessage(), ex);
1090 db.endTransaction();
1095 // Generates a new ID to use for an object in your database. This method should be only
1096 // called from the main UI thread. As an exception, we do call it when we call the
1097 // constructor from the worker thread; however, this doesn't extend until after the
1098 // constructor is called, and we only pass a reference to LauncherProvider to LauncherApp
1101 public long generateNewItemId() {
1102 if (mMaxItemId < 0) {
1103 throw new RuntimeException("Error: max item id was not initialized");
1109 public AppWidgetHost newLauncherWidgetHost() {
1110 return new AppWidgetHost(mContext, Launcher.APPWIDGET_HOST_ID);
1114 public long insertAndCheck(SQLiteDatabase db, ContentValues values) {
1115 return dbInsertAndCheck(this, db, Favorites.TABLE_NAME, null, values);
1118 public void checkId(String table, ContentValues values) {
1119 long id = values.getAsLong(LauncherSettings.BaseLauncherColumns._ID);
1120 if (WorkspaceScreens.TABLE_NAME.equals(table)) {
1121 mMaxScreenId = Math.max(id, mMaxScreenId);
1123 mMaxItemId = Math.max(id, mMaxItemId);
1127 private long initializeMaxItemId(SQLiteDatabase db) {
1128 return getMaxId(db, Favorites.TABLE_NAME);
1131 // Generates a new ID to use for an workspace screen in your database. This method
1132 // should be only called from the main UI thread. As an exception, we do call it when we
1133 // call the constructor from the worker thread; however, this doesn't extend until after the
1134 // constructor is called, and we only pass a reference to LauncherProvider to LauncherApp
1136 public long generateNewScreenId() {
1137 if (mMaxScreenId < 0) {
1138 throw new RuntimeException("Error: max screen id was not initialized");
1141 return mMaxScreenId;
1144 private long initializeMaxScreenId(SQLiteDatabase db) {
1145 return getMaxId(db, WorkspaceScreens.TABLE_NAME);
1148 @Thunk int loadFavorites(SQLiteDatabase db, AutoInstallsLayout loader) {
1149 ArrayList<Long> screenIds = new ArrayList<Long>();
1150 // TODO: Use multiple loaders with fall-back and transaction.
1151 int count = loader.loadLayout(db, screenIds);
1153 // Add the screens specified by the items above
1154 Collections.sort(screenIds);
1156 ContentValues values = new ContentValues();
1157 for (Long id : screenIds) {
1159 values.put(LauncherSettings.WorkspaceScreens._ID, id);
1160 values.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, rank);
1161 if (dbInsertAndCheck(this, db, WorkspaceScreens.TABLE_NAME, null, values) < 0) {
1162 throw new RuntimeException("Failed initialize screen table"
1163 + "from default layout");
1168 // Ensure that the max ids are initialized
1169 mMaxItemId = initializeMaxItemId(db);
1170 mMaxScreenId = initializeMaxScreenId(db);
1177 * @return the max _id in the provided table.
1179 @Thunk static long getMaxId(SQLiteDatabase db, String table) {
1180 Cursor c = db.rawQuery("SELECT MAX(_id) FROM " + table, null);
1183 if (c != null && c.moveToNext()) {
1191 throw new RuntimeException("Error: could not query max id in " + table);
1197 static class SqlArguments {
1198 public final String table;
1199 public final String where;
1200 public final String[] args;
1202 SqlArguments(Uri url, String where, String[] args) {
1203 if (url.getPathSegments().size() == 1) {
1204 this.table = url.getPathSegments().get(0);
1207 } else if (url.getPathSegments().size() != 2) {
1208 throw new IllegalArgumentException("Invalid URI: " + url);
1209 } else if (!TextUtils.isEmpty(where)) {
1210 throw new UnsupportedOperationException("WHERE clause not supported: " + url);
1212 this.table = url.getPathSegments().get(0);
1213 this.where = "_id=" + ContentUris.parseId(url);
1218 SqlArguments(Uri url) {
1219 if (url.getPathSegments().size() == 1) {
1220 table = url.getPathSegments().get(0);
1224 throw new IllegalArgumentException("Invalid URI: " + url);
1229 private static class ChangeListenerWrapper implements Handler.Callback {
1231 private static final int MSG_LAUNCHER_PROVIDER_CHANGED = 1;
1232 private static final int MSG_EXTRACTED_COLORS_CHANGED = 2;
1233 private static final int MSG_APP_WIDGET_HOST_RESET = 3;
1235 private LauncherProviderChangeListener mListener;
1238 public boolean handleMessage(Message msg) {
1239 if (mListener != null) {
1241 case MSG_LAUNCHER_PROVIDER_CHANGED:
1242 mListener.onLauncherProviderChanged();
1244 case MSG_EXTRACTED_COLORS_CHANGED:
1245 mListener.onExtractedColorsChanged();
1247 case MSG_APP_WIDGET_HOST_RESET:
1248 mListener.onAppWidgetHostReset();