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