LauncherProvider.java revision 33d443897658e6ad8b76bd2e58e3598161fd3ead
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.app.SearchManager;
20import android.appwidget.AppWidgetHost;
21import android.appwidget.AppWidgetManager;
22import android.appwidget.AppWidgetProviderInfo;
23import android.content.ComponentName;
24import android.content.ContentProvider;
25import android.content.ContentProviderOperation;
26import android.content.ContentProviderResult;
27import android.content.ContentResolver;
28import android.content.ContentUris;
29import android.content.ContentValues;
30import android.content.Context;
31import android.content.Intent;
32import android.content.OperationApplicationException;
33import android.content.SharedPreferences;
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.graphics.Bitmap;
42import android.graphics.BitmapFactory;
43import android.net.Uri;
44import android.provider.Settings;
45import android.text.TextUtils;
46import android.util.Log;
47import android.util.SparseArray;
48
49import com.android.launcher3.AutoInstallsLayout.LayoutParserCallback;
50import com.android.launcher3.LauncherSettings.Favorites;
51import com.android.launcher3.compat.UserHandleCompat;
52import com.android.launcher3.compat.UserManagerCompat;
53import com.android.launcher3.config.ProviderConfig;
54
55import java.io.File;
56import java.net.URISyntaxException;
57import java.util.ArrayList;
58import java.util.Collections;
59import java.util.HashSet;
60import java.util.List;
61
62public class LauncherProvider extends ContentProvider {
63    private static final String TAG = "Launcher.LauncherProvider";
64    private static final boolean LOGD = false;
65
66    private static final int DATABASE_VERSION = 20;
67
68    static final String OLD_AUTHORITY = "com.android.launcher2.settings";
69    static final String AUTHORITY = ProviderConfig.AUTHORITY;
70
71    // Should we attempt to load anything from the com.android.launcher2 provider?
72    static final boolean IMPORT_LAUNCHER2_DATABASE = false;
73
74    static final String TABLE_FAVORITES = "favorites";
75    static final String TABLE_WORKSPACE_SCREENS = "workspaceScreens";
76    static final String PARAMETER_NOTIFY = "notify";
77    static final String UPGRADED_FROM_OLD_DATABASE =
78            "UPGRADED_FROM_OLD_DATABASE";
79    static final String EMPTY_DATABASE_CREATED =
80            "EMPTY_DATABASE_CREATED";
81
82    private static final String URI_PARAM_IS_EXTERNAL_ADD = "isExternalAdd";
83
84    private LauncherProviderChangeListener mListener;
85
86    /**
87     * {@link Uri} triggered at any registered {@link android.database.ContentObserver} when
88     * {@link AppWidgetHost#deleteHost()} is called during database creation.
89     * Use this to recall {@link AppWidgetHost#startListening()} if needed.
90     */
91    static final Uri CONTENT_APPWIDGET_RESET_URI =
92            Uri.parse("content://" + AUTHORITY + "/appWidgetReset");
93
94    private DatabaseHelper mOpenHelper;
95    private static boolean sJustLoadedFromOldDb;
96
97    @Override
98    public boolean onCreate() {
99        final Context context = getContext();
100        mOpenHelper = new DatabaseHelper(context);
101        LauncherAppState.setLauncherProvider(this);
102        return true;
103    }
104
105    public boolean wasNewDbCreated() {
106        return mOpenHelper.wasNewDbCreated();
107    }
108
109    public void setLauncherProviderChangeListener(LauncherProviderChangeListener listener) {
110        mListener = listener;
111    }
112
113    @Override
114    public String getType(Uri uri) {
115        SqlArguments args = new SqlArguments(uri, null, null);
116        if (TextUtils.isEmpty(args.where)) {
117            return "vnd.android.cursor.dir/" + args.table;
118        } else {
119            return "vnd.android.cursor.item/" + args.table;
120        }
121    }
122
123    @Override
124    public Cursor query(Uri uri, String[] projection, String selection,
125            String[] selectionArgs, String sortOrder) {
126
127        SqlArguments args = new SqlArguments(uri, selection, selectionArgs);
128        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
129        qb.setTables(args.table);
130
131        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
132        Cursor result = qb.query(db, projection, args.where, args.args, null, null, sortOrder);
133        result.setNotificationUri(getContext().getContentResolver(), uri);
134
135        return result;
136    }
137
138    private static long dbInsertAndCheck(DatabaseHelper helper,
139            SQLiteDatabase db, String table, String nullColumnHack, ContentValues values) {
140        if (values == null) {
141            throw new RuntimeException("Error: attempting to insert null values");
142        }
143        if (!values.containsKey(LauncherSettings.ChangeLogColumns._ID)) {
144            throw new RuntimeException("Error: attempting to add item without specifying an id");
145        }
146        helper.checkId(table, values);
147        return db.insert(table, nullColumnHack, values);
148    }
149
150    @Override
151    public Uri insert(Uri uri, ContentValues initialValues) {
152        SqlArguments args = new SqlArguments(uri);
153
154        // In very limited cases, we support system|signature permission apps to add to the db
155        String externalAdd = uri.getQueryParameter(URI_PARAM_IS_EXTERNAL_ADD);
156        if (externalAdd != null && "true".equals(externalAdd)) {
157            if (!mOpenHelper.initializeExternalAdd(initialValues)) {
158                return null;
159            }
160        }
161
162        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
163        addModifiedTime(initialValues);
164        final long rowId = dbInsertAndCheck(mOpenHelper, db, args.table, null, initialValues);
165        if (rowId <= 0) return null;
166
167        uri = ContentUris.withAppendedId(uri, rowId);
168        sendNotify(uri);
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        sendNotify(uri);
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) sendNotify(uri);
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) sendNotify(uri);
230
231        return count;
232    }
233
234    private void sendNotify(Uri uri) {
235        String notify = uri.getQueryParameter(PARAMETER_NOTIFY);
236        if (notify == null || "true".equals(notify)) {
237            getContext().getContentResolver().notifyChange(uri, null);
238        }
239
240        // always notify the backup agent
241        LauncherBackupAgentHelper.dataChanged(getContext());
242        if (mListener != null) {
243            mListener.onLauncherProviderChange();
244        }
245    }
246
247    private void addModifiedTime(ContentValues values) {
248        values.put(LauncherSettings.ChangeLogColumns.MODIFIED, System.currentTimeMillis());
249    }
250
251    public long generateNewItemId() {
252        return mOpenHelper.generateNewItemId();
253    }
254
255    public void updateMaxItemId(long id) {
256        mOpenHelper.updateMaxItemId(id);
257    }
258
259    public long generateNewScreenId() {
260        return mOpenHelper.generateNewScreenId();
261    }
262
263    // This is only required one time while loading the workspace during the
264    // upgrade path, and should never be called from anywhere else.
265    public void updateMaxScreenId(long maxScreenId) {
266        mOpenHelper.updateMaxScreenId(maxScreenId);
267    }
268
269    /**
270     * @param Should we load the old db for upgrade? first run only.
271     */
272    synchronized public boolean justLoadedOldDb() {
273        String spKey = LauncherAppState.getSharedPreferencesKey();
274        SharedPreferences sp = getContext().getSharedPreferences(spKey, Context.MODE_PRIVATE);
275
276        boolean loadedOldDb = false || sJustLoadedFromOldDb;
277
278        sJustLoadedFromOldDb = false;
279        if (sp.getBoolean(UPGRADED_FROM_OLD_DATABASE, false)) {
280
281            SharedPreferences.Editor editor = sp.edit();
282            editor.remove(UPGRADED_FROM_OLD_DATABASE);
283            editor.commit();
284            loadedOldDb = true;
285        }
286        return loadedOldDb;
287    }
288
289    /**
290     * Clears all the data for a fresh start.
291     */
292    synchronized public void createEmptyDB() {
293        mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
294    }
295
296    public void clearFlagEmptyDbCreated() {
297        String spKey = LauncherAppState.getSharedPreferencesKey();
298        getContext().getSharedPreferences(spKey, Context.MODE_PRIVATE)
299            .edit()
300            .remove(EMPTY_DATABASE_CREATED)
301            .commit();
302    }
303
304    /**
305     * Loads the default workspace based on the following priority scheme:
306     *   1) From a package provided by play store
307     *   2) From a partner configuration APK, already in the system image
308     *   3) The default configuration for the particular device
309     */
310    synchronized public void loadDefaultFavoritesIfNecessary() {
311        String spKey = LauncherAppState.getSharedPreferencesKey();
312        SharedPreferences sp = getContext().getSharedPreferences(spKey, Context.MODE_PRIVATE);
313
314        if (sp.getBoolean(EMPTY_DATABASE_CREATED, false)) {
315            Log.d(TAG, "loading default workspace");
316
317            AutoInstallsLayout loader = AutoInstallsLayout.get(getContext(),
318                    mOpenHelper.mAppWidgetHost, mOpenHelper);
319
320            if (loader == null) {
321                final Partner partner = Partner.get(getContext().getPackageManager());
322                if (partner != null && partner.hasDefaultLayout()) {
323                    final Resources partnerRes = partner.getResources();
324                    int workspaceResId = partnerRes.getIdentifier(Partner.RES_DEFAULT_LAYOUT,
325                            "xml", partner.getPackageName());
326                    if (workspaceResId != 0) {
327                        loader = new DefaultLayoutParser(getContext(), mOpenHelper.mAppWidgetHost,
328                                mOpenHelper, partnerRes, workspaceResId);
329                    }
330                }
331            }
332
333            final boolean usingExternallyProvidedLayout = loader != null;
334            if (loader == null) {
335                loader = getDefaultLayoutParser();
336            }
337            // Populate favorites table with initial favorites
338            if ((mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), loader) <= 0)
339                    && usingExternallyProvidedLayout) {
340                // Unable to load external layout. Cleanup and load the internal layout.
341                createEmptyDB();
342                mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(),
343                        getDefaultLayoutParser());
344            }
345            clearFlagEmptyDbCreated();
346        }
347    }
348
349    private DefaultLayoutParser getDefaultLayoutParser() {
350        int defaultLayout = LauncherAppState.getInstance()
351                .getDynamicGrid().getDeviceProfile().defaultLayoutId;
352        return new DefaultLayoutParser(getContext(), mOpenHelper.mAppWidgetHost,
353                mOpenHelper, getContext().getResources(), defaultLayout);
354    }
355
356    public void migrateLauncher2Shortcuts() {
357        mOpenHelper.migrateLauncher2Shortcuts(mOpenHelper.getWritableDatabase(),
358                Uri.parse(getContext().getString(R.string.old_launcher_provider_uri)));
359    }
360
361    private static interface ContentValuesCallback {
362        public void onRow(ContentValues values);
363    }
364
365    private static boolean shouldImportLauncher2Database(Context context) {
366        boolean isTablet = context.getResources().getBoolean(R.bool.is_tablet);
367
368        // We don't import the old databse for tablets, as the grid size has changed.
369        return !isTablet && IMPORT_LAUNCHER2_DATABASE;
370    }
371
372    public void deleteDatabase() {
373        // Are you sure? (y/n)
374        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
375        final File dbFile = new File(db.getPath());
376        mOpenHelper.close();
377        if (dbFile.exists()) {
378            SQLiteDatabase.deleteDatabase(dbFile);
379        }
380        mOpenHelper = new DatabaseHelper(getContext());
381    }
382
383    private static class DatabaseHelper extends SQLiteOpenHelper implements LayoutParserCallback {
384        private final Context mContext;
385        private final AppWidgetHost mAppWidgetHost;
386        private long mMaxItemId = -1;
387        private long mMaxScreenId = -1;
388
389        private boolean mNewDbCreated = false;
390
391        DatabaseHelper(Context context) {
392            super(context, LauncherFiles.LAUNCHER_DB, null, DATABASE_VERSION);
393            mContext = context;
394            mAppWidgetHost = new AppWidgetHost(context, Launcher.APPWIDGET_HOST_ID);
395
396            // In the case where neither onCreate nor onUpgrade gets called, we read the maxId from
397            // the DB here
398            if (mMaxItemId == -1) {
399                mMaxItemId = initializeMaxItemId(getWritableDatabase());
400            }
401            if (mMaxScreenId == -1) {
402                mMaxScreenId = initializeMaxScreenId(getWritableDatabase());
403            }
404        }
405
406        public boolean wasNewDbCreated() {
407            return mNewDbCreated;
408        }
409
410        /**
411         * Send notification that we've deleted the {@link AppWidgetHost},
412         * probably as part of the initial database creation. The receiver may
413         * want to re-call {@link AppWidgetHost#startListening()} to ensure
414         * callbacks are correctly set.
415         */
416        private void sendAppWidgetResetNotify() {
417            final ContentResolver resolver = mContext.getContentResolver();
418            resolver.notifyChange(CONTENT_APPWIDGET_RESET_URI, null);
419        }
420
421        @Override
422        public void onCreate(SQLiteDatabase db) {
423            if (LOGD) Log.d(TAG, "creating new launcher database");
424
425            mMaxItemId = 1;
426            mMaxScreenId = 0;
427            mNewDbCreated = true;
428
429            UserManagerCompat userManager = UserManagerCompat.getInstance(mContext);
430            long userSerialNumber = userManager.getSerialNumberForUser(
431                    UserHandleCompat.myUserHandle());
432
433            db.execSQL("CREATE TABLE favorites (" +
434                    "_id INTEGER PRIMARY KEY," +
435                    "title TEXT," +
436                    "intent TEXT," +
437                    "container INTEGER," +
438                    "screen INTEGER," +
439                    "cellX INTEGER," +
440                    "cellY INTEGER," +
441                    "spanX INTEGER," +
442                    "spanY INTEGER," +
443                    "itemType INTEGER," +
444                    "appWidgetId INTEGER NOT NULL DEFAULT -1," +
445                    "isShortcut INTEGER," +
446                    "iconType INTEGER," +
447                    "iconPackage TEXT," +
448                    "iconResource TEXT," +
449                    "icon BLOB," +
450                    "uri TEXT," +
451                    "displayMode INTEGER," +
452                    "appWidgetProvider TEXT," +
453                    "modified INTEGER NOT NULL DEFAULT 0," +
454                    "restored INTEGER NOT NULL DEFAULT 0," +
455                    "profileId INTEGER DEFAULT " + userSerialNumber +
456                    ");");
457            addWorkspacesTable(db);
458
459            // Database was just created, so wipe any previous widgets
460            if (mAppWidgetHost != null) {
461                mAppWidgetHost.deleteHost();
462                sendAppWidgetResetNotify();
463            }
464
465            if (shouldImportLauncher2Database(mContext)) {
466                // Try converting the old database
467                ContentValuesCallback permuteScreensCb = new ContentValuesCallback() {
468                    public void onRow(ContentValues values) {
469                        int container = values.getAsInteger(LauncherSettings.Favorites.CONTAINER);
470                        if (container == Favorites.CONTAINER_DESKTOP) {
471                            int screen = values.getAsInteger(LauncherSettings.Favorites.SCREEN);
472                            screen = (int) upgradeLauncherDb_permuteScreens(screen);
473                            values.put(LauncherSettings.Favorites.SCREEN, screen);
474                        }
475                    }
476                };
477                Uri uri = Uri.parse("content://" + Settings.AUTHORITY +
478                        "/old_favorites?notify=true");
479                if (!convertDatabase(db, uri, permuteScreensCb, true)) {
480                    // Try and upgrade from the Launcher2 db
481                    uri = Uri.parse(mContext.getString(R.string.old_launcher_provider_uri));
482                    if (!convertDatabase(db, uri, permuteScreensCb, false)) {
483                        // If we fail, then set a flag to load the default workspace
484                        setFlagEmptyDbCreated();
485                        return;
486                    }
487                }
488                // Right now, in non-default workspace cases, we want to run the final
489                // upgrade code (ie. to fix workspace screen indices -> ids, etc.), so
490                // set that flag too.
491                setFlagJustLoadedOldDb();
492            } else {
493                // Fresh and clean launcher DB.
494                mMaxItemId = initializeMaxItemId(db);
495                setFlagEmptyDbCreated();
496            }
497        }
498
499        private void addWorkspacesTable(SQLiteDatabase db) {
500            db.execSQL("CREATE TABLE " + TABLE_WORKSPACE_SCREENS + " (" +
501                    LauncherSettings.WorkspaceScreens._ID + " INTEGER," +
502                    LauncherSettings.WorkspaceScreens.SCREEN_RANK + " INTEGER," +
503                    LauncherSettings.ChangeLogColumns.MODIFIED + " INTEGER NOT NULL DEFAULT 0" +
504                    ");");
505        }
506
507        private void removeOrphanedItems(SQLiteDatabase db) {
508            // Delete items directly on the workspace who's screen id doesn't exist
509            //  "DELETE FROM favorites WHERE screen NOT IN (SELECT _id FROM workspaceScreens)
510            //   AND container = -100"
511            String removeOrphanedDesktopItems = "DELETE FROM " + TABLE_FAVORITES +
512                    " WHERE " +
513                    LauncherSettings.Favorites.SCREEN + " NOT IN (SELECT " +
514                    LauncherSettings.WorkspaceScreens._ID + " FROM " + TABLE_WORKSPACE_SCREENS + ")" +
515                    " AND " +
516                    LauncherSettings.Favorites.CONTAINER + " = " +
517                    LauncherSettings.Favorites.CONTAINER_DESKTOP;
518            db.execSQL(removeOrphanedDesktopItems);
519
520            // Delete items contained in folders which no longer exist (after above statement)
521            //  "DELETE FROM favorites  WHERE container <> -100 AND container <> -101 AND container
522            //   NOT IN (SELECT _id FROM favorites WHERE itemType = 2)"
523            String removeOrphanedFolderItems = "DELETE FROM " + TABLE_FAVORITES +
524                    " WHERE " +
525                    LauncherSettings.Favorites.CONTAINER + " <> " +
526                    LauncherSettings.Favorites.CONTAINER_DESKTOP +
527                    " AND "
528                    + LauncherSettings.Favorites.CONTAINER + " <> " +
529                    LauncherSettings.Favorites.CONTAINER_HOTSEAT +
530                    " AND "
531                    + LauncherSettings.Favorites.CONTAINER + " NOT IN (SELECT " +
532                    LauncherSettings.Favorites._ID + " FROM " + TABLE_FAVORITES +
533                    " WHERE " + LauncherSettings.Favorites.ITEM_TYPE + " = " +
534                    LauncherSettings.Favorites.ITEM_TYPE_FOLDER + ")";
535            db.execSQL(removeOrphanedFolderItems);
536        }
537
538        private void setFlagJustLoadedOldDb() {
539            String spKey = LauncherAppState.getSharedPreferencesKey();
540            SharedPreferences sp = mContext.getSharedPreferences(spKey, Context.MODE_PRIVATE);
541            SharedPreferences.Editor editor = sp.edit();
542            editor.putBoolean(UPGRADED_FROM_OLD_DATABASE, true);
543            editor.putBoolean(EMPTY_DATABASE_CREATED, false);
544            editor.commit();
545        }
546
547        private void setFlagEmptyDbCreated() {
548            String spKey = LauncherAppState.getSharedPreferencesKey();
549            SharedPreferences sp = mContext.getSharedPreferences(spKey, Context.MODE_PRIVATE);
550            SharedPreferences.Editor editor = sp.edit();
551            editor.putBoolean(EMPTY_DATABASE_CREATED, true);
552            editor.putBoolean(UPGRADED_FROM_OLD_DATABASE, false);
553            editor.commit();
554        }
555
556        // We rearrange the screens from the old launcher
557        // 12345 -> 34512
558        private long upgradeLauncherDb_permuteScreens(long screen) {
559            if (screen >= 2) {
560                return screen - 2;
561            } else {
562                return screen + 3;
563            }
564        }
565
566        private boolean convertDatabase(SQLiteDatabase db, Uri uri,
567                                        ContentValuesCallback cb, boolean deleteRows) {
568            if (LOGD) Log.d(TAG, "converting database from an older format, but not onUpgrade");
569            boolean converted = false;
570
571            final ContentResolver resolver = mContext.getContentResolver();
572            Cursor cursor = null;
573
574            try {
575                cursor = resolver.query(uri, null, null, null, null);
576            } catch (Exception e) {
577                // Ignore
578            }
579
580            // We already have a favorites database in the old provider
581            if (cursor != null) {
582                try {
583                     if (cursor.getCount() > 0) {
584                        converted = copyFromCursor(db, cursor, cb) > 0;
585                        if (converted && deleteRows) {
586                            resolver.delete(uri, null, null);
587                        }
588                    }
589                } finally {
590                    cursor.close();
591                }
592            }
593
594            if (converted) {
595                // Convert widgets from this import into widgets
596                if (LOGD) Log.d(TAG, "converted and now triggering widget upgrade");
597                convertWidgets(db);
598
599                // Update max item id
600                mMaxItemId = initializeMaxItemId(db);
601                if (LOGD) Log.d(TAG, "mMaxItemId: " + mMaxItemId);
602            }
603
604            return converted;
605        }
606
607        private int copyFromCursor(SQLiteDatabase db, Cursor c, ContentValuesCallback cb) {
608            final int idIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID);
609            final int intentIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.INTENT);
610            final int titleIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.TITLE);
611            final int iconTypeIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_TYPE);
612            final int iconIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON);
613            final int iconPackageIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_PACKAGE);
614            final int iconResourceIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_RESOURCE);
615            final int containerIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CONTAINER);
616            final int itemTypeIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE);
617            final int screenIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SCREEN);
618            final int cellXIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLX);
619            final int cellYIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLY);
620            final int uriIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.URI);
621            final int displayModeIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.DISPLAY_MODE);
622
623            ContentValues[] rows = new ContentValues[c.getCount()];
624            int i = 0;
625            while (c.moveToNext()) {
626                ContentValues values = new ContentValues(c.getColumnCount());
627                values.put(LauncherSettings.Favorites._ID, c.getLong(idIndex));
628                values.put(LauncherSettings.Favorites.INTENT, c.getString(intentIndex));
629                values.put(LauncherSettings.Favorites.TITLE, c.getString(titleIndex));
630                values.put(LauncherSettings.Favorites.ICON_TYPE, c.getInt(iconTypeIndex));
631                values.put(LauncherSettings.Favorites.ICON, c.getBlob(iconIndex));
632                values.put(LauncherSettings.Favorites.ICON_PACKAGE, c.getString(iconPackageIndex));
633                values.put(LauncherSettings.Favorites.ICON_RESOURCE, c.getString(iconResourceIndex));
634                values.put(LauncherSettings.Favorites.CONTAINER, c.getInt(containerIndex));
635                values.put(LauncherSettings.Favorites.ITEM_TYPE, c.getInt(itemTypeIndex));
636                values.put(LauncherSettings.Favorites.APPWIDGET_ID, -1);
637                values.put(LauncherSettings.Favorites.SCREEN, c.getInt(screenIndex));
638                values.put(LauncherSettings.Favorites.CELLX, c.getInt(cellXIndex));
639                values.put(LauncherSettings.Favorites.CELLY, c.getInt(cellYIndex));
640                values.put(LauncherSettings.Favorites.URI, c.getString(uriIndex));
641                values.put(LauncherSettings.Favorites.DISPLAY_MODE, c.getInt(displayModeIndex));
642                if (cb != null) {
643                    cb.onRow(values);
644                }
645                rows[i++] = values;
646            }
647
648            int total = 0;
649            if (i > 0) {
650                db.beginTransaction();
651                try {
652                    int numValues = rows.length;
653                    for (i = 0; i < numValues; i++) {
654                        if (dbInsertAndCheck(this, db, TABLE_FAVORITES, null, rows[i]) < 0) {
655                            return 0;
656                        } else {
657                            total++;
658                        }
659                    }
660                    db.setTransactionSuccessful();
661                } finally {
662                    db.endTransaction();
663                }
664            }
665
666            return total;
667        }
668
669        @Override
670        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
671            if (LOGD) Log.d(TAG, "onUpgrade triggered: " + oldVersion);
672
673            int version = oldVersion;
674            if (version < 3) {
675                // upgrade 1,2 -> 3 added appWidgetId column
676                db.beginTransaction();
677                try {
678                    // Insert new column for holding appWidgetIds
679                    db.execSQL("ALTER TABLE favorites " +
680                        "ADD COLUMN appWidgetId INTEGER NOT NULL DEFAULT -1;");
681                    db.setTransactionSuccessful();
682                    version = 3;
683                } catch (SQLException ex) {
684                    // Old version remains, which means we wipe old data
685                    Log.e(TAG, ex.getMessage(), ex);
686                } finally {
687                    db.endTransaction();
688                }
689
690                // Convert existing widgets only if table upgrade was successful
691                if (version == 3) {
692                    convertWidgets(db);
693                }
694            }
695
696            if (version < 4) {
697                version = 4;
698            }
699
700            // Where's version 5?
701            // - Donut and sholes on 2.0 shipped with version 4 of launcher1.
702            // - Passion shipped on 2.1 with version 6 of launcher3
703            // - Sholes shipped on 2.1r1 (aka Mr. 3) with version 5 of launcher 1
704            //   but version 5 on there was the updateContactsShortcuts change
705            //   which was version 6 in launcher 2 (first shipped on passion 2.1r1).
706            // The updateContactsShortcuts change is idempotent, so running it twice
707            // is okay so we'll do that when upgrading the devices that shipped with it.
708            if (version < 6) {
709                // We went from 3 to 5 screens. Move everything 1 to the right
710                db.beginTransaction();
711                try {
712                    db.execSQL("UPDATE favorites SET screen=(screen + 1);");
713                    db.setTransactionSuccessful();
714                } catch (SQLException ex) {
715                    // Old version remains, which means we wipe old data
716                    Log.e(TAG, ex.getMessage(), ex);
717                } finally {
718                    db.endTransaction();
719                }
720
721               // We added the fast track.
722                if (updateContactsShortcuts(db)) {
723                    version = 6;
724                }
725            }
726
727            if (version < 7) {
728                // Version 7 gets rid of the special search widget.
729                convertWidgets(db);
730                version = 7;
731            }
732
733            if (version < 8) {
734                // Version 8 (froyo) has the icons all normalized.  This should
735                // already be the case in practice, but we now rely on it and don't
736                // resample the images each time.
737                normalizeIcons(db);
738                version = 8;
739            }
740
741            if (version < 9) {
742                // The max id is not yet set at this point (onUpgrade is triggered in the ctor
743                // before it gets a change to get set, so we need to read it here when we use it)
744                if (mMaxItemId == -1) {
745                    mMaxItemId = initializeMaxItemId(db);
746                }
747
748                // Add default hotseat icons
749                loadFavorites(db, new DefaultLayoutParser(mContext, mAppWidgetHost, this,
750                        mContext.getResources(), R.xml.update_workspace));
751                version = 9;
752            }
753
754            // We bumped the version three time during JB, once to update the launch flags, once to
755            // update the override for the default launch animation and once to set the mimetype
756            // to improve startup performance
757            if (version < 12) {
758                // Contact shortcuts need a different set of flags to be launched now
759                // The updateContactsShortcuts change is idempotent, so we can keep using it like
760                // back in the Donut days
761                updateContactsShortcuts(db);
762                version = 12;
763            }
764
765            if (version < 13) {
766                // With the new shrink-wrapped and re-orderable workspaces, it makes sense
767                // to persist workspace screens and their relative order.
768                mMaxScreenId = 0;
769
770                // This will never happen in the wild, but when we switch to using workspace
771                // screen ids, redo the import from old launcher.
772                sJustLoadedFromOldDb = true;
773
774                addWorkspacesTable(db);
775                version = 13;
776            }
777
778            if (version < 14) {
779                db.beginTransaction();
780                try {
781                    // Insert new column for holding widget provider name
782                    db.execSQL("ALTER TABLE favorites " +
783                            "ADD COLUMN appWidgetProvider TEXT;");
784                    db.setTransactionSuccessful();
785                    version = 14;
786                } catch (SQLException ex) {
787                    // Old version remains, which means we wipe old data
788                    Log.e(TAG, ex.getMessage(), ex);
789                } finally {
790                    db.endTransaction();
791                }
792            }
793
794            if (version < 15) {
795                db.beginTransaction();
796                try {
797                    // Insert new column for holding update timestamp
798                    db.execSQL("ALTER TABLE favorites " +
799                            "ADD COLUMN modified INTEGER NOT NULL DEFAULT 0;");
800                    db.execSQL("ALTER TABLE workspaceScreens " +
801                            "ADD COLUMN modified INTEGER NOT NULL DEFAULT 0;");
802                    db.setTransactionSuccessful();
803                    version = 15;
804                } catch (SQLException ex) {
805                    // Old version remains, which means we wipe old data
806                    Log.e(TAG, ex.getMessage(), ex);
807                } finally {
808                    db.endTransaction();
809                }
810            }
811
812
813            if (version < 16) {
814                db.beginTransaction();
815                try {
816                    // Insert new column for holding restore status
817                    db.execSQL("ALTER TABLE favorites " +
818                            "ADD COLUMN restored INTEGER NOT NULL DEFAULT 0;");
819                    db.setTransactionSuccessful();
820                    version = 16;
821                } catch (SQLException ex) {
822                    // Old version remains, which means we wipe old data
823                    Log.e(TAG, ex.getMessage(), ex);
824                } finally {
825                    db.endTransaction();
826                }
827            }
828
829            if (version < 17) {
830                // We use the db version upgrade here to identify users who may not have seen
831                // clings yet (because they weren't available), but for whom the clings are now
832                // available (tablet users). Because one of the possible cling flows (migration)
833                // is very destructive (wipes out workspaces), we want to prevent this from showing
834                // until clear data. We do so by marking that the clings have been shown.
835                LauncherClings.synchonouslyMarkFirstRunClingDismissed(mContext);
836                version = 17;
837            }
838
839            if (version < 18) {
840                // No-op
841                version = 18;
842            }
843
844            if (version < 19) {
845                // Due to a data loss bug, some users may have items associated with screen ids
846                // which no longer exist. Since this can cause other problems, and since the user
847                // will never see these items anyway, we use database upgrade as an opportunity to
848                // clean things up.
849                removeOrphanedItems(db);
850                version = 19;
851            }
852
853            if (version < 20) {
854                // Add userId column
855                if (addProfileColumn(db)) {
856                    version = 20;
857                }
858                // else old version remains, which means we wipe old data
859            }
860
861            if (version != DATABASE_VERSION) {
862                Log.w(TAG, "Destroying all old data.");
863                db.execSQL("DROP TABLE IF EXISTS " + TABLE_FAVORITES);
864                db.execSQL("DROP TABLE IF EXISTS " + TABLE_WORKSPACE_SCREENS);
865
866                onCreate(db);
867            }
868        }
869
870        @Override
871        public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
872            // This shouldn't happen -- throw our hands up in the air and start over.
873            Log.w(TAG, "Database version downgrade from: " + oldVersion + " to " + newVersion +
874                    ". Wiping databse.");
875            createEmptyDB(db);
876        }
877
878
879        /**
880         * Clears all the data for a fresh start.
881         */
882        public void createEmptyDB(SQLiteDatabase db) {
883            db.execSQL("DROP TABLE IF EXISTS " + TABLE_FAVORITES);
884            db.execSQL("DROP TABLE IF EXISTS " + TABLE_WORKSPACE_SCREENS);
885            onCreate(db);
886        }
887
888        private boolean addProfileColumn(SQLiteDatabase db) {
889            db.beginTransaction();
890            try {
891                UserManagerCompat userManager = UserManagerCompat.getInstance(mContext);
892                // Default to the serial number of this user, for older
893                // shortcuts.
894                long userSerialNumber = userManager.getSerialNumberForUser(
895                        UserHandleCompat.myUserHandle());
896                // Insert new column for holding user serial number
897                db.execSQL("ALTER TABLE favorites " +
898                        "ADD COLUMN profileId INTEGER DEFAULT "
899                                        + userSerialNumber + ";");
900                db.setTransactionSuccessful();
901            } catch (SQLException ex) {
902                // Old version remains, which means we wipe old data
903                Log.e(TAG, ex.getMessage(), ex);
904                return false;
905            } finally {
906                db.endTransaction();
907            }
908            return true;
909        }
910
911        private boolean updateContactsShortcuts(SQLiteDatabase db) {
912            final String selectWhere = buildOrWhereString(Favorites.ITEM_TYPE,
913                    new int[] { Favorites.ITEM_TYPE_SHORTCUT });
914
915            Cursor c = null;
916            final String actionQuickContact = "com.android.contacts.action.QUICK_CONTACT";
917            db.beginTransaction();
918            try {
919                // Select and iterate through each matching widget
920                c = db.query(TABLE_FAVORITES,
921                        new String[] { Favorites._ID, Favorites.INTENT },
922                        selectWhere, null, null, null, null);
923                if (c == null) return false;
924
925                if (LOGD) Log.d(TAG, "found upgrade cursor count=" + c.getCount());
926
927                final int idIndex = c.getColumnIndex(Favorites._ID);
928                final int intentIndex = c.getColumnIndex(Favorites.INTENT);
929
930                while (c.moveToNext()) {
931                    long favoriteId = c.getLong(idIndex);
932                    final String intentUri = c.getString(intentIndex);
933                    if (intentUri != null) {
934                        try {
935                            final Intent intent = Intent.parseUri(intentUri, 0);
936                            android.util.Log.d("Home", intent.toString());
937                            final Uri uri = intent.getData();
938                            if (uri != null) {
939                                final String data = uri.toString();
940                                if ((Intent.ACTION_VIEW.equals(intent.getAction()) ||
941                                        actionQuickContact.equals(intent.getAction())) &&
942                                        (data.startsWith("content://contacts/people/") ||
943                                        data.startsWith("content://com.android.contacts/" +
944                                                "contacts/lookup/"))) {
945
946                                    final Intent newIntent = new Intent(actionQuickContact);
947                                    // When starting from the launcher, start in a new, cleared task
948                                    // CLEAR_WHEN_TASK_RESET cannot reset the root of a task, so we
949                                    // clear the whole thing preemptively here since
950                                    // QuickContactActivity will finish itself when launching other
951                                    // detail activities.
952                                    newIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
953                                            Intent.FLAG_ACTIVITY_CLEAR_TASK);
954                                    newIntent.putExtra(
955                                            Launcher.INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION, true);
956                                    newIntent.setData(uri);
957                                    // Determine the type and also put that in the shortcut
958                                    // (that can speed up launch a bit)
959                                    newIntent.setDataAndType(uri, newIntent.resolveType(mContext));
960
961                                    final ContentValues values = new ContentValues();
962                                    values.put(LauncherSettings.Favorites.INTENT,
963                                            newIntent.toUri(0));
964
965                                    String updateWhere = Favorites._ID + "=" + favoriteId;
966                                    db.update(TABLE_FAVORITES, values, updateWhere, null);
967                                }
968                            }
969                        } catch (RuntimeException ex) {
970                            Log.e(TAG, "Problem upgrading shortcut", ex);
971                        } catch (URISyntaxException e) {
972                            Log.e(TAG, "Problem upgrading shortcut", e);
973                        }
974                    }
975                }
976
977                db.setTransactionSuccessful();
978            } catch (SQLException ex) {
979                Log.w(TAG, "Problem while upgrading contacts", ex);
980                return false;
981            } finally {
982                db.endTransaction();
983                if (c != null) {
984                    c.close();
985                }
986            }
987
988            return true;
989        }
990
991        private void normalizeIcons(SQLiteDatabase db) {
992            Log.d(TAG, "normalizing icons");
993
994            db.beginTransaction();
995            Cursor c = null;
996            SQLiteStatement update = null;
997            try {
998                boolean logged = false;
999                update = db.compileStatement("UPDATE favorites "
1000                        + "SET icon=? WHERE _id=?");
1001
1002                c = db.rawQuery("SELECT _id, icon FROM favorites WHERE iconType=" +
1003                        Favorites.ICON_TYPE_BITMAP, null);
1004
1005                final int idIndex = c.getColumnIndexOrThrow(Favorites._ID);
1006                final int iconIndex = c.getColumnIndexOrThrow(Favorites.ICON);
1007
1008                while (c.moveToNext()) {
1009                    long id = c.getLong(idIndex);
1010                    byte[] data = c.getBlob(iconIndex);
1011                    try {
1012                        Bitmap bitmap = Utilities.createIconBitmap(
1013                                BitmapFactory.decodeByteArray(data, 0, data.length),
1014                                mContext);
1015                        if (bitmap != null) {
1016                            update.bindLong(1, id);
1017                            data = ItemInfo.flattenBitmap(bitmap);
1018                            if (data != null) {
1019                                update.bindBlob(2, data);
1020                                update.execute();
1021                            }
1022                            bitmap.recycle();
1023                        }
1024                    } catch (Exception e) {
1025                        if (!logged) {
1026                            Log.e(TAG, "Failed normalizing icon " + id, e);
1027                        } else {
1028                            Log.e(TAG, "Also failed normalizing icon " + id);
1029                        }
1030                        logged = true;
1031                    }
1032                }
1033                db.setTransactionSuccessful();
1034            } catch (SQLException ex) {
1035                Log.w(TAG, "Problem while allocating appWidgetIds for existing widgets", ex);
1036            } finally {
1037                db.endTransaction();
1038                if (update != null) {
1039                    update.close();
1040                }
1041                if (c != null) {
1042                    c.close();
1043                }
1044            }
1045        }
1046
1047        // Generates a new ID to use for an object in your database. This method should be only
1048        // called from the main UI thread. As an exception, we do call it when we call the
1049        // constructor from the worker thread; however, this doesn't extend until after the
1050        // constructor is called, and we only pass a reference to LauncherProvider to LauncherApp
1051        // after that point
1052        @Override
1053        public long generateNewItemId() {
1054            if (mMaxItemId < 0) {
1055                throw new RuntimeException("Error: max item id was not initialized");
1056            }
1057            mMaxItemId += 1;
1058            return mMaxItemId;
1059        }
1060
1061        @Override
1062        public long insertAndCheck(SQLiteDatabase db, ContentValues values) {
1063            return dbInsertAndCheck(this, db, TABLE_FAVORITES, null, values);
1064        }
1065
1066        public void updateMaxItemId(long id) {
1067            mMaxItemId = id + 1;
1068        }
1069
1070        public void checkId(String table, ContentValues values) {
1071            long id = values.getAsLong(LauncherSettings.BaseLauncherColumns._ID);
1072            if (table == LauncherProvider.TABLE_WORKSPACE_SCREENS) {
1073                mMaxScreenId = Math.max(id, mMaxScreenId);
1074            }  else {
1075                mMaxItemId = Math.max(id, mMaxItemId);
1076            }
1077        }
1078
1079        private long initializeMaxItemId(SQLiteDatabase db) {
1080            Cursor c = db.rawQuery("SELECT MAX(_id) FROM favorites", null);
1081
1082            // get the result
1083            final int maxIdIndex = 0;
1084            long id = -1;
1085            if (c != null && c.moveToNext()) {
1086                id = c.getLong(maxIdIndex);
1087            }
1088            if (c != null) {
1089                c.close();
1090            }
1091
1092            if (id == -1) {
1093                throw new RuntimeException("Error: could not query max item id");
1094            }
1095
1096            return id;
1097        }
1098
1099        // Generates a new ID to use for an workspace screen in your database. This method
1100        // should be only called from the main UI thread. As an exception, we do call it when we
1101        // call the constructor from the worker thread; however, this doesn't extend until after the
1102        // constructor is called, and we only pass a reference to LauncherProvider to LauncherApp
1103        // after that point
1104        public long generateNewScreenId() {
1105            if (mMaxScreenId < 0) {
1106                throw new RuntimeException("Error: max screen id was not initialized");
1107            }
1108            mMaxScreenId += 1;
1109            // Log to disk
1110            Launcher.addDumpLog(TAG, "11683562 - generateNewScreenId(): " + mMaxScreenId, true);
1111            return mMaxScreenId;
1112        }
1113
1114        public void updateMaxScreenId(long maxScreenId) {
1115            // Log to disk
1116            Launcher.addDumpLog(TAG, "11683562 - updateMaxScreenId(): " + maxScreenId, true);
1117            mMaxScreenId = maxScreenId;
1118        }
1119
1120        private long initializeMaxScreenId(SQLiteDatabase db) {
1121            Cursor c = db.rawQuery("SELECT MAX(" + LauncherSettings.WorkspaceScreens._ID + ") FROM " + TABLE_WORKSPACE_SCREENS, null);
1122
1123            // get the result
1124            final int maxIdIndex = 0;
1125            long id = -1;
1126            if (c != null && c.moveToNext()) {
1127                id = c.getLong(maxIdIndex);
1128            }
1129            if (c != null) {
1130                c.close();
1131            }
1132
1133            if (id == -1) {
1134                throw new RuntimeException("Error: could not query max screen id");
1135            }
1136
1137            // Log to disk
1138            Launcher.addDumpLog(TAG, "11683562 - initializeMaxScreenId(): " + id, true);
1139            return id;
1140        }
1141
1142        /**
1143         * Upgrade existing clock and photo frame widgets into their new widget
1144         * equivalents.
1145         */
1146        private void convertWidgets(SQLiteDatabase db) {
1147            final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext);
1148            final int[] bindSources = new int[] {
1149                    Favorites.ITEM_TYPE_WIDGET_CLOCK,
1150                    Favorites.ITEM_TYPE_WIDGET_PHOTO_FRAME,
1151                    Favorites.ITEM_TYPE_WIDGET_SEARCH,
1152            };
1153
1154            final String selectWhere = buildOrWhereString(Favorites.ITEM_TYPE, bindSources);
1155
1156            Cursor c = null;
1157
1158            db.beginTransaction();
1159            try {
1160                // Select and iterate through each matching widget
1161                c = db.query(TABLE_FAVORITES, new String[] { Favorites._ID, Favorites.ITEM_TYPE },
1162                        selectWhere, null, null, null, null);
1163
1164                if (LOGD) Log.d(TAG, "found upgrade cursor count=" + c.getCount());
1165
1166                final ContentValues values = new ContentValues();
1167                while (c != null && c.moveToNext()) {
1168                    long favoriteId = c.getLong(0);
1169                    int favoriteType = c.getInt(1);
1170
1171                    // Allocate and update database with new appWidgetId
1172                    try {
1173                        int appWidgetId = mAppWidgetHost.allocateAppWidgetId();
1174
1175                        if (LOGD) {
1176                            Log.d(TAG, "allocated appWidgetId=" + appWidgetId
1177                                    + " for favoriteId=" + favoriteId);
1178                        }
1179                        values.clear();
1180                        values.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_APPWIDGET);
1181                        values.put(Favorites.APPWIDGET_ID, appWidgetId);
1182
1183                        // Original widgets might not have valid spans when upgrading
1184                        if (favoriteType == Favorites.ITEM_TYPE_WIDGET_SEARCH) {
1185                            values.put(LauncherSettings.Favorites.SPANX, 4);
1186                            values.put(LauncherSettings.Favorites.SPANY, 1);
1187                        } else {
1188                            values.put(LauncherSettings.Favorites.SPANX, 2);
1189                            values.put(LauncherSettings.Favorites.SPANY, 2);
1190                        }
1191
1192                        String updateWhere = Favorites._ID + "=" + favoriteId;
1193                        db.update(TABLE_FAVORITES, values, updateWhere, null);
1194
1195                        if (favoriteType == Favorites.ITEM_TYPE_WIDGET_CLOCK) {
1196                            // TODO: check return value
1197                            appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId,
1198                                    new ComponentName("com.android.alarmclock",
1199                                    "com.android.alarmclock.AnalogAppWidgetProvider"));
1200                        } else if (favoriteType == Favorites.ITEM_TYPE_WIDGET_PHOTO_FRAME) {
1201                            // TODO: check return value
1202                            appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId,
1203                                    new ComponentName("com.android.camera",
1204                                    "com.android.camera.PhotoAppWidgetProvider"));
1205                        } else if (favoriteType == Favorites.ITEM_TYPE_WIDGET_SEARCH) {
1206                            // TODO: check return value
1207                            appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId,
1208                                    getSearchWidgetProvider());
1209                        }
1210                    } catch (RuntimeException ex) {
1211                        Log.e(TAG, "Problem allocating appWidgetId", ex);
1212                    }
1213                }
1214
1215                db.setTransactionSuccessful();
1216            } catch (SQLException ex) {
1217                Log.w(TAG, "Problem while allocating appWidgetIds for existing widgets", ex);
1218            } finally {
1219                db.endTransaction();
1220                if (c != null) {
1221                    c.close();
1222                }
1223            }
1224
1225            // Update max item id
1226            mMaxItemId = initializeMaxItemId(db);
1227            if (LOGD) Log.d(TAG, "mMaxItemId: " + mMaxItemId);
1228        }
1229
1230        private boolean initializeExternalAdd(ContentValues values) {
1231            // 1. Ensure that externally added items have a valid item id
1232            long id = generateNewItemId();
1233            values.put(LauncherSettings.Favorites._ID, id);
1234
1235            // 2. In the case of an app widget, and if no app widget id is specified, we
1236            // attempt allocate and bind the widget.
1237            Integer itemType = values.getAsInteger(LauncherSettings.Favorites.ITEM_TYPE);
1238            if (itemType != null &&
1239                    itemType.intValue() == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET &&
1240                    !values.containsKey(LauncherSettings.Favorites.APPWIDGET_ID)) {
1241
1242                final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext);
1243                ComponentName cn = ComponentName.unflattenFromString(
1244                        values.getAsString(Favorites.APPWIDGET_PROVIDER));
1245
1246                if (cn != null) {
1247                    try {
1248                        int appWidgetId = mAppWidgetHost.allocateAppWidgetId();
1249                        values.put(LauncherSettings.Favorites.APPWIDGET_ID, appWidgetId);
1250                        if (!appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId,cn)) {
1251                            return false;
1252                        }
1253                    } catch (RuntimeException e) {
1254                        Log.e(TAG, "Failed to initialize external widget", e);
1255                        return false;
1256                    }
1257                } else {
1258                    return false;
1259                }
1260            }
1261
1262            // Add screen id if not present
1263            long screenId = values.getAsLong(LauncherSettings.Favorites.SCREEN);
1264            if (!addScreenIdIfNecessary(screenId)) {
1265                return false;
1266            }
1267            return true;
1268        }
1269
1270        // Returns true of screen id exists, or if successfully added
1271        private boolean addScreenIdIfNecessary(long screenId) {
1272            if (!hasScreenId(screenId)) {
1273                int rank = getMaxScreenRank() + 1;
1274
1275                ContentValues v = new ContentValues();
1276                v.put(LauncherSettings.WorkspaceScreens._ID, screenId);
1277                v.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, rank);
1278                if (dbInsertAndCheck(this, getWritableDatabase(),
1279                        TABLE_WORKSPACE_SCREENS, null, v) < 0) {
1280                    return false;
1281                }
1282            }
1283            return true;
1284        }
1285
1286        private boolean hasScreenId(long screenId) {
1287            SQLiteDatabase db = getWritableDatabase();
1288            Cursor c = db.rawQuery("SELECT * FROM " + TABLE_WORKSPACE_SCREENS + " WHERE "
1289                    + LauncherSettings.WorkspaceScreens._ID + " = " + screenId, null);
1290            if (c != null) {
1291                int count = c.getCount();
1292                c.close();
1293                return count > 0;
1294            } else {
1295                return false;
1296            }
1297        }
1298
1299        private int getMaxScreenRank() {
1300            SQLiteDatabase db = getWritableDatabase();
1301            Cursor c = db.rawQuery("SELECT MAX(" + LauncherSettings.WorkspaceScreens.SCREEN_RANK
1302                    + ") FROM " + TABLE_WORKSPACE_SCREENS, null);
1303
1304            // get the result
1305            final int maxRankIndex = 0;
1306            int rank = -1;
1307            if (c != null && c.moveToNext()) {
1308                rank = c.getInt(maxRankIndex);
1309            }
1310            if (c != null) {
1311                c.close();
1312            }
1313
1314            return rank;
1315        }
1316
1317        private int loadFavorites(SQLiteDatabase db, AutoInstallsLayout loader) {
1318            ArrayList<Long> screenIds = new ArrayList<Long>();
1319            // TODO: Use multiple loaders with fall-back and transaction.
1320            int count = loader.loadLayout(db, screenIds);
1321
1322            // Add the screens specified by the items above
1323            Collections.sort(screenIds);
1324            int rank = 0;
1325            ContentValues values = new ContentValues();
1326            for (Long id : screenIds) {
1327                values.clear();
1328                values.put(LauncherSettings.WorkspaceScreens._ID, id);
1329                values.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, rank);
1330                if (dbInsertAndCheck(this, db, TABLE_WORKSPACE_SCREENS, null, values) < 0) {
1331                    throw new RuntimeException("Failed initialize screen table"
1332                            + "from default layout");
1333                }
1334                rank++;
1335            }
1336
1337            // Ensure that the max ids are initialized
1338            mMaxItemId = initializeMaxItemId(db);
1339            mMaxScreenId = initializeMaxScreenId(db);
1340
1341            return count;
1342        }
1343
1344        private ComponentName getSearchWidgetProvider() {
1345            SearchManager searchManager =
1346                    (SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE);
1347            ComponentName searchComponent = searchManager.getGlobalSearchActivity();
1348            if (searchComponent == null) return null;
1349            return getProviderInPackage(searchComponent.getPackageName());
1350        }
1351
1352        /**
1353         * Gets an appwidget provider from the given package. If the package contains more than
1354         * one appwidget provider, an arbitrary one is returned.
1355         */
1356        private ComponentName getProviderInPackage(String packageName) {
1357            AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext);
1358            List<AppWidgetProviderInfo> providers = appWidgetManager.getInstalledProviders();
1359            if (providers == null) return null;
1360            final int providerCount = providers.size();
1361            for (int i = 0; i < providerCount; i++) {
1362                ComponentName provider = providers.get(i).provider;
1363                if (provider != null && provider.getPackageName().equals(packageName)) {
1364                    return provider;
1365                }
1366            }
1367            return null;
1368        }
1369
1370        private void migrateLauncher2Shortcuts(SQLiteDatabase db, Uri uri) {
1371            final ContentResolver resolver = mContext.getContentResolver();
1372            Cursor c = null;
1373            int count = 0;
1374            int curScreen = 0;
1375
1376            try {
1377                c = resolver.query(uri, null, null, null, "title ASC");
1378            } catch (Exception e) {
1379                // Ignore
1380            }
1381
1382            // We already have a favorites database in the old provider
1383            if (c != null) {
1384                try {
1385                    if (c.getCount() > 0) {
1386                        final int idIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID);
1387                        final int intentIndex
1388                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.INTENT);
1389                        final int titleIndex
1390                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.TITLE);
1391                        final int iconTypeIndex
1392                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_TYPE);
1393                        final int iconIndex
1394                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON);
1395                        final int iconPackageIndex
1396                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_PACKAGE);
1397                        final int iconResourceIndex
1398                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_RESOURCE);
1399                        final int containerIndex
1400                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CONTAINER);
1401                        final int itemTypeIndex
1402                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE);
1403                        final int screenIndex
1404                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SCREEN);
1405                        final int cellXIndex
1406                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLX);
1407                        final int cellYIndex
1408                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLY);
1409                        final int uriIndex
1410                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.URI);
1411                        final int displayModeIndex
1412                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.DISPLAY_MODE);
1413                        final int profileIndex
1414                                = c.getColumnIndex(LauncherSettings.Favorites.PROFILE_ID);
1415
1416                        int i = 0;
1417                        int curX = 0;
1418                        int curY = 0;
1419
1420                        final LauncherAppState app = LauncherAppState.getInstance();
1421                        final DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
1422                        final int width = (int) grid.numColumns;
1423                        final int height = (int) grid.numRows;
1424                        final int hotseatWidth = (int) grid.numHotseatIcons;
1425
1426                        final HashSet<String> seenIntents = new HashSet<String>(c.getCount());
1427
1428                        final ArrayList<ContentValues> shortcuts = new ArrayList<ContentValues>();
1429                        final ArrayList<ContentValues> folders = new ArrayList<ContentValues>();
1430                        final SparseArray<ContentValues> hotseat = new SparseArray<ContentValues>();
1431
1432                        while (c.moveToNext()) {
1433                            final int itemType = c.getInt(itemTypeIndex);
1434                            if (itemType != Favorites.ITEM_TYPE_APPLICATION
1435                                    && itemType != Favorites.ITEM_TYPE_SHORTCUT
1436                                    && itemType != Favorites.ITEM_TYPE_FOLDER) {
1437                                continue;
1438                            }
1439
1440                            final int cellX = c.getInt(cellXIndex);
1441                            final int cellY = c.getInt(cellYIndex);
1442                            final int screen = c.getInt(screenIndex);
1443                            int container = c.getInt(containerIndex);
1444                            final String intentStr = c.getString(intentIndex);
1445
1446                            UserManagerCompat userManager = UserManagerCompat.getInstance(mContext);
1447                            UserHandleCompat userHandle;
1448                            final long userSerialNumber;
1449                            if (profileIndex != -1 && !c.isNull(profileIndex)) {
1450                                userSerialNumber = c.getInt(profileIndex);
1451                                userHandle = userManager.getUserForSerialNumber(userSerialNumber);
1452                            } else {
1453                                // Default to the serial number of this user, for older
1454                                // shortcuts.
1455                                userHandle = UserHandleCompat.myUserHandle();
1456                                userSerialNumber = userManager.getSerialNumberForUser(userHandle);
1457                            }
1458                            Launcher.addDumpLog(TAG, "migrating \""
1459                                + c.getString(titleIndex) + "\" ("
1460                                + cellX + "," + cellY + "@"
1461                                + LauncherSettings.Favorites.containerToString(container)
1462                                + "/" + screen
1463                                + "): " + intentStr, true);
1464
1465                            if (itemType != Favorites.ITEM_TYPE_FOLDER) {
1466
1467                                final Intent intent;
1468                                final ComponentName cn;
1469                                try {
1470                                    intent = Intent.parseUri(intentStr, 0);
1471                                } catch (URISyntaxException e) {
1472                                    // bogus intent?
1473                                    Launcher.addDumpLog(TAG,
1474                                            "skipping invalid intent uri", true);
1475                                    continue;
1476                                }
1477
1478                                cn = intent.getComponent();
1479                                if (TextUtils.isEmpty(intentStr)) {
1480                                    // no intent? no icon
1481                                    Launcher.addDumpLog(TAG, "skipping empty intent", true);
1482                                    continue;
1483                                } else if (cn != null &&
1484                                        !LauncherModel.isValidPackageActivity(mContext, cn,
1485                                                userHandle)) {
1486                                    // component no longer exists.
1487                                    Launcher.addDumpLog(TAG, "skipping item whose component " +
1488                                            "no longer exists.", true);
1489                                    continue;
1490                                } else if (container ==
1491                                        LauncherSettings.Favorites.CONTAINER_DESKTOP) {
1492                                    // Dedupe icons directly on the workspace
1493
1494                                    // Canonicalize
1495                                    // the Play Store sets the package parameter, but Launcher
1496                                    // does not, so we clear that out to keep them the same.
1497                                    // Also ignore intent flags for the purposes of deduping.
1498                                    intent.setPackage(null);
1499                                    int flags = intent.getFlags();
1500                                    intent.setFlags(0);
1501                                    final String key = intent.toUri(0);
1502                                    intent.setFlags(flags);
1503                                    if (seenIntents.contains(key)) {
1504                                        Launcher.addDumpLog(TAG, "skipping duplicate", true);
1505                                        continue;
1506                                    } else {
1507                                        seenIntents.add(key);
1508                                    }
1509                                }
1510                            }
1511
1512                            ContentValues values = new ContentValues(c.getColumnCount());
1513                            values.put(LauncherSettings.Favorites._ID, c.getInt(idIndex));
1514                            values.put(LauncherSettings.Favorites.INTENT, intentStr);
1515                            values.put(LauncherSettings.Favorites.TITLE, c.getString(titleIndex));
1516                            values.put(LauncherSettings.Favorites.ICON_TYPE,
1517                                    c.getInt(iconTypeIndex));
1518                            values.put(LauncherSettings.Favorites.ICON, c.getBlob(iconIndex));
1519                            values.put(LauncherSettings.Favorites.ICON_PACKAGE,
1520                                    c.getString(iconPackageIndex));
1521                            values.put(LauncherSettings.Favorites.ICON_RESOURCE,
1522                                    c.getString(iconResourceIndex));
1523                            values.put(LauncherSettings.Favorites.ITEM_TYPE, itemType);
1524                            values.put(LauncherSettings.Favorites.APPWIDGET_ID, -1);
1525                            values.put(LauncherSettings.Favorites.URI, c.getString(uriIndex));
1526                            values.put(LauncherSettings.Favorites.DISPLAY_MODE,
1527                                    c.getInt(displayModeIndex));
1528                            values.put(LauncherSettings.Favorites.PROFILE_ID, userSerialNumber);
1529
1530                            if (container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
1531                                hotseat.put(screen, values);
1532                            }
1533
1534                            if (container != LauncherSettings.Favorites.CONTAINER_DESKTOP) {
1535                                // In a folder or in the hotseat, preserve position
1536                                values.put(LauncherSettings.Favorites.SCREEN, screen);
1537                                values.put(LauncherSettings.Favorites.CELLX, cellX);
1538                                values.put(LauncherSettings.Favorites.CELLY, cellY);
1539                            } else {
1540                                // For items contained directly on one of the workspace screen,
1541                                // we'll determine their location (screen, x, y) in a second pass.
1542                            }
1543
1544                            values.put(LauncherSettings.Favorites.CONTAINER, container);
1545
1546                            if (itemType != Favorites.ITEM_TYPE_FOLDER) {
1547                                shortcuts.add(values);
1548                            } else {
1549                                folders.add(values);
1550                            }
1551                        }
1552
1553                        // Now that we have all the hotseat icons, let's go through them left-right
1554                        // and assign valid locations for them in the new hotseat
1555                        final int N = hotseat.size();
1556                        for (int idx=0; idx<N; idx++) {
1557                            int hotseatX = hotseat.keyAt(idx);
1558                            ContentValues values = hotseat.valueAt(idx);
1559
1560                            if (hotseatX == grid.hotseatAllAppsRank) {
1561                                // let's drop this in the next available hole in the hotseat
1562                                while (++hotseatX < hotseatWidth) {
1563                                    if (hotseat.get(hotseatX) == null) {
1564                                        // found a spot! move it here
1565                                        values.put(LauncherSettings.Favorites.SCREEN,
1566                                                hotseatX);
1567                                        break;
1568                                    }
1569                                }
1570                            }
1571                            if (hotseatX >= hotseatWidth) {
1572                                // no room for you in the hotseat? it's off to the desktop with you
1573                                values.put(LauncherSettings.Favorites.CONTAINER,
1574                                           Favorites.CONTAINER_DESKTOP);
1575                            }
1576                        }
1577
1578                        final ArrayList<ContentValues> allItems = new ArrayList<ContentValues>();
1579                        // Folders first
1580                        allItems.addAll(folders);
1581                        // Then shortcuts
1582                        allItems.addAll(shortcuts);
1583
1584                        // Layout all the folders
1585                        for (ContentValues values: allItems) {
1586                            if (values.getAsInteger(LauncherSettings.Favorites.CONTAINER) !=
1587                                    LauncherSettings.Favorites.CONTAINER_DESKTOP) {
1588                                // Hotseat items and folder items have already had their
1589                                // location information set. Nothing to be done here.
1590                                continue;
1591                            }
1592                            values.put(LauncherSettings.Favorites.SCREEN, curScreen);
1593                            values.put(LauncherSettings.Favorites.CELLX, curX);
1594                            values.put(LauncherSettings.Favorites.CELLY, curY);
1595                            curX = (curX + 1) % width;
1596                            if (curX == 0) {
1597                                curY = (curY + 1);
1598                            }
1599                            // Leave the last row of icons blank on every screen
1600                            if (curY == height - 1) {
1601                                curScreen = (int) generateNewScreenId();
1602                                curY = 0;
1603                            }
1604                        }
1605
1606                        if (allItems.size() > 0) {
1607                            db.beginTransaction();
1608                            try {
1609                                for (ContentValues row: allItems) {
1610                                    if (row == null) continue;
1611                                    if (dbInsertAndCheck(this, db, TABLE_FAVORITES, null, row)
1612                                            < 0) {
1613                                        return;
1614                                    } else {
1615                                        count++;
1616                                    }
1617                                }
1618                                db.setTransactionSuccessful();
1619                            } finally {
1620                                db.endTransaction();
1621                            }
1622                        }
1623
1624                        db.beginTransaction();
1625                        try {
1626                            for (i=0; i<=curScreen; i++) {
1627                                final ContentValues values = new ContentValues();
1628                                values.put(LauncherSettings.WorkspaceScreens._ID, i);
1629                                values.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, i);
1630                                if (dbInsertAndCheck(this, db, TABLE_WORKSPACE_SCREENS, null, values)
1631                                        < 0) {
1632                                    return;
1633                                }
1634                            }
1635                            db.setTransactionSuccessful();
1636                        } finally {
1637                            db.endTransaction();
1638                        }
1639                    }
1640                } finally {
1641                    c.close();
1642                }
1643            }
1644
1645            Launcher.addDumpLog(TAG, "migrated " + count + " icons from Launcher2 into "
1646                    + (curScreen+1) + " screens", true);
1647
1648            // ensure that new screens are created to hold these icons
1649            setFlagJustLoadedOldDb();
1650
1651            // Update max IDs; very important since we just grabbed IDs from another database
1652            mMaxItemId = initializeMaxItemId(db);
1653            mMaxScreenId = initializeMaxScreenId(db);
1654            if (LOGD) Log.d(TAG, "mMaxItemId: " + mMaxItemId + " mMaxScreenId: " + mMaxScreenId);
1655        }
1656    }
1657
1658    /**
1659     * Build a query string that will match any row where the column matches
1660     * anything in the values list.
1661     */
1662    private static String buildOrWhereString(String column, int[] values) {
1663        StringBuilder selectWhere = new StringBuilder();
1664        for (int i = values.length - 1; i >= 0; i--) {
1665            selectWhere.append(column).append("=").append(values[i]);
1666            if (i > 0) {
1667                selectWhere.append(" OR ");
1668            }
1669        }
1670        return selectWhere.toString();
1671    }
1672
1673    static class SqlArguments {
1674        public final String table;
1675        public final String where;
1676        public final String[] args;
1677
1678        SqlArguments(Uri url, String where, String[] args) {
1679            if (url.getPathSegments().size() == 1) {
1680                this.table = url.getPathSegments().get(0);
1681                this.where = where;
1682                this.args = args;
1683            } else if (url.getPathSegments().size() != 2) {
1684                throw new IllegalArgumentException("Invalid URI: " + url);
1685            } else if (!TextUtils.isEmpty(where)) {
1686                throw new UnsupportedOperationException("WHERE clause not supported: " + url);
1687            } else {
1688                this.table = url.getPathSegments().get(0);
1689                this.where = "_id=" + ContentUris.parseId(url);
1690                this.args = null;
1691            }
1692        }
1693
1694        SqlArguments(Uri url) {
1695            if (url.getPathSegments().size() == 1) {
1696                table = url.getPathSegments().get(0);
1697                where = null;
1698                args = null;
1699            } else {
1700                throw new IllegalArgumentException("Invalid URI: " + url);
1701            }
1702        }
1703    }
1704}
1705