LauncherProvider.java revision 14334bda039b0f70980e8782e379c5df059a5d6e
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 = 21;
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 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            // Populate favorites table with initial favorites
301            if ((mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), loader) <= 0)
302                    && usingExternallyProvidedLayout) {
303                // Unable to load external layout. Cleanup and load the internal layout.
304                createEmptyDB();
305                mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(),
306                        getDefaultLayoutParser());
307            }
308            clearFlagEmptyDbCreated();
309        }
310    }
311
312    private DefaultLayoutParser getDefaultLayoutParser() {
313        int defaultLayout = LauncherAppState.getInstance()
314                .getDynamicGrid().getDeviceProfile().defaultLayoutId;
315        return new DefaultLayoutParser(getContext(), mOpenHelper.mAppWidgetHost,
316                mOpenHelper, getContext().getResources(), defaultLayout);
317    }
318
319    public void migrateLauncher2Shortcuts() {
320        mOpenHelper.migrateLauncher2Shortcuts(mOpenHelper.getWritableDatabase(),
321                Uri.parse(getContext().getString(R.string.old_launcher_provider_uri)));
322    }
323
324    public void updateFolderItemsRank() {
325        mOpenHelper.updateFolderItemsRank(mOpenHelper.getWritableDatabase(), false);
326    }
327
328    public void deleteDatabase() {
329        // Are you sure? (y/n)
330        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
331        final File dbFile = new File(db.getPath());
332        mOpenHelper.close();
333        if (dbFile.exists()) {
334            SQLiteDatabase.deleteDatabase(dbFile);
335        }
336        mOpenHelper = new DatabaseHelper(getContext());
337    }
338
339    private static class DatabaseHelper extends SQLiteOpenHelper implements LayoutParserCallback {
340        private final Context mContext;
341        private final AppWidgetHost mAppWidgetHost;
342        private long mMaxItemId = -1;
343        private long mMaxScreenId = -1;
344
345        private boolean mNewDbCreated = false;
346
347        DatabaseHelper(Context context) {
348            super(context, LauncherFiles.LAUNCHER_DB, null, DATABASE_VERSION);
349            mContext = context;
350            mAppWidgetHost = new AppWidgetHost(context, Launcher.APPWIDGET_HOST_ID);
351
352            // In the case where neither onCreate nor onUpgrade gets called, we read the maxId from
353            // the DB here
354            if (mMaxItemId == -1) {
355                mMaxItemId = initializeMaxItemId(getWritableDatabase());
356            }
357            if (mMaxScreenId == -1) {
358                mMaxScreenId = initializeMaxScreenId(getWritableDatabase());
359            }
360        }
361
362        public boolean wasNewDbCreated() {
363            return mNewDbCreated;
364        }
365
366        /**
367         * Send notification that we've deleted the {@link AppWidgetHost},
368         * probably as part of the initial database creation. The receiver may
369         * want to re-call {@link AppWidgetHost#startListening()} to ensure
370         * callbacks are correctly set.
371         */
372        private void sendAppWidgetResetNotify() {
373            final ContentResolver resolver = mContext.getContentResolver();
374            resolver.notifyChange(CONTENT_APPWIDGET_RESET_URI, null);
375        }
376
377        @Override
378        public void onCreate(SQLiteDatabase db) {
379            if (LOGD) Log.d(TAG, "creating new launcher database");
380
381            mMaxItemId = 1;
382            mMaxScreenId = 0;
383            mNewDbCreated = true;
384
385            UserManagerCompat userManager = UserManagerCompat.getInstance(mContext);
386            long userSerialNumber = userManager.getSerialNumberForUser(
387                    UserHandleCompat.myUserHandle());
388
389            db.execSQL("CREATE TABLE favorites (" +
390                    "_id INTEGER PRIMARY KEY," +
391                    "title TEXT," +
392                    "intent TEXT," +
393                    "container INTEGER," +
394                    "screen INTEGER," +
395                    "cellX INTEGER," +
396                    "cellY INTEGER," +
397                    "spanX INTEGER," +
398                    "spanY INTEGER," +
399                    "itemType INTEGER," +
400                    "appWidgetId INTEGER NOT NULL DEFAULT -1," +
401                    "isShortcut INTEGER," +
402                    "iconType INTEGER," +
403                    "iconPackage TEXT," +
404                    "iconResource TEXT," +
405                    "icon BLOB," +
406                    "uri TEXT," +
407                    "displayMode INTEGER," +
408                    "appWidgetProvider TEXT," +
409                    "modified INTEGER NOT NULL DEFAULT 0," +
410                    "restored INTEGER NOT NULL DEFAULT 0," +
411                    "profileId INTEGER DEFAULT " + userSerialNumber + "," +
412                    "rank INTEGER NOT NULL DEFAULT 0" +
413                    ");");
414            addWorkspacesTable(db);
415
416            // Database was just created, so wipe any previous widgets
417            if (mAppWidgetHost != null) {
418                mAppWidgetHost.deleteHost();
419                sendAppWidgetResetNotify();
420            }
421
422            // Fresh and clean launcher DB.
423            mMaxItemId = initializeMaxItemId(db);
424            setFlagEmptyDbCreated();
425        }
426
427        private void addWorkspacesTable(SQLiteDatabase db) {
428            db.execSQL("CREATE TABLE " + TABLE_WORKSPACE_SCREENS + " (" +
429                    LauncherSettings.WorkspaceScreens._ID + " INTEGER," +
430                    LauncherSettings.WorkspaceScreens.SCREEN_RANK + " INTEGER," +
431                    LauncherSettings.ChangeLogColumns.MODIFIED + " INTEGER NOT NULL DEFAULT 0" +
432                    ");");
433        }
434
435        private void removeOrphanedItems(SQLiteDatabase db) {
436            // Delete items directly on the workspace who's screen id doesn't exist
437            //  "DELETE FROM favorites WHERE screen NOT IN (SELECT _id FROM workspaceScreens)
438            //   AND container = -100"
439            String removeOrphanedDesktopItems = "DELETE FROM " + TABLE_FAVORITES +
440                    " WHERE " +
441                    LauncherSettings.Favorites.SCREEN + " NOT IN (SELECT " +
442                    LauncherSettings.WorkspaceScreens._ID + " FROM " + TABLE_WORKSPACE_SCREENS + ")" +
443                    " AND " +
444                    LauncherSettings.Favorites.CONTAINER + " = " +
445                    LauncherSettings.Favorites.CONTAINER_DESKTOP;
446            db.execSQL(removeOrphanedDesktopItems);
447
448            // Delete items contained in folders which no longer exist (after above statement)
449            //  "DELETE FROM favorites  WHERE container <> -100 AND container <> -101 AND container
450            //   NOT IN (SELECT _id FROM favorites WHERE itemType = 2)"
451            String removeOrphanedFolderItems = "DELETE FROM " + TABLE_FAVORITES +
452                    " WHERE " +
453                    LauncherSettings.Favorites.CONTAINER + " <> " +
454                    LauncherSettings.Favorites.CONTAINER_DESKTOP +
455                    " AND "
456                    + LauncherSettings.Favorites.CONTAINER + " <> " +
457                    LauncherSettings.Favorites.CONTAINER_HOTSEAT +
458                    " AND "
459                    + LauncherSettings.Favorites.CONTAINER + " NOT IN (SELECT " +
460                    LauncherSettings.Favorites._ID + " FROM " + TABLE_FAVORITES +
461                    " WHERE " + LauncherSettings.Favorites.ITEM_TYPE + " = " +
462                    LauncherSettings.Favorites.ITEM_TYPE_FOLDER + ")";
463            db.execSQL(removeOrphanedFolderItems);
464        }
465
466        private void setFlagJustLoadedOldDb() {
467            String spKey = LauncherAppState.getSharedPreferencesKey();
468            SharedPreferences sp = mContext.getSharedPreferences(spKey, Context.MODE_PRIVATE);
469            sp.edit().putBoolean(EMPTY_DATABASE_CREATED, false).commit();
470        }
471
472        private void setFlagEmptyDbCreated() {
473            String spKey = LauncherAppState.getSharedPreferencesKey();
474            SharedPreferences sp = mContext.getSharedPreferences(spKey, Context.MODE_PRIVATE);
475            sp.edit().putBoolean(EMPTY_DATABASE_CREATED, true).commit();
476        }
477
478        @Override
479        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
480            if (LOGD) Log.d(TAG, "onUpgrade triggered: " + oldVersion);
481            switch (oldVersion) {
482                // The version cannot be lower that 12, as Launcher3 never supported a lower
483                // version of the DB.
484                case 12: {
485                    // With the new shrink-wrapped and re-orderable workspaces, it makes sense
486                    // to persist workspace screens and their relative order.
487                    mMaxScreenId = 0;
488                    addWorkspacesTable(db);
489                }
490                case 13: {
491                    db.beginTransaction();
492                    try {
493                        // Insert new column for holding widget provider name
494                        db.execSQL("ALTER TABLE favorites " +
495                                "ADD COLUMN appWidgetProvider TEXT;");
496                        db.setTransactionSuccessful();
497                    } catch (SQLException ex) {
498                        Log.e(TAG, ex.getMessage(), ex);
499                        // Old version remains, which means we wipe old data
500                        break;
501                    } finally {
502                        db.endTransaction();
503                    }
504                }
505                case 14: {
506                    db.beginTransaction();
507                    try {
508                        // Insert new column for holding update timestamp
509                        db.execSQL("ALTER TABLE favorites " +
510                                "ADD COLUMN modified INTEGER NOT NULL DEFAULT 0;");
511                        db.execSQL("ALTER TABLE workspaceScreens " +
512                                "ADD COLUMN modified INTEGER NOT NULL DEFAULT 0;");
513                        db.setTransactionSuccessful();
514                    } catch (SQLException ex) {
515                        Log.e(TAG, ex.getMessage(), ex);
516                        // Old version remains, which means we wipe old data
517                        break;
518                    } finally {
519                        db.endTransaction();
520                    }
521                }
522                case 15: {
523                    db.beginTransaction();
524                    try {
525                        // Insert new column for holding restore status
526                        db.execSQL("ALTER TABLE favorites " +
527                                "ADD COLUMN restored INTEGER NOT NULL DEFAULT 0;");
528                        db.setTransactionSuccessful();
529                    } catch (SQLException ex) {
530                        Log.e(TAG, ex.getMessage(), ex);
531                        // Old version remains, which means we wipe old data
532                        break;
533                    } finally {
534                        db.endTransaction();
535                    }
536                }
537                case 16: {
538                    // We use the db version upgrade here to identify users who may not have seen
539                    // clings yet (because they weren't available), but for whom the clings are now
540                    // available (tablet users). Because one of the possible cling flows (migration)
541                    // is very destructive (wipes out workspaces), we want to prevent this from showing
542                    // until clear data. We do so by marking that the clings have been shown.
543                    LauncherClings.synchonouslyMarkFirstRunClingDismissed(mContext);
544                }
545                case 17: {
546                    // No-op
547                }
548                case 18: {
549                    // Due to a data loss bug, some users may have items associated with screen ids
550                    // which no longer exist. Since this can cause other problems, and since the user
551                    // will never see these items anyway, we use database upgrade as an opportunity to
552                    // clean things up.
553                    removeOrphanedItems(db);
554                }
555                case 19: {
556                    // Add userId column
557                    if (!addProfileColumn(db)) {
558                        // Old version remains, which means we wipe old data
559                        break;
560                    }
561                }
562                case 20:
563                    if (!updateFolderItemsRank(db, true)) {
564                        break;
565                    }
566                case 21: {
567                    // DB Upgraded successfully
568                    return;
569                }
570            }
571
572            // DB was not upgraded
573            Log.w(TAG, "Destroying all old data.");
574            createEmptyDB(db);
575        }
576
577        @Override
578        public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
579            // This shouldn't happen -- throw our hands up in the air and start over.
580            Log.w(TAG, "Database version downgrade from: " + oldVersion + " to " + newVersion +
581                    ". Wiping databse.");
582            createEmptyDB(db);
583        }
584
585
586        /**
587         * Clears all the data for a fresh start.
588         */
589        public void createEmptyDB(SQLiteDatabase db) {
590            db.execSQL("DROP TABLE IF EXISTS " + TABLE_FAVORITES);
591            db.execSQL("DROP TABLE IF EXISTS " + TABLE_WORKSPACE_SCREENS);
592            onCreate(db);
593        }
594
595        private boolean updateFolderItemsRank(SQLiteDatabase db, boolean addRankColumn) {
596            db.beginTransaction();
597            try {
598                if (addRankColumn) {
599                    // Insert new column for holding rank
600                    db.execSQL("ALTER TABLE favorites ADD COLUMN rank INTEGER NOT NULL DEFAULT 0;");
601                }
602
603                // Get a map for folder ID to folder width
604                Cursor c = db.rawQuery("SELECT container, MAX(cellX) FROM favorites"
605                        + " WHERE container IN (SELECT _id FROM favorites WHERE itemType = ?)"
606                        + " GROUP BY container;",
607                        new String[] {Integer.toString(LauncherSettings.Favorites.ITEM_TYPE_FOLDER)});
608
609                while (c.moveToNext()) {
610                    db.execSQL("UPDATE favorites SET rank=cellX+(cellY*?) WHERE "
611                            + "container=? AND cellX IS NOT NULL AND cellY IS NOT NULL;",
612                            new Object[] {c.getLong(1) + 1, c.getLong(0)});
613                }
614
615                c.close();
616                db.setTransactionSuccessful();
617            } catch (SQLException ex) {
618                // Old version remains, which means we wipe old data
619                Log.e(TAG, ex.getMessage(), ex);
620                return false;
621            } finally {
622                db.endTransaction();
623            }
624            return true;
625        }
626
627        private boolean addProfileColumn(SQLiteDatabase db) {
628            db.beginTransaction();
629            try {
630                UserManagerCompat userManager = UserManagerCompat.getInstance(mContext);
631                // Default to the serial number of this user, for older
632                // shortcuts.
633                long userSerialNumber = userManager.getSerialNumberForUser(
634                        UserHandleCompat.myUserHandle());
635                // Insert new column for holding user serial number
636                db.execSQL("ALTER TABLE favorites " +
637                        "ADD COLUMN profileId INTEGER DEFAULT "
638                                        + userSerialNumber + ";");
639                db.setTransactionSuccessful();
640            } catch (SQLException ex) {
641                // Old version remains, which means we wipe old data
642                Log.e(TAG, ex.getMessage(), ex);
643                return false;
644            } finally {
645                db.endTransaction();
646            }
647            return true;
648        }
649
650        // Generates a new ID to use for an object in your database. This method should be only
651        // called from the main UI thread. As an exception, we do call it when we call the
652        // constructor from the worker thread; however, this doesn't extend until after the
653        // constructor is called, and we only pass a reference to LauncherProvider to LauncherApp
654        // after that point
655        @Override
656        public long generateNewItemId() {
657            if (mMaxItemId < 0) {
658                throw new RuntimeException("Error: max item id was not initialized");
659            }
660            mMaxItemId += 1;
661            return mMaxItemId;
662        }
663
664        @Override
665        public long insertAndCheck(SQLiteDatabase db, ContentValues values) {
666            return dbInsertAndCheck(this, db, TABLE_FAVORITES, null, values);
667        }
668
669        public void updateMaxItemId(long id) {
670            mMaxItemId = id + 1;
671        }
672
673        public void checkId(String table, ContentValues values) {
674            long id = values.getAsLong(LauncherSettings.BaseLauncherColumns._ID);
675            if (table == LauncherProvider.TABLE_WORKSPACE_SCREENS) {
676                mMaxScreenId = Math.max(id, mMaxScreenId);
677            }  else {
678                mMaxItemId = Math.max(id, mMaxItemId);
679            }
680        }
681
682        private long initializeMaxItemId(SQLiteDatabase db) {
683            Cursor c = db.rawQuery("SELECT MAX(_id) FROM favorites", null);
684
685            // get the result
686            final int maxIdIndex = 0;
687            long id = -1;
688            if (c != null && c.moveToNext()) {
689                id = c.getLong(maxIdIndex);
690            }
691            if (c != null) {
692                c.close();
693            }
694
695            if (id == -1) {
696                throw new RuntimeException("Error: could not query max item id");
697            }
698
699            return id;
700        }
701
702        // Generates a new ID to use for an workspace screen in your database. This method
703        // should be only called from the main UI thread. As an exception, we do call it when we
704        // call the constructor from the worker thread; however, this doesn't extend until after the
705        // constructor is called, and we only pass a reference to LauncherProvider to LauncherApp
706        // after that point
707        public long generateNewScreenId() {
708            if (mMaxScreenId < 0) {
709                throw new RuntimeException("Error: max screen id was not initialized");
710            }
711            mMaxScreenId += 1;
712            // Log to disk
713            Launcher.addDumpLog(TAG, "11683562 - generateNewScreenId(): " + mMaxScreenId, true);
714            return mMaxScreenId;
715        }
716
717        private long initializeMaxScreenId(SQLiteDatabase db) {
718            Cursor c = db.rawQuery("SELECT MAX(" + LauncherSettings.WorkspaceScreens._ID + ") FROM " + TABLE_WORKSPACE_SCREENS, null);
719
720            // get the result
721            final int maxIdIndex = 0;
722            long id = -1;
723            if (c != null && c.moveToNext()) {
724                id = c.getLong(maxIdIndex);
725            }
726            if (c != null) {
727                c.close();
728            }
729
730            if (id == -1) {
731                throw new RuntimeException("Error: could not query max screen id");
732            }
733
734            // Log to disk
735            Launcher.addDumpLog(TAG, "11683562 - initializeMaxScreenId(): " + id, true);
736            return id;
737        }
738
739        private boolean initializeExternalAdd(ContentValues values) {
740            // 1. Ensure that externally added items have a valid item id
741            long id = generateNewItemId();
742            values.put(LauncherSettings.Favorites._ID, id);
743
744            // 2. In the case of an app widget, and if no app widget id is specified, we
745            // attempt allocate and bind the widget.
746            Integer itemType = values.getAsInteger(LauncherSettings.Favorites.ITEM_TYPE);
747            if (itemType != null &&
748                    itemType.intValue() == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET &&
749                    !values.containsKey(LauncherSettings.Favorites.APPWIDGET_ID)) {
750
751                final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext);
752                ComponentName cn = ComponentName.unflattenFromString(
753                        values.getAsString(Favorites.APPWIDGET_PROVIDER));
754
755                if (cn != null) {
756                    try {
757                        int appWidgetId = mAppWidgetHost.allocateAppWidgetId();
758                        values.put(LauncherSettings.Favorites.APPWIDGET_ID, appWidgetId);
759                        if (!appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId,cn)) {
760                            return false;
761                        }
762                    } catch (RuntimeException e) {
763                        Log.e(TAG, "Failed to initialize external widget", e);
764                        return false;
765                    }
766                } else {
767                    return false;
768                }
769            }
770
771            // Add screen id if not present
772            long screenId = values.getAsLong(LauncherSettings.Favorites.SCREEN);
773            if (!addScreenIdIfNecessary(screenId)) {
774                return false;
775            }
776            return true;
777        }
778
779        // Returns true of screen id exists, or if successfully added
780        private boolean addScreenIdIfNecessary(long screenId) {
781            if (!hasScreenId(screenId)) {
782                int rank = getMaxScreenRank() + 1;
783
784                ContentValues v = new ContentValues();
785                v.put(LauncherSettings.WorkspaceScreens._ID, screenId);
786                v.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, rank);
787                if (dbInsertAndCheck(this, getWritableDatabase(),
788                        TABLE_WORKSPACE_SCREENS, null, v) < 0) {
789                    return false;
790                }
791            }
792            return true;
793        }
794
795        private boolean hasScreenId(long screenId) {
796            SQLiteDatabase db = getWritableDatabase();
797            Cursor c = db.rawQuery("SELECT * FROM " + TABLE_WORKSPACE_SCREENS + " WHERE "
798                    + LauncherSettings.WorkspaceScreens._ID + " = " + screenId, null);
799            if (c != null) {
800                int count = c.getCount();
801                c.close();
802                return count > 0;
803            } else {
804                return false;
805            }
806        }
807
808        private int getMaxScreenRank() {
809            SQLiteDatabase db = getWritableDatabase();
810            Cursor c = db.rawQuery("SELECT MAX(" + LauncherSettings.WorkspaceScreens.SCREEN_RANK
811                    + ") FROM " + TABLE_WORKSPACE_SCREENS, null);
812
813            // get the result
814            final int maxRankIndex = 0;
815            int rank = -1;
816            if (c != null && c.moveToNext()) {
817                rank = c.getInt(maxRankIndex);
818            }
819            if (c != null) {
820                c.close();
821            }
822
823            return rank;
824        }
825
826        private int loadFavorites(SQLiteDatabase db, AutoInstallsLayout loader) {
827            ArrayList<Long> screenIds = new ArrayList<Long>();
828            // TODO: Use multiple loaders with fall-back and transaction.
829            int count = loader.loadLayout(db, screenIds);
830
831            // Add the screens specified by the items above
832            Collections.sort(screenIds);
833            int rank = 0;
834            ContentValues values = new ContentValues();
835            for (Long id : screenIds) {
836                values.clear();
837                values.put(LauncherSettings.WorkspaceScreens._ID, id);
838                values.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, rank);
839                if (dbInsertAndCheck(this, db, TABLE_WORKSPACE_SCREENS, null, values) < 0) {
840                    throw new RuntimeException("Failed initialize screen table"
841                            + "from default layout");
842                }
843                rank++;
844            }
845
846            // Ensure that the max ids are initialized
847            mMaxItemId = initializeMaxItemId(db);
848            mMaxScreenId = initializeMaxScreenId(db);
849
850            return count;
851        }
852
853        private void migrateLauncher2Shortcuts(SQLiteDatabase db, Uri uri) {
854            final ContentResolver resolver = mContext.getContentResolver();
855            Cursor c = null;
856            int count = 0;
857            int curScreen = 0;
858
859            try {
860                c = resolver.query(uri, null, null, null, "title ASC");
861            } catch (Exception e) {
862                // Ignore
863            }
864
865            // We already have a favorites database in the old provider
866            if (c != null) {
867                try {
868                    if (c.getCount() > 0) {
869                        final int idIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID);
870                        final int intentIndex
871                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.INTENT);
872                        final int titleIndex
873                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.TITLE);
874                        final int iconTypeIndex
875                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_TYPE);
876                        final int iconIndex
877                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON);
878                        final int iconPackageIndex
879                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_PACKAGE);
880                        final int iconResourceIndex
881                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_RESOURCE);
882                        final int containerIndex
883                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CONTAINER);
884                        final int itemTypeIndex
885                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE);
886                        final int screenIndex
887                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SCREEN);
888                        final int cellXIndex
889                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLX);
890                        final int cellYIndex
891                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLY);
892                        final int uriIndex
893                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.URI);
894                        final int displayModeIndex
895                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.DISPLAY_MODE);
896                        final int profileIndex
897                                = c.getColumnIndex(LauncherSettings.Favorites.PROFILE_ID);
898
899                        int i = 0;
900                        int curX = 0;
901                        int curY = 0;
902
903                        final LauncherAppState app = LauncherAppState.getInstance();
904                        final DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
905                        final int width = (int) grid.numColumns;
906                        final int height = (int) grid.numRows;
907                        final int hotseatWidth = (int) grid.numHotseatIcons;
908
909                        final HashSet<String> seenIntents = new HashSet<String>(c.getCount());
910
911                        final ArrayList<ContentValues> shortcuts = new ArrayList<ContentValues>();
912                        final ArrayList<ContentValues> folders = new ArrayList<ContentValues>();
913                        final SparseArray<ContentValues> hotseat = new SparseArray<ContentValues>();
914
915                        while (c.moveToNext()) {
916                            final int itemType = c.getInt(itemTypeIndex);
917                            if (itemType != Favorites.ITEM_TYPE_APPLICATION
918                                    && itemType != Favorites.ITEM_TYPE_SHORTCUT
919                                    && itemType != Favorites.ITEM_TYPE_FOLDER) {
920                                continue;
921                            }
922
923                            final int cellX = c.getInt(cellXIndex);
924                            final int cellY = c.getInt(cellYIndex);
925                            final int screen = c.getInt(screenIndex);
926                            int container = c.getInt(containerIndex);
927                            final String intentStr = c.getString(intentIndex);
928
929                            UserManagerCompat userManager = UserManagerCompat.getInstance(mContext);
930                            UserHandleCompat userHandle;
931                            final long userSerialNumber;
932                            if (profileIndex != -1 && !c.isNull(profileIndex)) {
933                                userSerialNumber = c.getInt(profileIndex);
934                                userHandle = userManager.getUserForSerialNumber(userSerialNumber);
935                            } else {
936                                // Default to the serial number of this user, for older
937                                // shortcuts.
938                                userHandle = UserHandleCompat.myUserHandle();
939                                userSerialNumber = userManager.getSerialNumberForUser(userHandle);
940                            }
941
942                            if (userHandle == null) {
943                                Launcher.addDumpLog(TAG, "skipping deleted user", true);
944                                continue;
945                            }
946
947                            Launcher.addDumpLog(TAG, "migrating \""
948                                + c.getString(titleIndex) + "\" ("
949                                + cellX + "," + cellY + "@"
950                                + LauncherSettings.Favorites.containerToString(container)
951                                + "/" + screen
952                                + "): " + intentStr, true);
953
954                            if (itemType != Favorites.ITEM_TYPE_FOLDER) {
955
956                                final Intent intent;
957                                final ComponentName cn;
958                                try {
959                                    intent = Intent.parseUri(intentStr, 0);
960                                } catch (URISyntaxException e) {
961                                    // bogus intent?
962                                    Launcher.addDumpLog(TAG,
963                                            "skipping invalid intent uri", true);
964                                    continue;
965                                }
966
967                                cn = intent.getComponent();
968                                if (TextUtils.isEmpty(intentStr)) {
969                                    // no intent? no icon
970                                    Launcher.addDumpLog(TAG, "skipping empty intent", true);
971                                    continue;
972                                } else if (cn != null &&
973                                        !LauncherModel.isValidPackageActivity(mContext, cn,
974                                                userHandle)) {
975                                    // component no longer exists.
976                                    Launcher.addDumpLog(TAG, "skipping item whose component " +
977                                            "no longer exists.", true);
978                                    continue;
979                                } else if (container ==
980                                        LauncherSettings.Favorites.CONTAINER_DESKTOP) {
981                                    // Dedupe icons directly on the workspace
982
983                                    // Canonicalize
984                                    // the Play Store sets the package parameter, but Launcher
985                                    // does not, so we clear that out to keep them the same.
986                                    // Also ignore intent flags for the purposes of deduping.
987                                    intent.setPackage(null);
988                                    int flags = intent.getFlags();
989                                    intent.setFlags(0);
990                                    final String key = intent.toUri(0);
991                                    intent.setFlags(flags);
992                                    if (seenIntents.contains(key)) {
993                                        Launcher.addDumpLog(TAG, "skipping duplicate", true);
994                                        continue;
995                                    } else {
996                                        seenIntents.add(key);
997                                    }
998                                }
999                            }
1000
1001                            ContentValues values = new ContentValues(c.getColumnCount());
1002                            values.put(LauncherSettings.Favorites._ID, c.getInt(idIndex));
1003                            values.put(LauncherSettings.Favorites.INTENT, intentStr);
1004                            values.put(LauncherSettings.Favorites.TITLE, c.getString(titleIndex));
1005                            values.put(LauncherSettings.Favorites.ICON_TYPE,
1006                                    c.getInt(iconTypeIndex));
1007                            values.put(LauncherSettings.Favorites.ICON, c.getBlob(iconIndex));
1008                            values.put(LauncherSettings.Favorites.ICON_PACKAGE,
1009                                    c.getString(iconPackageIndex));
1010                            values.put(LauncherSettings.Favorites.ICON_RESOURCE,
1011                                    c.getString(iconResourceIndex));
1012                            values.put(LauncherSettings.Favorites.ITEM_TYPE, itemType);
1013                            values.put(LauncherSettings.Favorites.APPWIDGET_ID, -1);
1014                            values.put(LauncherSettings.Favorites.URI, c.getString(uriIndex));
1015                            values.put(LauncherSettings.Favorites.DISPLAY_MODE,
1016                                    c.getInt(displayModeIndex));
1017                            values.put(LauncherSettings.Favorites.PROFILE_ID, userSerialNumber);
1018
1019                            if (container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
1020                                hotseat.put(screen, values);
1021                            }
1022
1023                            if (container != LauncherSettings.Favorites.CONTAINER_DESKTOP) {
1024                                // In a folder or in the hotseat, preserve position
1025                                values.put(LauncherSettings.Favorites.SCREEN, screen);
1026                                values.put(LauncherSettings.Favorites.CELLX, cellX);
1027                                values.put(LauncherSettings.Favorites.CELLY, cellY);
1028                            } else {
1029                                // For items contained directly on one of the workspace screen,
1030                                // we'll determine their location (screen, x, y) in a second pass.
1031                            }
1032
1033                            values.put(LauncherSettings.Favorites.CONTAINER, container);
1034
1035                            if (itemType != Favorites.ITEM_TYPE_FOLDER) {
1036                                shortcuts.add(values);
1037                            } else {
1038                                folders.add(values);
1039                            }
1040                        }
1041
1042                        // Now that we have all the hotseat icons, let's go through them left-right
1043                        // and assign valid locations for them in the new hotseat
1044                        final int N = hotseat.size();
1045                        for (int idx=0; idx<N; idx++) {
1046                            int hotseatX = hotseat.keyAt(idx);
1047                            ContentValues values = hotseat.valueAt(idx);
1048
1049                            if (hotseatX == grid.hotseatAllAppsRank) {
1050                                // let's drop this in the next available hole in the hotseat
1051                                while (++hotseatX < hotseatWidth) {
1052                                    if (hotseat.get(hotseatX) == null) {
1053                                        // found a spot! move it here
1054                                        values.put(LauncherSettings.Favorites.SCREEN,
1055                                                hotseatX);
1056                                        break;
1057                                    }
1058                                }
1059                            }
1060                            if (hotseatX >= hotseatWidth) {
1061                                // no room for you in the hotseat? it's off to the desktop with you
1062                                values.put(LauncherSettings.Favorites.CONTAINER,
1063                                           Favorites.CONTAINER_DESKTOP);
1064                            }
1065                        }
1066
1067                        final ArrayList<ContentValues> allItems = new ArrayList<ContentValues>();
1068                        // Folders first
1069                        allItems.addAll(folders);
1070                        // Then shortcuts
1071                        allItems.addAll(shortcuts);
1072
1073                        // Layout all the folders
1074                        for (ContentValues values: allItems) {
1075                            if (values.getAsInteger(LauncherSettings.Favorites.CONTAINER) !=
1076                                    LauncherSettings.Favorites.CONTAINER_DESKTOP) {
1077                                // Hotseat items and folder items have already had their
1078                                // location information set. Nothing to be done here.
1079                                continue;
1080                            }
1081                            values.put(LauncherSettings.Favorites.SCREEN, curScreen);
1082                            values.put(LauncherSettings.Favorites.CELLX, curX);
1083                            values.put(LauncherSettings.Favorites.CELLY, curY);
1084                            curX = (curX + 1) % width;
1085                            if (curX == 0) {
1086                                curY = (curY + 1);
1087                            }
1088                            // Leave the last row of icons blank on every screen
1089                            if (curY == height - 1) {
1090                                curScreen = (int) generateNewScreenId();
1091                                curY = 0;
1092                            }
1093                        }
1094
1095                        if (allItems.size() > 0) {
1096                            db.beginTransaction();
1097                            try {
1098                                for (ContentValues row: allItems) {
1099                                    if (row == null) continue;
1100                                    if (dbInsertAndCheck(this, db, TABLE_FAVORITES, null, row)
1101                                            < 0) {
1102                                        return;
1103                                    } else {
1104                                        count++;
1105                                    }
1106                                }
1107                                db.setTransactionSuccessful();
1108                            } finally {
1109                                db.endTransaction();
1110                            }
1111                        }
1112
1113                        db.beginTransaction();
1114                        try {
1115                            for (i=0; i<=curScreen; i++) {
1116                                final ContentValues values = new ContentValues();
1117                                values.put(LauncherSettings.WorkspaceScreens._ID, i);
1118                                values.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, i);
1119                                if (dbInsertAndCheck(this, db, TABLE_WORKSPACE_SCREENS, null, values)
1120                                        < 0) {
1121                                    return;
1122                                }
1123                            }
1124                            db.setTransactionSuccessful();
1125                        } finally {
1126                            db.endTransaction();
1127                        }
1128
1129                        updateFolderItemsRank(db, false);
1130                    }
1131                } finally {
1132                    c.close();
1133                }
1134            }
1135
1136            Launcher.addDumpLog(TAG, "migrated " + count + " icons from Launcher2 into "
1137                    + (curScreen+1) + " screens", true);
1138
1139            // ensure that new screens are created to hold these icons
1140            setFlagJustLoadedOldDb();
1141
1142            // Update max IDs; very important since we just grabbed IDs from another database
1143            mMaxItemId = initializeMaxItemId(db);
1144            mMaxScreenId = initializeMaxScreenId(db);
1145            if (LOGD) Log.d(TAG, "mMaxItemId: " + mMaxItemId + " mMaxScreenId: " + mMaxScreenId);
1146        }
1147    }
1148
1149    static class SqlArguments {
1150        public final String table;
1151        public final String where;
1152        public final String[] args;
1153
1154        SqlArguments(Uri url, String where, String[] args) {
1155            if (url.getPathSegments().size() == 1) {
1156                this.table = url.getPathSegments().get(0);
1157                this.where = where;
1158                this.args = args;
1159            } else if (url.getPathSegments().size() != 2) {
1160                throw new IllegalArgumentException("Invalid URI: " + url);
1161            } else if (!TextUtils.isEmpty(where)) {
1162                throw new UnsupportedOperationException("WHERE clause not supported: " + url);
1163            } else {
1164                this.table = url.getPathSegments().get(0);
1165                this.where = "_id=" + ContentUris.parseId(url);
1166                this.args = null;
1167            }
1168        }
1169
1170        SqlArguments(Uri url) {
1171            if (url.getPathSegments().size() == 1) {
1172                table = url.getPathSegments().get(0);
1173                where = null;
1174                args = null;
1175            } else {
1176                throw new IllegalArgumentException("Invalid URI: " + url);
1177            }
1178        }
1179    }
1180}
1181