LauncherProvider.java revision 323f47f8bf24d4d9ef5281377b287431efbeada9
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.appwidget.AppWidgetHost;
20import android.appwidget.AppWidgetManager;
21import android.content.ComponentName;
22import android.content.ContentProvider;
23import android.content.ContentProviderOperation;
24import android.content.ContentProviderResult;
25import android.content.ContentResolver;
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.res.Resources;
33import android.database.Cursor;
34import android.database.SQLException;
35import android.database.sqlite.SQLiteDatabase;
36import android.database.sqlite.SQLiteOpenHelper;
37import android.database.sqlite.SQLiteQueryBuilder;
38import android.net.Uri;
39import android.os.StrictMode;
40import android.text.TextUtils;
41import android.util.Log;
42import android.util.SparseArray;
43
44import com.android.launcher3.AutoInstallsLayout.LayoutParserCallback;
45import com.android.launcher3.LauncherSettings.Favorites;
46import com.android.launcher3.compat.UserHandleCompat;
47import com.android.launcher3.compat.UserManagerCompat;
48import com.android.launcher3.config.ProviderConfig;
49
50import java.io.File;
51import java.net.URISyntaxException;
52import java.util.ArrayList;
53import java.util.Collections;
54import java.util.HashSet;
55
56public class LauncherProvider extends ContentProvider {
57    private static final String TAG = "Launcher.LauncherProvider";
58    private static final boolean LOGD = false;
59
60    private static final int DATABASE_VERSION = 22;
61
62    static final String OLD_AUTHORITY = "com.android.launcher2.settings";
63    static final String AUTHORITY = ProviderConfig.AUTHORITY;
64
65    static final String TABLE_FAVORITES = "favorites";
66    static final String TABLE_WORKSPACE_SCREENS = "workspaceScreens";
67    static final String PARAMETER_NOTIFY = "notify";
68    static final String EMPTY_DATABASE_CREATED = "EMPTY_DATABASE_CREATED";
69
70    private static final String URI_PARAM_IS_EXTERNAL_ADD = "isExternalAdd";
71
72    private LauncherProviderChangeListener mListener;
73
74    /**
75     * {@link Uri} triggered at any registered {@link android.database.ContentObserver} when
76     * {@link AppWidgetHost#deleteHost()} is called during database creation.
77     * Use this to recall {@link AppWidgetHost#startListening()} if needed.
78     */
79    static final Uri CONTENT_APPWIDGET_RESET_URI =
80            Uri.parse("content://" + AUTHORITY + "/appWidgetReset");
81
82    private DatabaseHelper mOpenHelper;
83
84    @Override
85    public boolean onCreate() {
86        final Context context = getContext();
87        StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
88        mOpenHelper = new DatabaseHelper(context);
89        StrictMode.setThreadPolicy(oldPolicy);
90        LauncherAppState.setLauncherProvider(this);
91        return true;
92    }
93
94    public boolean wasNewDbCreated() {
95        return mOpenHelper.wasNewDbCreated();
96    }
97
98    public void setLauncherProviderChangeListener(LauncherProviderChangeListener listener) {
99        mListener = listener;
100    }
101
102    @Override
103    public String getType(Uri uri) {
104        SqlArguments args = new SqlArguments(uri, null, null);
105        if (TextUtils.isEmpty(args.where)) {
106            return "vnd.android.cursor.dir/" + args.table;
107        } else {
108            return "vnd.android.cursor.item/" + args.table;
109        }
110    }
111
112    @Override
113    public Cursor query(Uri uri, String[] projection, String selection,
114            String[] selectionArgs, String sortOrder) {
115
116        SqlArguments args = new SqlArguments(uri, selection, selectionArgs);
117        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
118        qb.setTables(args.table);
119
120        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
121        Cursor result = qb.query(db, projection, args.where, args.args, null, null, sortOrder);
122        result.setNotificationUri(getContext().getContentResolver(), uri);
123
124        return result;
125    }
126
127    private static long dbInsertAndCheck(DatabaseHelper helper,
128            SQLiteDatabase db, String table, String nullColumnHack, ContentValues values) {
129        if (values == null) {
130            throw new RuntimeException("Error: attempting to insert null values");
131        }
132        if (!values.containsKey(LauncherSettings.ChangeLogColumns._ID)) {
133            throw new RuntimeException("Error: attempting to add item without specifying an id");
134        }
135        helper.checkId(table, values);
136        return db.insert(table, nullColumnHack, values);
137    }
138
139    @Override
140    public Uri insert(Uri uri, ContentValues initialValues) {
141        SqlArguments args = new SqlArguments(uri);
142
143        // In very limited cases, we support system|signature permission apps to add to the db
144        String externalAdd = uri.getQueryParameter(URI_PARAM_IS_EXTERNAL_ADD);
145        if (externalAdd != null && "true".equals(externalAdd)) {
146            if (!mOpenHelper.initializeExternalAdd(initialValues)) {
147                return null;
148            }
149        }
150
151        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
152        addModifiedTime(initialValues);
153        final long rowId = dbInsertAndCheck(mOpenHelper, db, args.table, null, initialValues);
154        if (rowId < 0) return null;
155
156        uri = ContentUris.withAppendedId(uri, rowId);
157        sendNotify(uri);
158
159        return uri;
160    }
161
162
163    @Override
164    public int bulkInsert(Uri uri, ContentValues[] values) {
165        SqlArguments args = new SqlArguments(uri);
166
167        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
168        db.beginTransaction();
169        try {
170            int numValues = values.length;
171            for (int i = 0; i < numValues; i++) {
172                addModifiedTime(values[i]);
173                if (dbInsertAndCheck(mOpenHelper, db, args.table, null, values[i]) < 0) {
174                    return 0;
175                }
176            }
177            db.setTransactionSuccessful();
178        } finally {
179            db.endTransaction();
180        }
181
182        sendNotify(uri);
183        return values.length;
184    }
185
186    @Override
187    public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
188            throws OperationApplicationException {
189        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
190        db.beginTransaction();
191        try {
192            ContentProviderResult[] result =  super.applyBatch(operations);
193            db.setTransactionSuccessful();
194            return result;
195        } finally {
196            db.endTransaction();
197        }
198    }
199
200    @Override
201    public int delete(Uri uri, String selection, String[] selectionArgs) {
202        SqlArguments args = new SqlArguments(uri, selection, selectionArgs);
203
204        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
205        int count = db.delete(args.table, args.where, args.args);
206        if (count > 0) sendNotify(uri);
207
208        return count;
209    }
210
211    @Override
212    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
213        SqlArguments args = new SqlArguments(uri, selection, selectionArgs);
214
215        addModifiedTime(values);
216        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
217        int count = db.update(args.table, values, args.where, args.args);
218        if (count > 0) sendNotify(uri);
219
220        return count;
221    }
222
223    private void sendNotify(Uri uri) {
224        String notify = uri.getQueryParameter(PARAMETER_NOTIFY);
225        if (notify == null || "true".equals(notify)) {
226            getContext().getContentResolver().notifyChange(uri, null);
227        }
228
229        // always notify the backup agent
230        LauncherBackupAgentHelper.dataChanged(getContext());
231        if (mListener != null) {
232            mListener.onLauncherProviderChange();
233        }
234    }
235
236    private static void addModifiedTime(ContentValues values) {
237        values.put(LauncherSettings.ChangeLogColumns.MODIFIED, System.currentTimeMillis());
238    }
239
240    public long generateNewItemId() {
241        return mOpenHelper.generateNewItemId();
242    }
243
244    public void updateMaxItemId(long id) {
245        mOpenHelper.updateMaxItemId(id);
246    }
247
248    public long generateNewScreenId() {
249        return mOpenHelper.generateNewScreenId();
250    }
251
252    /**
253     * Clears all the data for a fresh start.
254     */
255    synchronized public void createEmptyDB() {
256        mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
257    }
258
259    public void clearFlagEmptyDbCreated() {
260        String spKey = LauncherAppState.getSharedPreferencesKey();
261        getContext().getSharedPreferences(spKey, Context.MODE_PRIVATE)
262            .edit()
263            .remove(EMPTY_DATABASE_CREATED)
264            .commit();
265    }
266
267    /**
268     * Loads the default workspace based on the following priority scheme:
269     *   1) From a package provided by play store
270     *   2) From a partner configuration APK, already in the system image
271     *   3) The default configuration for the particular device
272     */
273    synchronized public void loadDefaultFavoritesIfNecessary() {
274        String spKey = LauncherAppState.getSharedPreferencesKey();
275        SharedPreferences sp = getContext().getSharedPreferences(spKey, Context.MODE_PRIVATE);
276
277        if (sp.getBoolean(EMPTY_DATABASE_CREATED, false)) {
278            Log.d(TAG, "loading default workspace");
279
280            AutoInstallsLayout loader = AutoInstallsLayout.get(getContext(),
281                    mOpenHelper.mAppWidgetHost, mOpenHelper);
282
283            if (loader == null) {
284                final Partner partner = Partner.get(getContext().getPackageManager());
285                if (partner != null && partner.hasDefaultLayout()) {
286                    final Resources partnerRes = partner.getResources();
287                    int workspaceResId = partnerRes.getIdentifier(Partner.RES_DEFAULT_LAYOUT,
288                            "xml", partner.getPackageName());
289                    if (workspaceResId != 0) {
290                        loader = new DefaultLayoutParser(getContext(), mOpenHelper.mAppWidgetHost,
291                                mOpenHelper, partnerRes, workspaceResId);
292                    }
293                }
294            }
295
296            final boolean usingExternallyProvidedLayout = loader != null;
297            if (loader == null) {
298                loader = getDefaultLayoutParser();
299            }
300
301            // There might be some partially restored DB items, due to buggy restore logic in
302            // previous versions of launcher.
303            createEmptyDB();
304            // Populate favorites table with initial favorites
305            if ((mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), loader) <= 0)
306                    && usingExternallyProvidedLayout) {
307                // Unable to load external layout. Cleanup and load the internal layout.
308                createEmptyDB();
309                mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(),
310                        getDefaultLayoutParser());
311            }
312            clearFlagEmptyDbCreated();
313        }
314    }
315
316    private DefaultLayoutParser getDefaultLayoutParser() {
317        int defaultLayout = LauncherAppState.getInstance()
318                .getDynamicGrid().getDeviceProfile().defaultLayoutId;
319        return new DefaultLayoutParser(getContext(), mOpenHelper.mAppWidgetHost,
320                mOpenHelper, getContext().getResources(), defaultLayout);
321    }
322
323    public void migrateLauncher2Shortcuts() {
324        mOpenHelper.migrateLauncher2Shortcuts(mOpenHelper.getWritableDatabase(),
325                Uri.parse(getContext().getString(R.string.old_launcher_provider_uri)));
326    }
327
328    public void updateFolderItemsRank() {
329        mOpenHelper.updateFolderItemsRank(mOpenHelper.getWritableDatabase(), false);
330    }
331
332    public void deleteDatabase() {
333        // Are you sure? (y/n)
334        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
335        final File dbFile = new File(db.getPath());
336        mOpenHelper.close();
337        if (dbFile.exists()) {
338            SQLiteDatabase.deleteDatabase(dbFile);
339        }
340        mOpenHelper = new DatabaseHelper(getContext());
341    }
342
343    private static class DatabaseHelper extends SQLiteOpenHelper implements LayoutParserCallback {
344        private final Context mContext;
345        private final AppWidgetHost mAppWidgetHost;
346        private long mMaxItemId = -1;
347        private long mMaxScreenId = -1;
348
349        private boolean mNewDbCreated = false;
350
351        DatabaseHelper(Context context) {
352            super(context, LauncherFiles.LAUNCHER_DB, null, DATABASE_VERSION);
353            mContext = context;
354            mAppWidgetHost = new AppWidgetHost(context, Launcher.APPWIDGET_HOST_ID);
355
356            // In the case where neither onCreate nor onUpgrade gets called, we read the maxId from
357            // the DB here
358            if (mMaxItemId == -1) {
359                mMaxItemId = initializeMaxItemId(getWritableDatabase());
360            }
361            if (mMaxScreenId == -1) {
362                mMaxScreenId = initializeMaxScreenId(getWritableDatabase());
363            }
364        }
365
366        public boolean wasNewDbCreated() {
367            return mNewDbCreated;
368        }
369
370        /**
371         * Send notification that we've deleted the {@link AppWidgetHost},
372         * probably as part of the initial database creation. The receiver may
373         * want to re-call {@link AppWidgetHost#startListening()} to ensure
374         * callbacks are correctly set.
375         */
376        private void sendAppWidgetResetNotify() {
377            final ContentResolver resolver = mContext.getContentResolver();
378            resolver.notifyChange(CONTENT_APPWIDGET_RESET_URI, null);
379        }
380
381        @Override
382        public void onCreate(SQLiteDatabase db) {
383            if (LOGD) Log.d(TAG, "creating new launcher database");
384
385            mMaxItemId = 1;
386            mMaxScreenId = 0;
387            mNewDbCreated = true;
388
389            UserManagerCompat userManager = UserManagerCompat.getInstance(mContext);
390            long userSerialNumber = userManager.getSerialNumberForUser(
391                    UserHandleCompat.myUserHandle());
392
393            db.execSQL("CREATE TABLE favorites (" +
394                    "_id INTEGER PRIMARY KEY," +
395                    "title TEXT," +
396                    "intent TEXT," +
397                    "container INTEGER," +
398                    "screen INTEGER," +
399                    "cellX INTEGER," +
400                    "cellY INTEGER," +
401                    "spanX INTEGER," +
402                    "spanY INTEGER," +
403                    "itemType INTEGER," +
404                    "appWidgetId INTEGER NOT NULL DEFAULT -1," +
405                    "isShortcut INTEGER," +
406                    "iconType INTEGER," +
407                    "iconPackage TEXT," +
408                    "iconResource TEXT," +
409                    "icon BLOB," +
410                    "uri TEXT," +
411                    "displayMode INTEGER," +
412                    "appWidgetProvider TEXT," +
413                    "modified INTEGER NOT NULL DEFAULT 0," +
414                    "restored INTEGER NOT NULL DEFAULT 0," +
415                    "profileId INTEGER DEFAULT " + userSerialNumber + "," +
416                    "rank INTEGER NOT NULL DEFAULT 0" +
417                    ");");
418            addWorkspacesTable(db);
419
420            // Database was just created, so wipe any previous widgets
421            if (mAppWidgetHost != null) {
422                mAppWidgetHost.deleteHost();
423                sendAppWidgetResetNotify();
424            }
425
426            // Fresh and clean launcher DB.
427            mMaxItemId = initializeMaxItemId(db);
428            setFlagEmptyDbCreated();
429        }
430
431        private void addWorkspacesTable(SQLiteDatabase db) {
432            db.execSQL("CREATE TABLE " + TABLE_WORKSPACE_SCREENS + " (" +
433                    LauncherSettings.WorkspaceScreens._ID + " INTEGER PRIMARY KEY," +
434                    LauncherSettings.WorkspaceScreens.SCREEN_RANK + " INTEGER," +
435                    LauncherSettings.ChangeLogColumns.MODIFIED + " INTEGER NOT NULL DEFAULT 0" +
436                    ");");
437        }
438
439        private void removeOrphanedItems(SQLiteDatabase db) {
440            // Delete items directly on the workspace who's screen id doesn't exist
441            //  "DELETE FROM favorites WHERE screen NOT IN (SELECT _id FROM workspaceScreens)
442            //   AND container = -100"
443            String removeOrphanedDesktopItems = "DELETE FROM " + TABLE_FAVORITES +
444                    " WHERE " +
445                    LauncherSettings.Favorites.SCREEN + " NOT IN (SELECT " +
446                    LauncherSettings.WorkspaceScreens._ID + " FROM " + TABLE_WORKSPACE_SCREENS + ")" +
447                    " AND " +
448                    LauncherSettings.Favorites.CONTAINER + " = " +
449                    LauncherSettings.Favorites.CONTAINER_DESKTOP;
450            db.execSQL(removeOrphanedDesktopItems);
451
452            // Delete items contained in folders which no longer exist (after above statement)
453            //  "DELETE FROM favorites  WHERE container <> -100 AND container <> -101 AND container
454            //   NOT IN (SELECT _id FROM favorites WHERE itemType = 2)"
455            String removeOrphanedFolderItems = "DELETE FROM " + TABLE_FAVORITES +
456                    " WHERE " +
457                    LauncherSettings.Favorites.CONTAINER + " <> " +
458                    LauncherSettings.Favorites.CONTAINER_DESKTOP +
459                    " AND "
460                    + LauncherSettings.Favorites.CONTAINER + " <> " +
461                    LauncherSettings.Favorites.CONTAINER_HOTSEAT +
462                    " AND "
463                    + LauncherSettings.Favorites.CONTAINER + " NOT IN (SELECT " +
464                    LauncherSettings.Favorites._ID + " FROM " + TABLE_FAVORITES +
465                    " WHERE " + LauncherSettings.Favorites.ITEM_TYPE + " = " +
466                    LauncherSettings.Favorites.ITEM_TYPE_FOLDER + ")";
467            db.execSQL(removeOrphanedFolderItems);
468        }
469
470        private void setFlagJustLoadedOldDb() {
471            String spKey = LauncherAppState.getSharedPreferencesKey();
472            SharedPreferences sp = mContext.getSharedPreferences(spKey, Context.MODE_PRIVATE);
473            sp.edit().putBoolean(EMPTY_DATABASE_CREATED, false).commit();
474        }
475
476        private void setFlagEmptyDbCreated() {
477            String spKey = LauncherAppState.getSharedPreferencesKey();
478            SharedPreferences sp = mContext.getSharedPreferences(spKey, Context.MODE_PRIVATE);
479            sp.edit().putBoolean(EMPTY_DATABASE_CREATED, true).commit();
480        }
481
482        @Override
483        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
484            if (LOGD) Log.d(TAG, "onUpgrade triggered: " + oldVersion);
485            switch (oldVersion) {
486                // The version cannot be lower that 12, as Launcher3 never supported a lower
487                // version of the DB.
488                case 12: {
489                    // With the new shrink-wrapped and re-orderable workspaces, it makes sense
490                    // to persist workspace screens and their relative order.
491                    mMaxScreenId = 0;
492                    addWorkspacesTable(db);
493                }
494                case 13: {
495                    db.beginTransaction();
496                    try {
497                        // Insert new column for holding widget provider name
498                        db.execSQL("ALTER TABLE favorites " +
499                                "ADD COLUMN appWidgetProvider TEXT;");
500                        db.setTransactionSuccessful();
501                    } catch (SQLException ex) {
502                        Log.e(TAG, ex.getMessage(), ex);
503                        // Old version remains, which means we wipe old data
504                        break;
505                    } finally {
506                        db.endTransaction();
507                    }
508                }
509                case 14: {
510                    db.beginTransaction();
511                    try {
512                        // Insert new column for holding update timestamp
513                        db.execSQL("ALTER TABLE favorites " +
514                                "ADD COLUMN modified INTEGER NOT NULL DEFAULT 0;");
515                        db.execSQL("ALTER TABLE workspaceScreens " +
516                                "ADD COLUMN modified INTEGER NOT NULL DEFAULT 0;");
517                        db.setTransactionSuccessful();
518                    } catch (SQLException ex) {
519                        Log.e(TAG, ex.getMessage(), ex);
520                        // Old version remains, which means we wipe old data
521                        break;
522                    } finally {
523                        db.endTransaction();
524                    }
525                }
526                case 15: {
527                    db.beginTransaction();
528                    try {
529                        // Insert new column for holding restore status
530                        db.execSQL("ALTER TABLE favorites " +
531                                "ADD COLUMN restored INTEGER NOT NULL DEFAULT 0;");
532                        db.setTransactionSuccessful();
533                    } catch (SQLException ex) {
534                        Log.e(TAG, ex.getMessage(), ex);
535                        // Old version remains, which means we wipe old data
536                        break;
537                    } finally {
538                        db.endTransaction();
539                    }
540                }
541                case 16: {
542                    // We use the db version upgrade here to identify users who may not have seen
543                    // clings yet (because they weren't available), but for whom the clings are now
544                    // available (tablet users). Because one of the possible cling flows (migration)
545                    // is very destructive (wipes out workspaces), we want to prevent this from showing
546                    // until clear data. We do so by marking that the clings have been shown.
547                    LauncherClings.synchonouslyMarkFirstRunClingDismissed(mContext);
548                }
549                case 17: {
550                    // No-op
551                }
552                case 18: {
553                    // Due to a data loss bug, some users may have items associated with screen ids
554                    // which no longer exist. Since this can cause other problems, and since the user
555                    // will never see these items anyway, we use database upgrade as an opportunity to
556                    // clean things up.
557                    removeOrphanedItems(db);
558                }
559                case 19: {
560                    // Add userId column
561                    if (!addProfileColumn(db)) {
562                        // Old version remains, which means we wipe old data
563                        break;
564                    }
565                }
566                case 20:
567                    if (!updateFolderItemsRank(db, true)) {
568                        break;
569                    }
570                case 21:
571                    // Recreate workspace table with screen id a primary key
572                    if (!recreateWorkspaceTable(db)) {
573                        break;
574                    }
575                case 22: {
576                    // DB Upgraded successfully
577                    return;
578                }
579            }
580
581            // DB was not upgraded
582            Log.w(TAG, "Destroying all old data.");
583            createEmptyDB(db);
584        }
585
586        @Override
587        public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
588            // This shouldn't happen -- throw our hands up in the air and start over.
589            Log.w(TAG, "Database version downgrade from: " + oldVersion + " to " + newVersion +
590                    ". Wiping databse.");
591            createEmptyDB(db);
592        }
593
594
595        /**
596         * Clears all the data for a fresh start.
597         */
598        public void createEmptyDB(SQLiteDatabase db) {
599            db.execSQL("DROP TABLE IF EXISTS " + TABLE_FAVORITES);
600            db.execSQL("DROP TABLE IF EXISTS " + TABLE_WORKSPACE_SCREENS);
601            onCreate(db);
602        }
603
604        /**
605         * Recreates workspace table and migrates data to the new table.
606         */
607        public boolean recreateWorkspaceTable(SQLiteDatabase db) {
608            db.beginTransaction();
609            try {
610                Cursor c = db.query(TABLE_WORKSPACE_SCREENS,
611                        new String[] {LauncherSettings.WorkspaceScreens._ID},
612                        null, null, null, null,
613                        LauncherSettings.WorkspaceScreens.SCREEN_RANK);
614                ArrayList<Long> sortedIDs = new ArrayList<Long>();
615                long maxId = 0;
616                try {
617                    while (c.moveToNext()) {
618                        Long id = c.getLong(0);
619                        if (!sortedIDs.contains(id)) {
620                            sortedIDs.add(id);
621                            maxId = Math.max(maxId, id);
622                        }
623                    }
624                } finally {
625                    c.close();
626                }
627
628                db.execSQL("DROP TABLE IF EXISTS " + TABLE_WORKSPACE_SCREENS);
629                addWorkspacesTable(db);
630
631                // Add all screen ids back
632                int total = sortedIDs.size();
633                for (int i = 0; i < total; i++) {
634                    ContentValues values = new ContentValues();
635                    values.put(LauncherSettings.WorkspaceScreens._ID, sortedIDs.get(i));
636                    values.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, i);
637                    addModifiedTime(values);
638                    db.insertOrThrow(TABLE_WORKSPACE_SCREENS, null, values);
639                }
640                db.setTransactionSuccessful();
641                mMaxScreenId = maxId;
642            } catch (SQLException ex) {
643                // Old version remains, which means we wipe old data
644                Log.e(TAG, ex.getMessage(), ex);
645                return false;
646            } finally {
647                db.endTransaction();
648            }
649            return true;
650        }
651
652        private boolean updateFolderItemsRank(SQLiteDatabase db, boolean addRankColumn) {
653            db.beginTransaction();
654            try {
655                if (addRankColumn) {
656                    // Insert new column for holding rank
657                    db.execSQL("ALTER TABLE favorites ADD COLUMN rank INTEGER NOT NULL DEFAULT 0;");
658                }
659
660                // Get a map for folder ID to folder width
661                Cursor c = db.rawQuery("SELECT container, MAX(cellX) FROM favorites"
662                        + " WHERE container IN (SELECT _id FROM favorites WHERE itemType = ?)"
663                        + " GROUP BY container;",
664                        new String[] {Integer.toString(LauncherSettings.Favorites.ITEM_TYPE_FOLDER)});
665
666                while (c.moveToNext()) {
667                    db.execSQL("UPDATE favorites SET rank=cellX+(cellY*?) WHERE "
668                            + "container=? AND cellX IS NOT NULL AND cellY IS NOT NULL;",
669                            new Object[] {c.getLong(1) + 1, c.getLong(0)});
670                }
671
672                c.close();
673                db.setTransactionSuccessful();
674            } catch (SQLException ex) {
675                // Old version remains, which means we wipe old data
676                Log.e(TAG, ex.getMessage(), ex);
677                return false;
678            } finally {
679                db.endTransaction();
680            }
681            return true;
682        }
683
684        private boolean addProfileColumn(SQLiteDatabase db) {
685            db.beginTransaction();
686            try {
687                UserManagerCompat userManager = UserManagerCompat.getInstance(mContext);
688                // Default to the serial number of this user, for older
689                // shortcuts.
690                long userSerialNumber = userManager.getSerialNumberForUser(
691                        UserHandleCompat.myUserHandle());
692                // Insert new column for holding user serial number
693                db.execSQL("ALTER TABLE favorites " +
694                        "ADD COLUMN profileId INTEGER DEFAULT "
695                                        + userSerialNumber + ";");
696                db.setTransactionSuccessful();
697            } catch (SQLException ex) {
698                // Old version remains, which means we wipe old data
699                Log.e(TAG, ex.getMessage(), ex);
700                return false;
701            } finally {
702                db.endTransaction();
703            }
704            return true;
705        }
706
707        // Generates a new ID to use for an object in your database. This method should be only
708        // called from the main UI thread. As an exception, we do call it when we call the
709        // constructor from the worker thread; however, this doesn't extend until after the
710        // constructor is called, and we only pass a reference to LauncherProvider to LauncherApp
711        // after that point
712        @Override
713        public long generateNewItemId() {
714            if (mMaxItemId < 0) {
715                throw new RuntimeException("Error: max item id was not initialized");
716            }
717            mMaxItemId += 1;
718            return mMaxItemId;
719        }
720
721        @Override
722        public long insertAndCheck(SQLiteDatabase db, ContentValues values) {
723            return dbInsertAndCheck(this, db, TABLE_FAVORITES, null, values);
724        }
725
726        public void updateMaxItemId(long id) {
727            mMaxItemId = id + 1;
728        }
729
730        public void checkId(String table, ContentValues values) {
731            long id = values.getAsLong(LauncherSettings.BaseLauncherColumns._ID);
732            if (table == LauncherProvider.TABLE_WORKSPACE_SCREENS) {
733                mMaxScreenId = Math.max(id, mMaxScreenId);
734            }  else {
735                mMaxItemId = Math.max(id, mMaxItemId);
736            }
737        }
738
739        private long initializeMaxItemId(SQLiteDatabase db) {
740            Cursor c = db.rawQuery("SELECT MAX(_id) FROM favorites", null);
741
742            // get the result
743            final int maxIdIndex = 0;
744            long id = -1;
745            if (c != null && c.moveToNext()) {
746                id = c.getLong(maxIdIndex);
747            }
748            if (c != null) {
749                c.close();
750            }
751
752            if (id == -1) {
753                throw new RuntimeException("Error: could not query max item id");
754            }
755
756            return id;
757        }
758
759        // Generates a new ID to use for an workspace screen in your database. This method
760        // should be only called from the main UI thread. As an exception, we do call it when we
761        // call the constructor from the worker thread; however, this doesn't extend until after the
762        // constructor is called, and we only pass a reference to LauncherProvider to LauncherApp
763        // after that point
764        public long generateNewScreenId() {
765            if (mMaxScreenId < 0) {
766                throw new RuntimeException("Error: max screen id was not initialized");
767            }
768            mMaxScreenId += 1;
769            // Log to disk
770            Launcher.addDumpLog(TAG, "11683562 - generateNewScreenId(): " + mMaxScreenId, true);
771            return mMaxScreenId;
772        }
773
774        private long initializeMaxScreenId(SQLiteDatabase db) {
775            Cursor c = db.rawQuery("SELECT MAX(" + LauncherSettings.WorkspaceScreens._ID + ") FROM " + TABLE_WORKSPACE_SCREENS, null);
776
777            // get the result
778            final int maxIdIndex = 0;
779            long id = -1;
780            if (c != null && c.moveToNext()) {
781                id = c.getLong(maxIdIndex);
782            }
783            if (c != null) {
784                c.close();
785            }
786
787            if (id == -1) {
788                throw new RuntimeException("Error: could not query max screen id");
789            }
790
791            // Log to disk
792            Launcher.addDumpLog(TAG, "11683562 - initializeMaxScreenId(): " + id, true);
793            return id;
794        }
795
796        private boolean initializeExternalAdd(ContentValues values) {
797            // 1. Ensure that externally added items have a valid item id
798            long id = generateNewItemId();
799            values.put(LauncherSettings.Favorites._ID, id);
800
801            // 2. In the case of an app widget, and if no app widget id is specified, we
802            // attempt allocate and bind the widget.
803            Integer itemType = values.getAsInteger(LauncherSettings.Favorites.ITEM_TYPE);
804            if (itemType != null &&
805                    itemType.intValue() == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET &&
806                    !values.containsKey(LauncherSettings.Favorites.APPWIDGET_ID)) {
807
808                final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext);
809                ComponentName cn = ComponentName.unflattenFromString(
810                        values.getAsString(Favorites.APPWIDGET_PROVIDER));
811
812                if (cn != null) {
813                    try {
814                        int appWidgetId = mAppWidgetHost.allocateAppWidgetId();
815                        values.put(LauncherSettings.Favorites.APPWIDGET_ID, appWidgetId);
816                        if (!appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId,cn)) {
817                            return false;
818                        }
819                    } catch (RuntimeException e) {
820                        Log.e(TAG, "Failed to initialize external widget", e);
821                        return false;
822                    }
823                } else {
824                    return false;
825                }
826            }
827
828            // Add screen id if not present
829            long screenId = values.getAsLong(LauncherSettings.Favorites.SCREEN);
830            if (!addScreenIdIfNecessary(screenId)) {
831                return false;
832            }
833            return true;
834        }
835
836        // Returns true of screen id exists, or if successfully added
837        private boolean addScreenIdIfNecessary(long screenId) {
838            if (!hasScreenId(screenId)) {
839                int rank = getMaxScreenRank() + 1;
840
841                ContentValues v = new ContentValues();
842                v.put(LauncherSettings.WorkspaceScreens._ID, screenId);
843                v.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, rank);
844                if (dbInsertAndCheck(this, getWritableDatabase(),
845                        TABLE_WORKSPACE_SCREENS, null, v) < 0) {
846                    return false;
847                }
848            }
849            return true;
850        }
851
852        private boolean hasScreenId(long screenId) {
853            SQLiteDatabase db = getWritableDatabase();
854            Cursor c = db.rawQuery("SELECT * FROM " + TABLE_WORKSPACE_SCREENS + " WHERE "
855                    + LauncherSettings.WorkspaceScreens._ID + " = " + screenId, null);
856            if (c != null) {
857                int count = c.getCount();
858                c.close();
859                return count > 0;
860            } else {
861                return false;
862            }
863        }
864
865        private int getMaxScreenRank() {
866            SQLiteDatabase db = getWritableDatabase();
867            Cursor c = db.rawQuery("SELECT MAX(" + LauncherSettings.WorkspaceScreens.SCREEN_RANK
868                    + ") FROM " + TABLE_WORKSPACE_SCREENS, null);
869
870            // get the result
871            final int maxRankIndex = 0;
872            int rank = -1;
873            if (c != null && c.moveToNext()) {
874                rank = c.getInt(maxRankIndex);
875            }
876            if (c != null) {
877                c.close();
878            }
879
880            return rank;
881        }
882
883        private int loadFavorites(SQLiteDatabase db, AutoInstallsLayout loader) {
884            ArrayList<Long> screenIds = new ArrayList<Long>();
885            // TODO: Use multiple loaders with fall-back and transaction.
886            int count = loader.loadLayout(db, screenIds);
887
888            // Add the screens specified by the items above
889            Collections.sort(screenIds);
890            int rank = 0;
891            ContentValues values = new ContentValues();
892            for (Long id : screenIds) {
893                values.clear();
894                values.put(LauncherSettings.WorkspaceScreens._ID, id);
895                values.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, rank);
896                if (dbInsertAndCheck(this, db, TABLE_WORKSPACE_SCREENS, null, values) < 0) {
897                    throw new RuntimeException("Failed initialize screen table"
898                            + "from default layout");
899                }
900                rank++;
901            }
902
903            // Ensure that the max ids are initialized
904            mMaxItemId = initializeMaxItemId(db);
905            mMaxScreenId = initializeMaxScreenId(db);
906
907            return count;
908        }
909
910        private void migrateLauncher2Shortcuts(SQLiteDatabase db, Uri uri) {
911            final ContentResolver resolver = mContext.getContentResolver();
912            Cursor c = null;
913            int count = 0;
914            int curScreen = 0;
915
916            try {
917                c = resolver.query(uri, null, null, null, "title ASC");
918            } catch (Exception e) {
919                // Ignore
920            }
921
922            // We already have a favorites database in the old provider
923            if (c != null) {
924                try {
925                    if (c.getCount() > 0) {
926                        final int idIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID);
927                        final int intentIndex
928                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.INTENT);
929                        final int titleIndex
930                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.TITLE);
931                        final int iconTypeIndex
932                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_TYPE);
933                        final int iconIndex
934                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON);
935                        final int iconPackageIndex
936                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_PACKAGE);
937                        final int iconResourceIndex
938                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_RESOURCE);
939                        final int containerIndex
940                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CONTAINER);
941                        final int itemTypeIndex
942                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE);
943                        final int screenIndex
944                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SCREEN);
945                        final int cellXIndex
946                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLX);
947                        final int cellYIndex
948                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLY);
949                        final int uriIndex
950                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.URI);
951                        final int displayModeIndex
952                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.DISPLAY_MODE);
953                        final int profileIndex
954                                = c.getColumnIndex(LauncherSettings.Favorites.PROFILE_ID);
955
956                        int i = 0;
957                        int curX = 0;
958                        int curY = 0;
959
960                        final LauncherAppState app = LauncherAppState.getInstance();
961                        final DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
962                        final int width = (int) grid.numColumns;
963                        final int height = (int) grid.numRows;
964                        final int hotseatWidth = (int) grid.numHotseatIcons;
965
966                        final HashSet<String> seenIntents = new HashSet<String>(c.getCount());
967
968                        final ArrayList<ContentValues> shortcuts = new ArrayList<ContentValues>();
969                        final ArrayList<ContentValues> folders = new ArrayList<ContentValues>();
970                        final SparseArray<ContentValues> hotseat = new SparseArray<ContentValues>();
971
972                        while (c.moveToNext()) {
973                            final int itemType = c.getInt(itemTypeIndex);
974                            if (itemType != Favorites.ITEM_TYPE_APPLICATION
975                                    && itemType != Favorites.ITEM_TYPE_SHORTCUT
976                                    && itemType != Favorites.ITEM_TYPE_FOLDER) {
977                                continue;
978                            }
979
980                            final int cellX = c.getInt(cellXIndex);
981                            final int cellY = c.getInt(cellYIndex);
982                            final int screen = c.getInt(screenIndex);
983                            int container = c.getInt(containerIndex);
984                            final String intentStr = c.getString(intentIndex);
985
986                            UserManagerCompat userManager = UserManagerCompat.getInstance(mContext);
987                            UserHandleCompat userHandle;
988                            final long userSerialNumber;
989                            if (profileIndex != -1 && !c.isNull(profileIndex)) {
990                                userSerialNumber = c.getInt(profileIndex);
991                                userHandle = userManager.getUserForSerialNumber(userSerialNumber);
992                            } else {
993                                // Default to the serial number of this user, for older
994                                // shortcuts.
995                                userHandle = UserHandleCompat.myUserHandle();
996                                userSerialNumber = userManager.getSerialNumberForUser(userHandle);
997                            }
998
999                            if (userHandle == null) {
1000                                Launcher.addDumpLog(TAG, "skipping deleted user", true);
1001                                continue;
1002                            }
1003
1004                            Launcher.addDumpLog(TAG, "migrating \""
1005                                + c.getString(titleIndex) + "\" ("
1006                                + cellX + "," + cellY + "@"
1007                                + LauncherSettings.Favorites.containerToString(container)
1008                                + "/" + screen
1009                                + "): " + intentStr, true);
1010
1011                            if (itemType != Favorites.ITEM_TYPE_FOLDER) {
1012
1013                                final Intent intent;
1014                                final ComponentName cn;
1015                                try {
1016                                    intent = Intent.parseUri(intentStr, 0);
1017                                } catch (URISyntaxException e) {
1018                                    // bogus intent?
1019                                    Launcher.addDumpLog(TAG,
1020                                            "skipping invalid intent uri", true);
1021                                    continue;
1022                                }
1023
1024                                cn = intent.getComponent();
1025                                if (TextUtils.isEmpty(intentStr)) {
1026                                    // no intent? no icon
1027                                    Launcher.addDumpLog(TAG, "skipping empty intent", true);
1028                                    continue;
1029                                } else if (cn != null &&
1030                                        !LauncherModel.isValidPackageActivity(mContext, cn,
1031                                                userHandle)) {
1032                                    // component no longer exists.
1033                                    Launcher.addDumpLog(TAG, "skipping item whose component " +
1034                                            "no longer exists.", true);
1035                                    continue;
1036                                } else if (container ==
1037                                        LauncherSettings.Favorites.CONTAINER_DESKTOP) {
1038                                    // Dedupe icons directly on the workspace
1039
1040                                    // Canonicalize
1041                                    // the Play Store sets the package parameter, but Launcher
1042                                    // does not, so we clear that out to keep them the same.
1043                                    // Also ignore intent flags for the purposes of deduping.
1044                                    intent.setPackage(null);
1045                                    int flags = intent.getFlags();
1046                                    intent.setFlags(0);
1047                                    final String key = intent.toUri(0);
1048                                    intent.setFlags(flags);
1049                                    if (seenIntents.contains(key)) {
1050                                        Launcher.addDumpLog(TAG, "skipping duplicate", true);
1051                                        continue;
1052                                    } else {
1053                                        seenIntents.add(key);
1054                                    }
1055                                }
1056                            }
1057
1058                            ContentValues values = new ContentValues(c.getColumnCount());
1059                            values.put(LauncherSettings.Favorites._ID, c.getInt(idIndex));
1060                            values.put(LauncherSettings.Favorites.INTENT, intentStr);
1061                            values.put(LauncherSettings.Favorites.TITLE, c.getString(titleIndex));
1062                            values.put(LauncherSettings.Favorites.ICON_TYPE,
1063                                    c.getInt(iconTypeIndex));
1064                            values.put(LauncherSettings.Favorites.ICON, c.getBlob(iconIndex));
1065                            values.put(LauncherSettings.Favorites.ICON_PACKAGE,
1066                                    c.getString(iconPackageIndex));
1067                            values.put(LauncherSettings.Favorites.ICON_RESOURCE,
1068                                    c.getString(iconResourceIndex));
1069                            values.put(LauncherSettings.Favorites.ITEM_TYPE, itemType);
1070                            values.put(LauncherSettings.Favorites.APPWIDGET_ID, -1);
1071                            values.put(LauncherSettings.Favorites.URI, c.getString(uriIndex));
1072                            values.put(LauncherSettings.Favorites.DISPLAY_MODE,
1073                                    c.getInt(displayModeIndex));
1074                            values.put(LauncherSettings.Favorites.PROFILE_ID, userSerialNumber);
1075
1076                            if (container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
1077                                hotseat.put(screen, values);
1078                            }
1079
1080                            if (container != LauncherSettings.Favorites.CONTAINER_DESKTOP) {
1081                                // In a folder or in the hotseat, preserve position
1082                                values.put(LauncherSettings.Favorites.SCREEN, screen);
1083                                values.put(LauncherSettings.Favorites.CELLX, cellX);
1084                                values.put(LauncherSettings.Favorites.CELLY, cellY);
1085                            } else {
1086                                // For items contained directly on one of the workspace screen,
1087                                // we'll determine their location (screen, x, y) in a second pass.
1088                            }
1089
1090                            values.put(LauncherSettings.Favorites.CONTAINER, container);
1091
1092                            if (itemType != Favorites.ITEM_TYPE_FOLDER) {
1093                                shortcuts.add(values);
1094                            } else {
1095                                folders.add(values);
1096                            }
1097                        }
1098
1099                        // Now that we have all the hotseat icons, let's go through them left-right
1100                        // and assign valid locations for them in the new hotseat
1101                        final int N = hotseat.size();
1102                        for (int idx=0; idx<N; idx++) {
1103                            int hotseatX = hotseat.keyAt(idx);
1104                            ContentValues values = hotseat.valueAt(idx);
1105
1106                            if (hotseatX == grid.hotseatAllAppsRank) {
1107                                // let's drop this in the next available hole in the hotseat
1108                                while (++hotseatX < hotseatWidth) {
1109                                    if (hotseat.get(hotseatX) == null) {
1110                                        // found a spot! move it here
1111                                        values.put(LauncherSettings.Favorites.SCREEN,
1112                                                hotseatX);
1113                                        break;
1114                                    }
1115                                }
1116                            }
1117                            if (hotseatX >= hotseatWidth) {
1118                                // no room for you in the hotseat? it's off to the desktop with you
1119                                values.put(LauncherSettings.Favorites.CONTAINER,
1120                                           Favorites.CONTAINER_DESKTOP);
1121                            }
1122                        }
1123
1124                        final ArrayList<ContentValues> allItems = new ArrayList<ContentValues>();
1125                        // Folders first
1126                        allItems.addAll(folders);
1127                        // Then shortcuts
1128                        allItems.addAll(shortcuts);
1129
1130                        // Layout all the folders
1131                        for (ContentValues values: allItems) {
1132                            if (values.getAsInteger(LauncherSettings.Favorites.CONTAINER) !=
1133                                    LauncherSettings.Favorites.CONTAINER_DESKTOP) {
1134                                // Hotseat items and folder items have already had their
1135                                // location information set. Nothing to be done here.
1136                                continue;
1137                            }
1138                            values.put(LauncherSettings.Favorites.SCREEN, curScreen);
1139                            values.put(LauncherSettings.Favorites.CELLX, curX);
1140                            values.put(LauncherSettings.Favorites.CELLY, curY);
1141                            curX = (curX + 1) % width;
1142                            if (curX == 0) {
1143                                curY = (curY + 1);
1144                            }
1145                            // Leave the last row of icons blank on every screen
1146                            if (curY == height - 1) {
1147                                curScreen = (int) generateNewScreenId();
1148                                curY = 0;
1149                            }
1150                        }
1151
1152                        if (allItems.size() > 0) {
1153                            db.beginTransaction();
1154                            try {
1155                                for (ContentValues row: allItems) {
1156                                    if (row == null) continue;
1157                                    if (dbInsertAndCheck(this, db, TABLE_FAVORITES, null, row)
1158                                            < 0) {
1159                                        return;
1160                                    } else {
1161                                        count++;
1162                                    }
1163                                }
1164                                db.setTransactionSuccessful();
1165                            } finally {
1166                                db.endTransaction();
1167                            }
1168                        }
1169
1170                        db.beginTransaction();
1171                        try {
1172                            for (i=0; i<=curScreen; i++) {
1173                                final ContentValues values = new ContentValues();
1174                                values.put(LauncherSettings.WorkspaceScreens._ID, i);
1175                                values.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, i);
1176                                if (dbInsertAndCheck(this, db, TABLE_WORKSPACE_SCREENS, null, values)
1177                                        < 0) {
1178                                    return;
1179                                }
1180                            }
1181                            db.setTransactionSuccessful();
1182                        } finally {
1183                            db.endTransaction();
1184                        }
1185
1186                        updateFolderItemsRank(db, false);
1187                    }
1188                } finally {
1189                    c.close();
1190                }
1191            }
1192
1193            Launcher.addDumpLog(TAG, "migrated " + count + " icons from Launcher2 into "
1194                    + (curScreen+1) + " screens", true);
1195
1196            // ensure that new screens are created to hold these icons
1197            setFlagJustLoadedOldDb();
1198
1199            // Update max IDs; very important since we just grabbed IDs from another database
1200            mMaxItemId = initializeMaxItemId(db);
1201            mMaxScreenId = initializeMaxScreenId(db);
1202            if (LOGD) Log.d(TAG, "mMaxItemId: " + mMaxItemId + " mMaxScreenId: " + mMaxScreenId);
1203        }
1204    }
1205
1206    static class SqlArguments {
1207        public final String table;
1208        public final String where;
1209        public final String[] args;
1210
1211        SqlArguments(Uri url, String where, String[] args) {
1212            if (url.getPathSegments().size() == 1) {
1213                this.table = url.getPathSegments().get(0);
1214                this.where = where;
1215                this.args = args;
1216            } else if (url.getPathSegments().size() != 2) {
1217                throw new IllegalArgumentException("Invalid URI: " + url);
1218            } else if (!TextUtils.isEmpty(where)) {
1219                throw new UnsupportedOperationException("WHERE clause not supported: " + url);
1220            } else {
1221                this.table = url.getPathSegments().get(0);
1222                this.where = "_id=" + ContentUris.parseId(url);
1223                this.args = null;
1224            }
1225        }
1226
1227        SqlArguments(Uri url) {
1228            if (url.getPathSegments().size() == 1) {
1229                table = url.getPathSegments().get(0);
1230                where = null;
1231                args = null;
1232            } else {
1233                throw new IllegalArgumentException("Invalid URI: " + url);
1234            }
1235        }
1236    }
1237}
1238