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