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.pm.ActivityInfo;
35import android.content.pm.ApplicationInfo;
36import android.content.pm.PackageManager;
37import android.content.pm.ResolveInfo;
38import android.content.res.Resources;
39import android.content.res.XmlResourceParser;
40import android.database.Cursor;
41import android.database.SQLException;
42import android.database.sqlite.SQLiteDatabase;
43import android.database.sqlite.SQLiteOpenHelper;
44import android.database.sqlite.SQLiteQueryBuilder;
45import android.database.sqlite.SQLiteStatement;
46import android.graphics.Bitmap;
47import android.graphics.BitmapFactory;
48import android.net.Uri;
49import android.os.Bundle;
50import android.provider.Settings;
51import android.text.TextUtils;
52import android.util.Log;
53import android.util.SparseArray;
54
55import com.android.launcher3.AutoInstallsLayout.LayoutParserCallback;
56import com.android.launcher3.LauncherSettings.Favorites;
57import com.android.launcher3.compat.UserHandleCompat;
58import com.android.launcher3.compat.UserManagerCompat;
59import com.android.launcher3.config.ProviderConfig;
60
61import org.xmlpull.v1.XmlPullParser;
62import org.xmlpull.v1.XmlPullParserException;
63
64import java.io.File;
65import java.io.IOException;
66import java.net.URISyntaxException;
67import java.util.ArrayList;
68import java.util.Collections;
69import java.util.HashSet;
70import java.util.List;
71
72public class LauncherProvider extends ContentProvider {
73    private static final String TAG = "Launcher.LauncherProvider";
74    private static final boolean LOGD = false;
75
76    private static final String DATABASE_NAME = "launcher.db";
77
78    private static final int DATABASE_VERSION = 20;
79
80    static final String OLD_AUTHORITY = "com.android.launcher2.settings";
81    static final String AUTHORITY = ProviderConfig.AUTHORITY;
82
83    // Should we attempt to load anything from the com.android.launcher2 provider?
84    static final boolean IMPORT_LAUNCHER2_DATABASE = false;
85
86    static final String TABLE_FAVORITES = "favorites";
87    static final String TABLE_WORKSPACE_SCREENS = "workspaceScreens";
88    static final String PARAMETER_NOTIFY = "notify";
89    static final String UPGRADED_FROM_OLD_DATABASE =
90            "UPGRADED_FROM_OLD_DATABASE";
91    static final String EMPTY_DATABASE_CREATED =
92            "EMPTY_DATABASE_CREATED";
93
94    private static final String ACTION_APPWIDGET_DEFAULT_WORKSPACE_CONFIGURE =
95            "com.android.launcher.action.APPWIDGET_DEFAULT_WORKSPACE_CONFIGURE";
96
97    private static final String URI_PARAM_IS_EXTERNAL_ADD = "isExternalAdd";
98
99    private LauncherProviderChangeListener mListener;
100
101    /**
102     * {@link Uri} triggered at any registered {@link android.database.ContentObserver} when
103     * {@link AppWidgetHost#deleteHost()} is called during database creation.
104     * Use this to recall {@link AppWidgetHost#startListening()} if needed.
105     */
106    static final Uri CONTENT_APPWIDGET_RESET_URI =
107            Uri.parse("content://" + AUTHORITY + "/appWidgetReset");
108
109    private DatabaseHelper mOpenHelper;
110    private static boolean sJustLoadedFromOldDb;
111
112    @Override
113    public boolean onCreate() {
114        final Context context = getContext();
115        mOpenHelper = new DatabaseHelper(context);
116        LauncherAppState.setLauncherProvider(this);
117        return true;
118    }
119
120    public boolean wasNewDbCreated() {
121        return mOpenHelper.wasNewDbCreated();
122    }
123
124    public void setLauncherProviderChangeListener(LauncherProviderChangeListener listener) {
125        mListener = listener;
126    }
127
128    @Override
129    public String getType(Uri uri) {
130        SqlArguments args = new SqlArguments(uri, null, null);
131        if (TextUtils.isEmpty(args.where)) {
132            return "vnd.android.cursor.dir/" + args.table;
133        } else {
134            return "vnd.android.cursor.item/" + args.table;
135        }
136    }
137
138    @Override
139    public Cursor query(Uri uri, String[] projection, String selection,
140            String[] selectionArgs, String sortOrder) {
141
142        SqlArguments args = new SqlArguments(uri, selection, selectionArgs);
143        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
144        qb.setTables(args.table);
145
146        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
147        Cursor result = qb.query(db, projection, args.where, args.args, null, null, sortOrder);
148        result.setNotificationUri(getContext().getContentResolver(), uri);
149
150        return result;
151    }
152
153    private static long dbInsertAndCheck(DatabaseHelper helper,
154            SQLiteDatabase db, String table, String nullColumnHack, ContentValues values) {
155        if (values == null) {
156            throw new RuntimeException("Error: attempting to insert null values");
157        }
158        if (!values.containsKey(LauncherSettings.ChangeLogColumns._ID)) {
159            throw new RuntimeException("Error: attempting to add item without specifying an id");
160        }
161        helper.checkId(table, values);
162        return db.insert(table, nullColumnHack, values);
163    }
164
165    private static void deleteId(SQLiteDatabase db, long id) {
166        Uri uri = LauncherSettings.Favorites.getContentUri(id, false);
167        SqlArguments args = new SqlArguments(uri, null, null);
168        db.delete(args.table, args.where, args.args);
169    }
170
171    @Override
172    public Uri insert(Uri uri, ContentValues initialValues) {
173        SqlArguments args = new SqlArguments(uri);
174
175        // In very limited cases, we support system|signature permission apps to add to the db
176        String externalAdd = uri.getQueryParameter(URI_PARAM_IS_EXTERNAL_ADD);
177        if (externalAdd != null && "true".equals(externalAdd)) {
178            if (!mOpenHelper.initializeExternalAdd(initialValues)) {
179                return null;
180            }
181        }
182
183        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
184        addModifiedTime(initialValues);
185        final long rowId = dbInsertAndCheck(mOpenHelper, db, args.table, null, initialValues);
186        if (rowId <= 0) return null;
187
188        uri = ContentUris.withAppendedId(uri, rowId);
189        sendNotify(uri);
190
191        return uri;
192    }
193
194
195    @Override
196    public int bulkInsert(Uri uri, ContentValues[] values) {
197        SqlArguments args = new SqlArguments(uri);
198
199        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
200        db.beginTransaction();
201        try {
202            int numValues = values.length;
203            for (int i = 0; i < numValues; i++) {
204                addModifiedTime(values[i]);
205                if (dbInsertAndCheck(mOpenHelper, db, args.table, null, values[i]) < 0) {
206                    return 0;
207                }
208            }
209            db.setTransactionSuccessful();
210        } finally {
211            db.endTransaction();
212        }
213
214        sendNotify(uri);
215        return values.length;
216    }
217
218    @Override
219    public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
220            throws OperationApplicationException {
221        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
222        db.beginTransaction();
223        try {
224            ContentProviderResult[] result =  super.applyBatch(operations);
225            db.setTransactionSuccessful();
226            return result;
227        } finally {
228            db.endTransaction();
229        }
230    }
231
232    @Override
233    public int delete(Uri uri, String selection, String[] selectionArgs) {
234        SqlArguments args = new SqlArguments(uri, selection, selectionArgs);
235
236        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
237        int count = db.delete(args.table, args.where, args.args);
238        if (count > 0) sendNotify(uri);
239
240        return count;
241    }
242
243    @Override
244    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
245        SqlArguments args = new SqlArguments(uri, selection, selectionArgs);
246
247        addModifiedTime(values);
248        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
249        int count = db.update(args.table, values, args.where, args.args);
250        if (count > 0) sendNotify(uri);
251
252        return count;
253    }
254
255    private void sendNotify(Uri uri) {
256        String notify = uri.getQueryParameter(PARAMETER_NOTIFY);
257        if (notify == null || "true".equals(notify)) {
258            getContext().getContentResolver().notifyChange(uri, null);
259        }
260
261        // always notify the backup agent
262        LauncherBackupAgentHelper.dataChanged(getContext());
263        if (mListener != null) {
264            mListener.onLauncherProviderChange();
265        }
266    }
267
268    private void addModifiedTime(ContentValues values) {
269        values.put(LauncherSettings.ChangeLogColumns.MODIFIED, System.currentTimeMillis());
270    }
271
272    public long generateNewItemId() {
273        return mOpenHelper.generateNewItemId();
274    }
275
276    public void updateMaxItemId(long id) {
277        mOpenHelper.updateMaxItemId(id);
278    }
279
280    public long generateNewScreenId() {
281        return mOpenHelper.generateNewScreenId();
282    }
283
284    // This is only required one time while loading the workspace during the
285    // upgrade path, and should never be called from anywhere else.
286    public void updateMaxScreenId(long maxScreenId) {
287        mOpenHelper.updateMaxScreenId(maxScreenId);
288    }
289
290    /**
291     * @param Should we load the old db for upgrade? first run only.
292     */
293    synchronized public boolean justLoadedOldDb() {
294        String spKey = LauncherAppState.getSharedPreferencesKey();
295        SharedPreferences sp = getContext().getSharedPreferences(spKey, Context.MODE_PRIVATE);
296
297        boolean loadedOldDb = false || sJustLoadedFromOldDb;
298
299        sJustLoadedFromOldDb = false;
300        if (sp.getBoolean(UPGRADED_FROM_OLD_DATABASE, false)) {
301
302            SharedPreferences.Editor editor = sp.edit();
303            editor.remove(UPGRADED_FROM_OLD_DATABASE);
304            editor.commit();
305            loadedOldDb = true;
306        }
307        return loadedOldDb;
308    }
309
310    /**
311     * Clears all the data for a fresh start.
312     */
313    synchronized public void createEmptyDB() {
314        mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
315    }
316
317    /**
318     * Loads the default workspace based on the following priority scheme:
319     *   1) From a package provided by play store
320     *   2) From a partner configuration APK, already in the system image
321     *   3) The default configuration for the particular device
322     */
323    synchronized public void loadDefaultFavoritesIfNecessary() {
324        String spKey = LauncherAppState.getSharedPreferencesKey();
325        SharedPreferences sp = getContext().getSharedPreferences(spKey, Context.MODE_PRIVATE);
326
327        if (sp.getBoolean(EMPTY_DATABASE_CREATED, false)) {
328            Log.d(TAG, "loading default workspace");
329
330            WorkspaceLoader loader = AutoInstallsLayout.get(getContext(),
331                    mOpenHelper.mAppWidgetHost, mOpenHelper);
332
333            if (loader == null) {
334                final Partner partner = Partner.get(getContext().getPackageManager());
335                if (partner != null && partner.hasDefaultLayout()) {
336                    final Resources partnerRes = partner.getResources();
337                    int workspaceResId = partnerRes.getIdentifier(Partner.RES_DEFAULT_LAYOUT,
338                            "xml", partner.getPackageName());
339                    if (workspaceResId != 0) {
340                        loader = new SimpleWorkspaceLoader(mOpenHelper, partnerRes, workspaceResId);
341                    }
342                }
343            }
344
345            if (loader == null) {
346                loader = new SimpleWorkspaceLoader(mOpenHelper, getContext().getResources(),
347                        getDefaultWorkspaceResourceId());
348            }
349
350            // Populate favorites table with initial favorites
351            SharedPreferences.Editor editor = sp.edit().remove(EMPTY_DATABASE_CREATED);
352            mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), loader);
353            editor.commit();
354        }
355    }
356
357    public void migrateLauncher2Shortcuts() {
358        mOpenHelper.migrateLauncher2Shortcuts(mOpenHelper.getWritableDatabase(),
359                Uri.parse(getContext().getString(R.string.old_launcher_provider_uri)));
360    }
361
362    private static int getDefaultWorkspaceResourceId() {
363        LauncherAppState app = LauncherAppState.getInstance();
364        DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
365        if (LauncherAppState.isDisableAllApps()) {
366            return grid.defaultNoAllAppsLayoutId;
367        } else {
368            return grid.defaultLayoutId;
369        }
370    }
371
372    private static interface ContentValuesCallback {
373        public void onRow(ContentValues values);
374    }
375
376    private static boolean shouldImportLauncher2Database(Context context) {
377        boolean isTablet = context.getResources().getBoolean(R.bool.is_tablet);
378
379        // We don't import the old databse for tablets, as the grid size has changed.
380        return !isTablet && IMPORT_LAUNCHER2_DATABASE;
381    }
382
383    public void deleteDatabase() {
384        // Are you sure? (y/n)
385        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
386        final File dbFile = new File(db.getPath());
387        mOpenHelper.close();
388        if (dbFile.exists()) {
389            SQLiteDatabase.deleteDatabase(dbFile);
390        }
391        mOpenHelper = new DatabaseHelper(getContext());
392    }
393
394    private static class DatabaseHelper extends SQLiteOpenHelper implements LayoutParserCallback {
395        private static final String TAG_RESOLVE = "resolve";
396        private static final String TAG_FAVORITES = "favorites";
397        private static final String TAG_FAVORITE = "favorite";
398        private static final String TAG_APPWIDGET = "appwidget";
399        private static final String TAG_SHORTCUT = "shortcut";
400        private static final String TAG_FOLDER = "folder";
401        private static final String TAG_PARTNER_FOLDER = "partner-folder";
402        private static final String TAG_EXTRA = "extra";
403        private static final String TAG_INCLUDE = "include";
404
405        // Style attrs -- "Favorite"
406        private static final String ATTR_CLASS_NAME = "className";
407        private static final String ATTR_PACKAGE_NAME = "packageName";
408        private static final String ATTR_CONTAINER = "container";
409        private static final String ATTR_SCREEN = "screen";
410        private static final String ATTR_X = "x";
411        private static final String ATTR_Y = "y";
412        private static final String ATTR_SPAN_X = "spanX";
413        private static final String ATTR_SPAN_Y = "spanY";
414        private static final String ATTR_ICON = "icon";
415        private static final String ATTR_TITLE = "title";
416        private static final String ATTR_URI = "uri";
417
418        // Style attrs -- "Include"
419        private static final String ATTR_WORKSPACE = "workspace";
420
421        // Style attrs -- "Extra"
422        private static final String ATTR_KEY = "key";
423        private static final String ATTR_VALUE = "value";
424
425        private final Context mContext;
426        private final PackageManager mPackageManager;
427        private final AppWidgetHost mAppWidgetHost;
428        private long mMaxItemId = -1;
429        private long mMaxScreenId = -1;
430
431        private boolean mNewDbCreated = false;
432
433        DatabaseHelper(Context context) {
434            super(context, DATABASE_NAME, null, DATABASE_VERSION);
435            mContext = context;
436            mPackageManager = context.getPackageManager();
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        /**
454         * Send notification that we've deleted the {@link AppWidgetHost},
455         * probably as part of the initial database creation. The receiver may
456         * want to re-call {@link AppWidgetHost#startListening()} to ensure
457         * callbacks are correctly set.
458         */
459        private void sendAppWidgetResetNotify() {
460            final ContentResolver resolver = mContext.getContentResolver();
461            resolver.notifyChange(CONTENT_APPWIDGET_RESET_URI, null);
462        }
463
464        @Override
465        public void onCreate(SQLiteDatabase db) {
466            if (LOGD) Log.d(TAG, "creating new launcher database");
467
468            mMaxItemId = 1;
469            mMaxScreenId = 0;
470            mNewDbCreated = true;
471
472            UserManagerCompat userManager = UserManagerCompat.getInstance(mContext);
473            long userSerialNumber = userManager.getSerialNumberForUser(
474                    UserHandleCompat.myUserHandle());
475
476            db.execSQL("CREATE TABLE favorites (" +
477                    "_id INTEGER PRIMARY KEY," +
478                    "title TEXT," +
479                    "intent TEXT," +
480                    "container INTEGER," +
481                    "screen INTEGER," +
482                    "cellX INTEGER," +
483                    "cellY INTEGER," +
484                    "spanX INTEGER," +
485                    "spanY INTEGER," +
486                    "itemType INTEGER," +
487                    "appWidgetId INTEGER NOT NULL DEFAULT -1," +
488                    "isShortcut INTEGER," +
489                    "iconType INTEGER," +
490                    "iconPackage TEXT," +
491                    "iconResource TEXT," +
492                    "icon BLOB," +
493                    "uri TEXT," +
494                    "displayMode INTEGER," +
495                    "appWidgetProvider TEXT," +
496                    "modified INTEGER NOT NULL DEFAULT 0," +
497                    "restored INTEGER NOT NULL DEFAULT 0," +
498                    "profileId INTEGER DEFAULT " + userSerialNumber +
499                    ");");
500            addWorkspacesTable(db);
501
502            // Database was just created, so wipe any previous widgets
503            if (mAppWidgetHost != null) {
504                mAppWidgetHost.deleteHost();
505                sendAppWidgetResetNotify();
506            }
507
508            if (shouldImportLauncher2Database(mContext)) {
509                // Try converting the old database
510                ContentValuesCallback permuteScreensCb = new ContentValuesCallback() {
511                    public void onRow(ContentValues values) {
512                        int container = values.getAsInteger(LauncherSettings.Favorites.CONTAINER);
513                        if (container == Favorites.CONTAINER_DESKTOP) {
514                            int screen = values.getAsInteger(LauncherSettings.Favorites.SCREEN);
515                            screen = (int) upgradeLauncherDb_permuteScreens(screen);
516                            values.put(LauncherSettings.Favorites.SCREEN, screen);
517                        }
518                    }
519                };
520                Uri uri = Uri.parse("content://" + Settings.AUTHORITY +
521                        "/old_favorites?notify=true");
522                if (!convertDatabase(db, uri, permuteScreensCb, true)) {
523                    // Try and upgrade from the Launcher2 db
524                    uri = Uri.parse(mContext.getString(R.string.old_launcher_provider_uri));
525                    if (!convertDatabase(db, uri, permuteScreensCb, false)) {
526                        // If we fail, then set a flag to load the default workspace
527                        setFlagEmptyDbCreated();
528                        return;
529                    }
530                }
531                // Right now, in non-default workspace cases, we want to run the final
532                // upgrade code (ie. to fix workspace screen indices -> ids, etc.), so
533                // set that flag too.
534                setFlagJustLoadedOldDb();
535            } else {
536                // Fresh and clean launcher DB.
537                mMaxItemId = initializeMaxItemId(db);
538                setFlagEmptyDbCreated();
539            }
540        }
541
542        private void addWorkspacesTable(SQLiteDatabase db) {
543            db.execSQL("CREATE TABLE " + TABLE_WORKSPACE_SCREENS + " (" +
544                    LauncherSettings.WorkspaceScreens._ID + " INTEGER," +
545                    LauncherSettings.WorkspaceScreens.SCREEN_RANK + " INTEGER," +
546                    LauncherSettings.ChangeLogColumns.MODIFIED + " INTEGER NOT NULL DEFAULT 0" +
547                    ");");
548        }
549
550        private void removeOrphanedItems(SQLiteDatabase db) {
551            // Delete items directly on the workspace who's screen id doesn't exist
552            //  "DELETE FROM favorites WHERE screen NOT IN (SELECT _id FROM workspaceScreens)
553            //   AND container = -100"
554            String removeOrphanedDesktopItems = "DELETE FROM " + TABLE_FAVORITES +
555                    " WHERE " +
556                    LauncherSettings.Favorites.SCREEN + " NOT IN (SELECT " +
557                    LauncherSettings.WorkspaceScreens._ID + " FROM " + TABLE_WORKSPACE_SCREENS + ")" +
558                    " AND " +
559                    LauncherSettings.Favorites.CONTAINER + " = " +
560                    LauncherSettings.Favorites.CONTAINER_DESKTOP;
561            db.execSQL(removeOrphanedDesktopItems);
562
563            // Delete items contained in folders which no longer exist (after above statement)
564            //  "DELETE FROM favorites  WHERE container <> -100 AND container <> -101 AND container
565            //   NOT IN (SELECT _id FROM favorites WHERE itemType = 2)"
566            String removeOrphanedFolderItems = "DELETE FROM " + TABLE_FAVORITES +
567                    " WHERE " +
568                    LauncherSettings.Favorites.CONTAINER + " <> " +
569                    LauncherSettings.Favorites.CONTAINER_DESKTOP +
570                    " AND "
571                    + LauncherSettings.Favorites.CONTAINER + " <> " +
572                    LauncherSettings.Favorites.CONTAINER_HOTSEAT +
573                    " AND "
574                    + LauncherSettings.Favorites.CONTAINER + " NOT IN (SELECT " +
575                    LauncherSettings.Favorites._ID + " FROM " + TABLE_FAVORITES +
576                    " WHERE " + LauncherSettings.Favorites.ITEM_TYPE + " = " +
577                    LauncherSettings.Favorites.ITEM_TYPE_FOLDER + ")";
578            db.execSQL(removeOrphanedFolderItems);
579        }
580
581        private void setFlagJustLoadedOldDb() {
582            String spKey = LauncherAppState.getSharedPreferencesKey();
583            SharedPreferences sp = mContext.getSharedPreferences(spKey, Context.MODE_PRIVATE);
584            SharedPreferences.Editor editor = sp.edit();
585            editor.putBoolean(UPGRADED_FROM_OLD_DATABASE, true);
586            editor.putBoolean(EMPTY_DATABASE_CREATED, false);
587            editor.commit();
588        }
589
590        private void setFlagEmptyDbCreated() {
591            String spKey = LauncherAppState.getSharedPreferencesKey();
592            SharedPreferences sp = mContext.getSharedPreferences(spKey, Context.MODE_PRIVATE);
593            SharedPreferences.Editor editor = sp.edit();
594            editor.putBoolean(EMPTY_DATABASE_CREATED, true);
595            editor.putBoolean(UPGRADED_FROM_OLD_DATABASE, false);
596            editor.commit();
597        }
598
599        // We rearrange the screens from the old launcher
600        // 12345 -> 34512
601        private long upgradeLauncherDb_permuteScreens(long screen) {
602            if (screen >= 2) {
603                return screen - 2;
604            } else {
605                return screen + 3;
606            }
607        }
608
609        private boolean convertDatabase(SQLiteDatabase db, Uri uri,
610                                        ContentValuesCallback cb, boolean deleteRows) {
611            if (LOGD) Log.d(TAG, "converting database from an older format, but not onUpgrade");
612            boolean converted = false;
613
614            final ContentResolver resolver = mContext.getContentResolver();
615            Cursor cursor = null;
616
617            try {
618                cursor = resolver.query(uri, null, null, null, null);
619            } catch (Exception e) {
620                // Ignore
621            }
622
623            // We already have a favorites database in the old provider
624            if (cursor != null) {
625                try {
626                     if (cursor.getCount() > 0) {
627                        converted = copyFromCursor(db, cursor, cb) > 0;
628                        if (converted && deleteRows) {
629                            resolver.delete(uri, null, null);
630                        }
631                    }
632                } finally {
633                    cursor.close();
634                }
635            }
636
637            if (converted) {
638                // Convert widgets from this import into widgets
639                if (LOGD) Log.d(TAG, "converted and now triggering widget upgrade");
640                convertWidgets(db);
641
642                // Update max item id
643                mMaxItemId = initializeMaxItemId(db);
644                if (LOGD) Log.d(TAG, "mMaxItemId: " + mMaxItemId);
645            }
646
647            return converted;
648        }
649
650        private int copyFromCursor(SQLiteDatabase db, Cursor c, ContentValuesCallback cb) {
651            final int idIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID);
652            final int intentIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.INTENT);
653            final int titleIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.TITLE);
654            final int iconTypeIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_TYPE);
655            final int iconIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON);
656            final int iconPackageIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_PACKAGE);
657            final int iconResourceIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_RESOURCE);
658            final int containerIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CONTAINER);
659            final int itemTypeIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE);
660            final int screenIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SCREEN);
661            final int cellXIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLX);
662            final int cellYIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLY);
663            final int uriIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.URI);
664            final int displayModeIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.DISPLAY_MODE);
665
666            ContentValues[] rows = new ContentValues[c.getCount()];
667            int i = 0;
668            while (c.moveToNext()) {
669                ContentValues values = new ContentValues(c.getColumnCount());
670                values.put(LauncherSettings.Favorites._ID, c.getLong(idIndex));
671                values.put(LauncherSettings.Favorites.INTENT, c.getString(intentIndex));
672                values.put(LauncherSettings.Favorites.TITLE, c.getString(titleIndex));
673                values.put(LauncherSettings.Favorites.ICON_TYPE, c.getInt(iconTypeIndex));
674                values.put(LauncherSettings.Favorites.ICON, c.getBlob(iconIndex));
675                values.put(LauncherSettings.Favorites.ICON_PACKAGE, c.getString(iconPackageIndex));
676                values.put(LauncherSettings.Favorites.ICON_RESOURCE, c.getString(iconResourceIndex));
677                values.put(LauncherSettings.Favorites.CONTAINER, c.getInt(containerIndex));
678                values.put(LauncherSettings.Favorites.ITEM_TYPE, c.getInt(itemTypeIndex));
679                values.put(LauncherSettings.Favorites.APPWIDGET_ID, -1);
680                values.put(LauncherSettings.Favorites.SCREEN, c.getInt(screenIndex));
681                values.put(LauncherSettings.Favorites.CELLX, c.getInt(cellXIndex));
682                values.put(LauncherSettings.Favorites.CELLY, c.getInt(cellYIndex));
683                values.put(LauncherSettings.Favorites.URI, c.getString(uriIndex));
684                values.put(LauncherSettings.Favorites.DISPLAY_MODE, c.getInt(displayModeIndex));
685                if (cb != null) {
686                    cb.onRow(values);
687                }
688                rows[i++] = values;
689            }
690
691            int total = 0;
692            if (i > 0) {
693                db.beginTransaction();
694                try {
695                    int numValues = rows.length;
696                    for (i = 0; i < numValues; i++) {
697                        if (dbInsertAndCheck(this, db, TABLE_FAVORITES, null, rows[i]) < 0) {
698                            return 0;
699                        } else {
700                            total++;
701                        }
702                    }
703                    db.setTransactionSuccessful();
704                } finally {
705                    db.endTransaction();
706                }
707            }
708
709            return total;
710        }
711
712        @Override
713        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
714            if (LOGD) Log.d(TAG, "onUpgrade triggered: " + oldVersion);
715
716            int version = oldVersion;
717            if (version < 3) {
718                // upgrade 1,2 -> 3 added appWidgetId column
719                db.beginTransaction();
720                try {
721                    // Insert new column for holding appWidgetIds
722                    db.execSQL("ALTER TABLE favorites " +
723                        "ADD COLUMN appWidgetId INTEGER NOT NULL DEFAULT -1;");
724                    db.setTransactionSuccessful();
725                    version = 3;
726                } catch (SQLException ex) {
727                    // Old version remains, which means we wipe old data
728                    Log.e(TAG, ex.getMessage(), ex);
729                } finally {
730                    db.endTransaction();
731                }
732
733                // Convert existing widgets only if table upgrade was successful
734                if (version == 3) {
735                    convertWidgets(db);
736                }
737            }
738
739            if (version < 4) {
740                version = 4;
741            }
742
743            // Where's version 5?
744            // - Donut and sholes on 2.0 shipped with version 4 of launcher1.
745            // - Passion shipped on 2.1 with version 6 of launcher3
746            // - Sholes shipped on 2.1r1 (aka Mr. 3) with version 5 of launcher 1
747            //   but version 5 on there was the updateContactsShortcuts change
748            //   which was version 6 in launcher 2 (first shipped on passion 2.1r1).
749            // The updateContactsShortcuts change is idempotent, so running it twice
750            // is okay so we'll do that when upgrading the devices that shipped with it.
751            if (version < 6) {
752                // We went from 3 to 5 screens. Move everything 1 to the right
753                db.beginTransaction();
754                try {
755                    db.execSQL("UPDATE favorites SET screen=(screen + 1);");
756                    db.setTransactionSuccessful();
757                } catch (SQLException ex) {
758                    // Old version remains, which means we wipe old data
759                    Log.e(TAG, ex.getMessage(), ex);
760                } finally {
761                    db.endTransaction();
762                }
763
764               // We added the fast track.
765                if (updateContactsShortcuts(db)) {
766                    version = 6;
767                }
768            }
769
770            if (version < 7) {
771                // Version 7 gets rid of the special search widget.
772                convertWidgets(db);
773                version = 7;
774            }
775
776            if (version < 8) {
777                // Version 8 (froyo) has the icons all normalized.  This should
778                // already be the case in practice, but we now rely on it and don't
779                // resample the images each time.
780                normalizeIcons(db);
781                version = 8;
782            }
783
784            if (version < 9) {
785                // The max id is not yet set at this point (onUpgrade is triggered in the ctor
786                // before it gets a change to get set, so we need to read it here when we use it)
787                if (mMaxItemId == -1) {
788                    mMaxItemId = initializeMaxItemId(db);
789                }
790
791                // Add default hotseat icons
792                loadFavorites(db, new SimpleWorkspaceLoader(this, mContext.getResources(),
793                        R.xml.update_workspace));
794                version = 9;
795            }
796
797            // We bumped the version three time during JB, once to update the launch flags, once to
798            // update the override for the default launch animation and once to set the mimetype
799            // to improve startup performance
800            if (version < 12) {
801                // Contact shortcuts need a different set of flags to be launched now
802                // The updateContactsShortcuts change is idempotent, so we can keep using it like
803                // back in the Donut days
804                updateContactsShortcuts(db);
805                version = 12;
806            }
807
808            if (version < 13) {
809                // With the new shrink-wrapped and re-orderable workspaces, it makes sense
810                // to persist workspace screens and their relative order.
811                mMaxScreenId = 0;
812
813                // This will never happen in the wild, but when we switch to using workspace
814                // screen ids, redo the import from old launcher.
815                sJustLoadedFromOldDb = true;
816
817                addWorkspacesTable(db);
818                version = 13;
819            }
820
821            if (version < 14) {
822                db.beginTransaction();
823                try {
824                    // Insert new column for holding widget provider name
825                    db.execSQL("ALTER TABLE favorites " +
826                            "ADD COLUMN appWidgetProvider TEXT;");
827                    db.setTransactionSuccessful();
828                    version = 14;
829                } catch (SQLException ex) {
830                    // Old version remains, which means we wipe old data
831                    Log.e(TAG, ex.getMessage(), ex);
832                } finally {
833                    db.endTransaction();
834                }
835            }
836
837            if (version < 15) {
838                db.beginTransaction();
839                try {
840                    // Insert new column for holding update timestamp
841                    db.execSQL("ALTER TABLE favorites " +
842                            "ADD COLUMN modified INTEGER NOT NULL DEFAULT 0;");
843                    db.execSQL("ALTER TABLE workspaceScreens " +
844                            "ADD COLUMN modified INTEGER NOT NULL DEFAULT 0;");
845                    db.setTransactionSuccessful();
846                    version = 15;
847                } catch (SQLException ex) {
848                    // Old version remains, which means we wipe old data
849                    Log.e(TAG, ex.getMessage(), ex);
850                } finally {
851                    db.endTransaction();
852                }
853            }
854
855
856            if (version < 16) {
857                db.beginTransaction();
858                try {
859                    // Insert new column for holding restore status
860                    db.execSQL("ALTER TABLE favorites " +
861                            "ADD COLUMN restored INTEGER NOT NULL DEFAULT 0;");
862                    db.setTransactionSuccessful();
863                    version = 16;
864                } catch (SQLException ex) {
865                    // Old version remains, which means we wipe old data
866                    Log.e(TAG, ex.getMessage(), ex);
867                } finally {
868                    db.endTransaction();
869                }
870            }
871
872            if (version < 17) {
873                // We use the db version upgrade here to identify users who may not have seen
874                // clings yet (because they weren't available), but for whom the clings are now
875                // available (tablet users). Because one of the possible cling flows (migration)
876                // is very destructive (wipes out workspaces), we want to prevent this from showing
877                // until clear data. We do so by marking that the clings have been shown.
878                LauncherClings.synchonouslyMarkFirstRunClingDismissed(mContext);
879                version = 17;
880            }
881
882            if (version < 18) {
883                // No-op
884                version = 18;
885            }
886
887            if (version < 19) {
888                // Due to a data loss bug, some users may have items associated with screen ids
889                // which no longer exist. Since this can cause other problems, and since the user
890                // will never see these items anyway, we use database upgrade as an opportunity to
891                // clean things up.
892                removeOrphanedItems(db);
893                version = 19;
894            }
895
896            if (version < 20) {
897                // Add userId column
898                if (addProfileColumn(db)) {
899                    version = 20;
900                }
901                // else old version remains, which means we wipe old data
902            }
903
904            if (version != DATABASE_VERSION) {
905                Log.w(TAG, "Destroying all old data.");
906                db.execSQL("DROP TABLE IF EXISTS " + TABLE_FAVORITES);
907                db.execSQL("DROP TABLE IF EXISTS " + TABLE_WORKSPACE_SCREENS);
908
909                onCreate(db);
910            }
911        }
912
913        @Override
914        public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
915            // This shouldn't happen -- throw our hands up in the air and start over.
916            Log.w(TAG, "Database version downgrade from: " + oldVersion + " to " + newVersion +
917                    ". Wiping databse.");
918            createEmptyDB(db);
919        }
920
921
922        /**
923         * Clears all the data for a fresh start.
924         */
925        public void createEmptyDB(SQLiteDatabase db) {
926            db.execSQL("DROP TABLE IF EXISTS " + TABLE_FAVORITES);
927            db.execSQL("DROP TABLE IF EXISTS " + TABLE_WORKSPACE_SCREENS);
928            onCreate(db);
929        }
930
931        private boolean addProfileColumn(SQLiteDatabase db) {
932            db.beginTransaction();
933            try {
934                UserManagerCompat userManager = UserManagerCompat.getInstance(mContext);
935                // Default to the serial number of this user, for older
936                // shortcuts.
937                long userSerialNumber = userManager.getSerialNumberForUser(
938                        UserHandleCompat.myUserHandle());
939                // Insert new column for holding user serial number
940                db.execSQL("ALTER TABLE favorites " +
941                        "ADD COLUMN profileId INTEGER DEFAULT "
942                                        + userSerialNumber + ";");
943                db.setTransactionSuccessful();
944            } catch (SQLException ex) {
945                // Old version remains, which means we wipe old data
946                Log.e(TAG, ex.getMessage(), ex);
947                return false;
948            } finally {
949                db.endTransaction();
950            }
951            return true;
952        }
953
954        private boolean updateContactsShortcuts(SQLiteDatabase db) {
955            final String selectWhere = buildOrWhereString(Favorites.ITEM_TYPE,
956                    new int[] { Favorites.ITEM_TYPE_SHORTCUT });
957
958            Cursor c = null;
959            final String actionQuickContact = "com.android.contacts.action.QUICK_CONTACT";
960            db.beginTransaction();
961            try {
962                // Select and iterate through each matching widget
963                c = db.query(TABLE_FAVORITES,
964                        new String[] { Favorites._ID, Favorites.INTENT },
965                        selectWhere, null, null, null, null);
966                if (c == null) return false;
967
968                if (LOGD) Log.d(TAG, "found upgrade cursor count=" + c.getCount());
969
970                final int idIndex = c.getColumnIndex(Favorites._ID);
971                final int intentIndex = c.getColumnIndex(Favorites.INTENT);
972
973                while (c.moveToNext()) {
974                    long favoriteId = c.getLong(idIndex);
975                    final String intentUri = c.getString(intentIndex);
976                    if (intentUri != null) {
977                        try {
978                            final Intent intent = Intent.parseUri(intentUri, 0);
979                            android.util.Log.d("Home", intent.toString());
980                            final Uri uri = intent.getData();
981                            if (uri != null) {
982                                final String data = uri.toString();
983                                if ((Intent.ACTION_VIEW.equals(intent.getAction()) ||
984                                        actionQuickContact.equals(intent.getAction())) &&
985                                        (data.startsWith("content://contacts/people/") ||
986                                        data.startsWith("content://com.android.contacts/" +
987                                                "contacts/lookup/"))) {
988
989                                    final Intent newIntent = new Intent(actionQuickContact);
990                                    // When starting from the launcher, start in a new, cleared task
991                                    // CLEAR_WHEN_TASK_RESET cannot reset the root of a task, so we
992                                    // clear the whole thing preemptively here since
993                                    // QuickContactActivity will finish itself when launching other
994                                    // detail activities.
995                                    newIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
996                                            Intent.FLAG_ACTIVITY_CLEAR_TASK);
997                                    newIntent.putExtra(
998                                            Launcher.INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION, true);
999                                    newIntent.setData(uri);
1000                                    // Determine the type and also put that in the shortcut
1001                                    // (that can speed up launch a bit)
1002                                    newIntent.setDataAndType(uri, newIntent.resolveType(mContext));
1003
1004                                    final ContentValues values = new ContentValues();
1005                                    values.put(LauncherSettings.Favorites.INTENT,
1006                                            newIntent.toUri(0));
1007
1008                                    String updateWhere = Favorites._ID + "=" + favoriteId;
1009                                    db.update(TABLE_FAVORITES, values, updateWhere, null);
1010                                }
1011                            }
1012                        } catch (RuntimeException ex) {
1013                            Log.e(TAG, "Problem upgrading shortcut", ex);
1014                        } catch (URISyntaxException e) {
1015                            Log.e(TAG, "Problem upgrading shortcut", e);
1016                        }
1017                    }
1018                }
1019
1020                db.setTransactionSuccessful();
1021            } catch (SQLException ex) {
1022                Log.w(TAG, "Problem while upgrading contacts", ex);
1023                return false;
1024            } finally {
1025                db.endTransaction();
1026                if (c != null) {
1027                    c.close();
1028                }
1029            }
1030
1031            return true;
1032        }
1033
1034        private void normalizeIcons(SQLiteDatabase db) {
1035            Log.d(TAG, "normalizing icons");
1036
1037            db.beginTransaction();
1038            Cursor c = null;
1039            SQLiteStatement update = null;
1040            try {
1041                boolean logged = false;
1042                update = db.compileStatement("UPDATE favorites "
1043                        + "SET icon=? WHERE _id=?");
1044
1045                c = db.rawQuery("SELECT _id, icon FROM favorites WHERE iconType=" +
1046                        Favorites.ICON_TYPE_BITMAP, null);
1047
1048                final int idIndex = c.getColumnIndexOrThrow(Favorites._ID);
1049                final int iconIndex = c.getColumnIndexOrThrow(Favorites.ICON);
1050
1051                while (c.moveToNext()) {
1052                    long id = c.getLong(idIndex);
1053                    byte[] data = c.getBlob(iconIndex);
1054                    try {
1055                        Bitmap bitmap = Utilities.resampleIconBitmap(
1056                                BitmapFactory.decodeByteArray(data, 0, data.length),
1057                                mContext);
1058                        if (bitmap != null) {
1059                            update.bindLong(1, id);
1060                            data = ItemInfo.flattenBitmap(bitmap);
1061                            if (data != null) {
1062                                update.bindBlob(2, data);
1063                                update.execute();
1064                            }
1065                            bitmap.recycle();
1066                        }
1067                    } catch (Exception e) {
1068                        if (!logged) {
1069                            Log.e(TAG, "Failed normalizing icon " + id, e);
1070                        } else {
1071                            Log.e(TAG, "Also failed normalizing icon " + id);
1072                        }
1073                        logged = true;
1074                    }
1075                }
1076                db.setTransactionSuccessful();
1077            } catch (SQLException ex) {
1078                Log.w(TAG, "Problem while allocating appWidgetIds for existing widgets", ex);
1079            } finally {
1080                db.endTransaction();
1081                if (update != null) {
1082                    update.close();
1083                }
1084                if (c != null) {
1085                    c.close();
1086                }
1087            }
1088        }
1089
1090        // Generates a new ID to use for an object in your database. This method should be only
1091        // called from the main UI thread. As an exception, we do call it when we call the
1092        // constructor from the worker thread; however, this doesn't extend until after the
1093        // constructor is called, and we only pass a reference to LauncherProvider to LauncherApp
1094        // after that point
1095        @Override
1096        public long generateNewItemId() {
1097            if (mMaxItemId < 0) {
1098                throw new RuntimeException("Error: max item id was not initialized");
1099            }
1100            mMaxItemId += 1;
1101            return mMaxItemId;
1102        }
1103
1104        @Override
1105        public long insertAndCheck(SQLiteDatabase db, ContentValues values) {
1106            return dbInsertAndCheck(this, db, TABLE_FAVORITES, null, values);
1107        }
1108
1109        public void updateMaxItemId(long id) {
1110            mMaxItemId = id + 1;
1111        }
1112
1113        public void checkId(String table, ContentValues values) {
1114            long id = values.getAsLong(LauncherSettings.BaseLauncherColumns._ID);
1115            if (table == LauncherProvider.TABLE_WORKSPACE_SCREENS) {
1116                mMaxScreenId = Math.max(id, mMaxScreenId);
1117            }  else {
1118                mMaxItemId = Math.max(id, mMaxItemId);
1119            }
1120        }
1121
1122        private long initializeMaxItemId(SQLiteDatabase db) {
1123            Cursor c = db.rawQuery("SELECT MAX(_id) FROM favorites", null);
1124
1125            // get the result
1126            final int maxIdIndex = 0;
1127            long id = -1;
1128            if (c != null && c.moveToNext()) {
1129                id = c.getLong(maxIdIndex);
1130            }
1131            if (c != null) {
1132                c.close();
1133            }
1134
1135            if (id == -1) {
1136                throw new RuntimeException("Error: could not query max item id");
1137            }
1138
1139            return id;
1140        }
1141
1142        // Generates a new ID to use for an workspace screen in your database. This method
1143        // should be only called from the main UI thread. As an exception, we do call it when we
1144        // call the constructor from the worker thread; however, this doesn't extend until after the
1145        // constructor is called, and we only pass a reference to LauncherProvider to LauncherApp
1146        // after that point
1147        public long generateNewScreenId() {
1148            if (mMaxScreenId < 0) {
1149                throw new RuntimeException("Error: max screen id was not initialized");
1150            }
1151            mMaxScreenId += 1;
1152            // Log to disk
1153            Launcher.addDumpLog(TAG, "11683562 - generateNewScreenId(): " + mMaxScreenId, true);
1154            return mMaxScreenId;
1155        }
1156
1157        public void updateMaxScreenId(long maxScreenId) {
1158            // Log to disk
1159            Launcher.addDumpLog(TAG, "11683562 - updateMaxScreenId(): " + maxScreenId, true);
1160            mMaxScreenId = maxScreenId;
1161        }
1162
1163        private long initializeMaxScreenId(SQLiteDatabase db) {
1164            Cursor c = db.rawQuery("SELECT MAX(" + LauncherSettings.WorkspaceScreens._ID + ") FROM " + TABLE_WORKSPACE_SCREENS, null);
1165
1166            // get the result
1167            final int maxIdIndex = 0;
1168            long id = -1;
1169            if (c != null && c.moveToNext()) {
1170                id = c.getLong(maxIdIndex);
1171            }
1172            if (c != null) {
1173                c.close();
1174            }
1175
1176            if (id == -1) {
1177                throw new RuntimeException("Error: could not query max screen id");
1178            }
1179
1180            // Log to disk
1181            Launcher.addDumpLog(TAG, "11683562 - initializeMaxScreenId(): " + id, true);
1182            return id;
1183        }
1184
1185        /**
1186         * Upgrade existing clock and photo frame widgets into their new widget
1187         * equivalents.
1188         */
1189        private void convertWidgets(SQLiteDatabase db) {
1190            final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext);
1191            final int[] bindSources = new int[] {
1192                    Favorites.ITEM_TYPE_WIDGET_CLOCK,
1193                    Favorites.ITEM_TYPE_WIDGET_PHOTO_FRAME,
1194                    Favorites.ITEM_TYPE_WIDGET_SEARCH,
1195            };
1196
1197            final String selectWhere = buildOrWhereString(Favorites.ITEM_TYPE, bindSources);
1198
1199            Cursor c = null;
1200
1201            db.beginTransaction();
1202            try {
1203                // Select and iterate through each matching widget
1204                c = db.query(TABLE_FAVORITES, new String[] { Favorites._ID, Favorites.ITEM_TYPE },
1205                        selectWhere, null, null, null, null);
1206
1207                if (LOGD) Log.d(TAG, "found upgrade cursor count=" + c.getCount());
1208
1209                final ContentValues values = new ContentValues();
1210                while (c != null && c.moveToNext()) {
1211                    long favoriteId = c.getLong(0);
1212                    int favoriteType = c.getInt(1);
1213
1214                    // Allocate and update database with new appWidgetId
1215                    try {
1216                        int appWidgetId = mAppWidgetHost.allocateAppWidgetId();
1217
1218                        if (LOGD) {
1219                            Log.d(TAG, "allocated appWidgetId=" + appWidgetId
1220                                    + " for favoriteId=" + favoriteId);
1221                        }
1222                        values.clear();
1223                        values.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_APPWIDGET);
1224                        values.put(Favorites.APPWIDGET_ID, appWidgetId);
1225
1226                        // Original widgets might not have valid spans when upgrading
1227                        if (favoriteType == Favorites.ITEM_TYPE_WIDGET_SEARCH) {
1228                            values.put(LauncherSettings.Favorites.SPANX, 4);
1229                            values.put(LauncherSettings.Favorites.SPANY, 1);
1230                        } else {
1231                            values.put(LauncherSettings.Favorites.SPANX, 2);
1232                            values.put(LauncherSettings.Favorites.SPANY, 2);
1233                        }
1234
1235                        String updateWhere = Favorites._ID + "=" + favoriteId;
1236                        db.update(TABLE_FAVORITES, values, updateWhere, null);
1237
1238                        if (favoriteType == Favorites.ITEM_TYPE_WIDGET_CLOCK) {
1239                            // TODO: check return value
1240                            appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId,
1241                                    new ComponentName("com.android.alarmclock",
1242                                    "com.android.alarmclock.AnalogAppWidgetProvider"));
1243                        } else if (favoriteType == Favorites.ITEM_TYPE_WIDGET_PHOTO_FRAME) {
1244                            // TODO: check return value
1245                            appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId,
1246                                    new ComponentName("com.android.camera",
1247                                    "com.android.camera.PhotoAppWidgetProvider"));
1248                        } else if (favoriteType == Favorites.ITEM_TYPE_WIDGET_SEARCH) {
1249                            // TODO: check return value
1250                            appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId,
1251                                    getSearchWidgetProvider());
1252                        }
1253                    } catch (RuntimeException ex) {
1254                        Log.e(TAG, "Problem allocating appWidgetId", ex);
1255                    }
1256                }
1257
1258                db.setTransactionSuccessful();
1259            } catch (SQLException ex) {
1260                Log.w(TAG, "Problem while allocating appWidgetIds for existing widgets", ex);
1261            } finally {
1262                db.endTransaction();
1263                if (c != null) {
1264                    c.close();
1265                }
1266            }
1267
1268            // Update max item id
1269            mMaxItemId = initializeMaxItemId(db);
1270            if (LOGD) Log.d(TAG, "mMaxItemId: " + mMaxItemId);
1271        }
1272
1273        private boolean initializeExternalAdd(ContentValues values) {
1274            // 1. Ensure that externally added items have a valid item id
1275            long id = generateNewItemId();
1276            values.put(LauncherSettings.Favorites._ID, id);
1277
1278            // 2. In the case of an app widget, and if no app widget id is specified, we
1279            // attempt allocate and bind the widget.
1280            Integer itemType = values.getAsInteger(LauncherSettings.Favorites.ITEM_TYPE);
1281            if (itemType != null &&
1282                    itemType.intValue() == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET &&
1283                    !values.containsKey(LauncherSettings.Favorites.APPWIDGET_ID)) {
1284
1285                final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext);
1286                ComponentName cn = ComponentName.unflattenFromString(
1287                        values.getAsString(Favorites.APPWIDGET_PROVIDER));
1288
1289                if (cn != null) {
1290                    try {
1291                        int appWidgetId = mAppWidgetHost.allocateAppWidgetId();
1292                        values.put(LauncherSettings.Favorites.APPWIDGET_ID, appWidgetId);
1293                        if (!appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId,cn)) {
1294                            return false;
1295                        }
1296                    } catch (RuntimeException e) {
1297                        Log.e(TAG, "Failed to initialize external widget", e);
1298                        return false;
1299                    }
1300                } else {
1301                    return false;
1302                }
1303            }
1304
1305            // Add screen id if not present
1306            long screenId = values.getAsLong(LauncherSettings.Favorites.SCREEN);
1307            if (!addScreenIdIfNecessary(screenId)) {
1308                return false;
1309            }
1310            return true;
1311        }
1312
1313        // Returns true of screen id exists, or if successfully added
1314        private boolean addScreenIdIfNecessary(long screenId) {
1315            if (!hasScreenId(screenId)) {
1316                int rank = getMaxScreenRank() + 1;
1317
1318                ContentValues v = new ContentValues();
1319                v.put(LauncherSettings.WorkspaceScreens._ID, screenId);
1320                v.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, rank);
1321                if (dbInsertAndCheck(this, getWritableDatabase(),
1322                        TABLE_WORKSPACE_SCREENS, null, v) < 0) {
1323                    return false;
1324                }
1325            }
1326            return true;
1327        }
1328
1329        private boolean hasScreenId(long screenId) {
1330            SQLiteDatabase db = getWritableDatabase();
1331            Cursor c = db.rawQuery("SELECT * FROM " + TABLE_WORKSPACE_SCREENS + " WHERE "
1332                    + LauncherSettings.WorkspaceScreens._ID + " = " + screenId, null);
1333            if (c != null) {
1334                int count = c.getCount();
1335                c.close();
1336                return count > 0;
1337            } else {
1338                return false;
1339            }
1340        }
1341
1342        private int getMaxScreenRank() {
1343            SQLiteDatabase db = getWritableDatabase();
1344            Cursor c = db.rawQuery("SELECT MAX(" + LauncherSettings.WorkspaceScreens.SCREEN_RANK
1345                    + ") FROM " + TABLE_WORKSPACE_SCREENS, null);
1346
1347            // get the result
1348            final int maxRankIndex = 0;
1349            int rank = -1;
1350            if (c != null && c.moveToNext()) {
1351                rank = c.getInt(maxRankIndex);
1352            }
1353            if (c != null) {
1354                c.close();
1355            }
1356
1357            return rank;
1358        }
1359
1360        private static final void beginDocument(XmlPullParser parser, String firstElementName)
1361                throws XmlPullParserException, IOException {
1362            int type;
1363            while ((type = parser.next()) != XmlPullParser.START_TAG
1364                    && type != XmlPullParser.END_DOCUMENT) {
1365                ;
1366            }
1367
1368            if (type != XmlPullParser.START_TAG) {
1369                throw new XmlPullParserException("No start tag found");
1370            }
1371
1372            if (!parser.getName().equals(firstElementName)) {
1373                throw new XmlPullParserException("Unexpected start tag: found " + parser.getName() +
1374                        ", expected " + firstElementName);
1375            }
1376        }
1377
1378        private static Intent buildMainIntent() {
1379            Intent intent = new Intent(Intent.ACTION_MAIN, null);
1380            intent.addCategory(Intent.CATEGORY_LAUNCHER);
1381            return intent;
1382        }
1383
1384        private int loadFavorites(SQLiteDatabase db, WorkspaceLoader loader) {
1385            ArrayList<Long> screenIds = new ArrayList<Long>();
1386            // TODO: Use multiple loaders with fall-back and transaction.
1387            int count = loader.loadLayout(db, screenIds);
1388
1389            // Add the screens specified by the items above
1390            Collections.sort(screenIds);
1391            int rank = 0;
1392            ContentValues values = new ContentValues();
1393            for (Long id : screenIds) {
1394                values.clear();
1395                values.put(LauncherSettings.WorkspaceScreens._ID, id);
1396                values.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, rank);
1397                if (dbInsertAndCheck(this, db, TABLE_WORKSPACE_SCREENS, null, values) < 0) {
1398                    throw new RuntimeException("Failed initialize screen table"
1399                            + "from default layout");
1400                }
1401                rank++;
1402            }
1403
1404            // Ensure that the max ids are initialized
1405            mMaxItemId = initializeMaxItemId(db);
1406            mMaxScreenId = initializeMaxScreenId(db);
1407
1408            return count;
1409        }
1410
1411        /**
1412         * Loads the default set of favorite packages from an xml file.
1413         *
1414         * @param db The database to write the values into
1415         * @param filterContainerId The specific container id of items to load
1416         * @param the set of screenIds which are used by the favorites
1417         */
1418        private int loadFavoritesRecursive(SQLiteDatabase db, Resources res, int workspaceResourceId,
1419                ArrayList<Long> screenIds) {
1420
1421            ContentValues values = new ContentValues();
1422            if (LOGD) Log.v(TAG, String.format("Loading favorites from resid=0x%08x", workspaceResourceId));
1423
1424            int count = 0;
1425            try {
1426                XmlResourceParser parser = res.getXml(workspaceResourceId);
1427                beginDocument(parser, TAG_FAVORITES);
1428
1429                final int depth = parser.getDepth();
1430
1431                int type;
1432                while (((type = parser.next()) != XmlPullParser.END_TAG ||
1433                        parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
1434
1435                    if (type != XmlPullParser.START_TAG) {
1436                        continue;
1437                    }
1438
1439                    boolean added = false;
1440                    final String name = parser.getName();
1441
1442                    if (TAG_INCLUDE.equals(name)) {
1443
1444                        final int resId = getAttributeResourceValue(parser, ATTR_WORKSPACE, 0);
1445
1446                        if (LOGD) Log.v(TAG, String.format(("%" + (2*(depth+1)) + "s<include workspace=%08x>"),
1447                                "", resId));
1448
1449                        if (resId != 0 && resId != workspaceResourceId) {
1450                            // recursively load some more favorites, why not?
1451                            count += loadFavoritesRecursive(db, res, resId, screenIds);
1452                            added = false;
1453                        } else {
1454                            Log.w(TAG, String.format("Skipping <include workspace=0x%08x>", resId));
1455                        }
1456
1457                        if (LOGD) Log.v(TAG, String.format(("%" + (2*(depth+1)) + "s</include>"), ""));
1458                        continue;
1459                    }
1460
1461                    // Assuming it's a <favorite> at this point
1462                    long container = LauncherSettings.Favorites.CONTAINER_DESKTOP;
1463                    String strContainer = getAttributeValue(parser, ATTR_CONTAINER);
1464                    if (strContainer != null) {
1465                        container = Long.valueOf(strContainer);
1466                    }
1467
1468                    String screen = getAttributeValue(parser, ATTR_SCREEN);
1469                    String x = getAttributeValue(parser, ATTR_X);
1470                    String y = getAttributeValue(parser, ATTR_Y);
1471
1472                    values.clear();
1473                    values.put(LauncherSettings.Favorites.CONTAINER, container);
1474                    values.put(LauncherSettings.Favorites.SCREEN, screen);
1475                    values.put(LauncherSettings.Favorites.CELLX, x);
1476                    values.put(LauncherSettings.Favorites.CELLY, y);
1477
1478                    if (LOGD) {
1479                        final String title = getAttributeValue(parser, ATTR_TITLE);
1480                        final String pkg = getAttributeValue(parser, ATTR_PACKAGE_NAME);
1481                        final String something = title != null ? title : pkg;
1482                        Log.v(TAG, String.format(
1483                                ("%" + (2*(depth+1)) + "s<%s%s c=%d s=%s x=%s y=%s>"),
1484                                "", name,
1485                                (something == null ? "" : (" \"" + something + "\"")),
1486                                container, screen, x, y));
1487                    }
1488
1489                    if (TAG_FAVORITE.equals(name)) {
1490                        long id = addAppShortcut(db, values, parser);
1491                        added = id >= 0;
1492                    } else if (TAG_APPWIDGET.equals(name)) {
1493                        added = addAppWidget(parser, type, db, values);
1494                    } else if (TAG_SHORTCUT.equals(name)) {
1495                        long id = addUriShortcut(db, values, res, parser);
1496                        added = id >= 0;
1497                    } else if (TAG_RESOLVE.equals(name)) {
1498                        // This looks through the contained favorites (or meta-favorites) and
1499                        // attempts to add them as shortcuts in the fallback group's location
1500                        // until one is added successfully.
1501                        added = false;
1502                        final int groupDepth = parser.getDepth();
1503                        while ((type = parser.next()) != XmlPullParser.END_TAG ||
1504                                parser.getDepth() > groupDepth) {
1505                            if (type != XmlPullParser.START_TAG) {
1506                                continue;
1507                            }
1508                            final String fallback_item_name = parser.getName();
1509                            if (!added) {
1510                                if (TAG_FAVORITE.equals(fallback_item_name)) {
1511                                    final long id = addAppShortcut(db, values, parser);
1512                                    added = id >= 0;
1513                                } else {
1514                                    Log.e(TAG, "Fallback groups can contain only favorites, found "
1515                                            + fallback_item_name);
1516                                }
1517                            }
1518                        }
1519                    } else if (TAG_FOLDER.equals(name)) {
1520                        // Folder contents are nested in this XML file
1521                        added = loadFolder(db, values, res, parser);
1522
1523                    } else if (TAG_PARTNER_FOLDER.equals(name)) {
1524                        // Folder contents come from an external XML resource
1525                        final Partner partner = Partner.get(mPackageManager);
1526                        if (partner != null) {
1527                            final Resources partnerRes = partner.getResources();
1528                            final int resId = partnerRes.getIdentifier(Partner.RES_FOLDER,
1529                                    "xml", partner.getPackageName());
1530                            if (resId != 0) {
1531                                final XmlResourceParser partnerParser = partnerRes.getXml(resId);
1532                                beginDocument(partnerParser, TAG_FOLDER);
1533                                added = loadFolder(db, values, partnerRes, partnerParser);
1534                            }
1535                        }
1536                    }
1537                    if (added) {
1538                        long screenId = Long.parseLong(screen);
1539                        // Keep track of the set of screens which need to be added to the db.
1540                        if (!screenIds.contains(screenId) &&
1541                                container == LauncherSettings.Favorites.CONTAINER_DESKTOP) {
1542                            screenIds.add(screenId);
1543                        }
1544                        count++;
1545                    }
1546                }
1547            } catch (XmlPullParserException e) {
1548                Log.w(TAG, "Got exception parsing favorites.", e);
1549            } catch (IOException e) {
1550                Log.w(TAG, "Got exception parsing favorites.", e);
1551            } catch (RuntimeException e) {
1552                Log.w(TAG, "Got exception parsing favorites.", e);
1553            }
1554            return count;
1555        }
1556
1557        /**
1558         * Parse folder items starting at {@link XmlPullParser} location. Allow recursive
1559         * includes of items.
1560         */
1561        private void addToFolder(SQLiteDatabase db, Resources res, XmlResourceParser parser,
1562                ArrayList<Long> folderItems, long folderId) throws IOException, XmlPullParserException {
1563            int type;
1564            int folderDepth = parser.getDepth();
1565            while ((type = parser.next()) != XmlPullParser.END_TAG ||
1566                    parser.getDepth() > folderDepth) {
1567                if (type != XmlPullParser.START_TAG) {
1568                    continue;
1569                }
1570                final String tag = parser.getName();
1571
1572                final ContentValues childValues = new ContentValues();
1573                childValues.put(LauncherSettings.Favorites.CONTAINER, folderId);
1574
1575                if (LOGD) {
1576                    final String pkg = getAttributeValue(parser, ATTR_PACKAGE_NAME);
1577                    final String uri = getAttributeValue(parser, ATTR_URI);
1578                    Log.v(TAG, String.format(("%" + (2*(folderDepth+1)) + "s<%s \"%s\">"), "",
1579                            tag, uri != null ? uri : pkg));
1580                }
1581
1582                if (TAG_FAVORITE.equals(tag) && folderId >= 0) {
1583                    final long id = addAppShortcut(db, childValues, parser);
1584                    if (id >= 0) {
1585                        folderItems.add(id);
1586                    }
1587                } else if (TAG_SHORTCUT.equals(tag) && folderId >= 0) {
1588                    final long id = addUriShortcut(db, childValues, res, parser);
1589                    if (id >= 0) {
1590                        folderItems.add(id);
1591                    }
1592                } else if (TAG_INCLUDE.equals(tag) && folderId >= 0) {
1593                    addToFolder(db, res, parser, folderItems, folderId);
1594                } else {
1595                    throw new RuntimeException("Folders can contain only shortcuts");
1596                }
1597            }
1598        }
1599
1600        /**
1601         * Parse folder starting at current {@link XmlPullParser} location.
1602         */
1603        private boolean loadFolder(SQLiteDatabase db, ContentValues values, Resources res,
1604                XmlResourceParser parser) throws IOException, XmlPullParserException {
1605            final String title;
1606            final int titleResId = getAttributeResourceValue(parser, ATTR_TITLE, 0);
1607            if (titleResId != 0) {
1608                title = res.getString(titleResId);
1609            } else {
1610                title = mContext.getResources().getString(R.string.folder_name);
1611            }
1612
1613            values.put(LauncherSettings.Favorites.TITLE, title);
1614            long folderId = addFolder(db, values);
1615            boolean added = folderId >= 0;
1616
1617            ArrayList<Long> folderItems = new ArrayList<Long>();
1618            addToFolder(db, res, parser, folderItems, folderId);
1619
1620            // We can only have folders with >= 2 items, so we need to remove the
1621            // folder and clean up if less than 2 items were included, or some
1622            // failed to add, and less than 2 were actually added
1623            if (folderItems.size() < 2 && folderId >= 0) {
1624                // Delete the folder
1625                deleteId(db, folderId);
1626
1627                // If we have a single item, promote it to where the folder
1628                // would have been.
1629                if (folderItems.size() == 1) {
1630                    final ContentValues childValues = new ContentValues();
1631                    copyInteger(values, childValues, LauncherSettings.Favorites.CONTAINER);
1632                    copyInteger(values, childValues, LauncherSettings.Favorites.SCREEN);
1633                    copyInteger(values, childValues, LauncherSettings.Favorites.CELLX);
1634                    copyInteger(values, childValues, LauncherSettings.Favorites.CELLY);
1635
1636                    final long id = folderItems.get(0);
1637                    db.update(TABLE_FAVORITES, childValues,
1638                            LauncherSettings.Favorites._ID + "=" + id, null);
1639                } else {
1640                    added = false;
1641                }
1642            }
1643            return added;
1644        }
1645
1646        // A meta shortcut attempts to resolve an intent specified as a URI in the XML, if a
1647        // logical choice for what shortcut should be used for that intent exists, then it is
1648        // added. Otherwise add nothing.
1649        private long addAppShortcutByUri(SQLiteDatabase db, ContentValues values,
1650                String intentUri) {
1651            Intent metaIntent;
1652            try {
1653                metaIntent = Intent.parseUri(intentUri, 0);
1654            } catch (URISyntaxException e) {
1655                Log.e(TAG, "Unable to add meta-favorite: " + intentUri, e);
1656                return -1;
1657            }
1658
1659            ResolveInfo resolved = mPackageManager.resolveActivity(metaIntent,
1660                    PackageManager.MATCH_DEFAULT_ONLY);
1661            final List<ResolveInfo> appList = mPackageManager.queryIntentActivities(
1662                    metaIntent, PackageManager.MATCH_DEFAULT_ONLY);
1663
1664            // Verify that the result is an app and not just the resolver dialog asking which
1665            // app to use.
1666            if (wouldLaunchResolverActivity(resolved, appList)) {
1667                // If only one of the results is a system app then choose that as the default.
1668                final ResolveInfo systemApp = getSingleSystemActivity(appList);
1669                if (systemApp == null) {
1670                    // There is no logical choice for this meta-favorite, so rather than making
1671                    // a bad choice just add nothing.
1672                    Log.w(TAG, "No preference or single system activity found for "
1673                            + metaIntent.toString());
1674                    return -1;
1675                }
1676                resolved = systemApp;
1677            }
1678            final ActivityInfo info = resolved.activityInfo;
1679            final Intent intent = mPackageManager.getLaunchIntentForPackage(info.packageName);
1680            if (intent == null) {
1681                return -1;
1682            }
1683            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
1684                    Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
1685
1686            return addAppShortcut(db, values, info.loadLabel(mPackageManager).toString(), intent);
1687        }
1688
1689        private ResolveInfo getSingleSystemActivity(List<ResolveInfo> appList) {
1690            ResolveInfo systemResolve = null;
1691            final int N = appList.size();
1692            for (int i = 0; i < N; ++i) {
1693                try {
1694                    ApplicationInfo info = mPackageManager.getApplicationInfo(
1695                            appList.get(i).activityInfo.packageName, 0);
1696                    if ((info.flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
1697                        if (systemResolve != null) {
1698                            return null;
1699                        } else {
1700                            systemResolve = appList.get(i);
1701                        }
1702                    }
1703                } catch (PackageManager.NameNotFoundException e) {
1704                    Log.w(TAG, "Unable to get info about resolve results", e);
1705                    return null;
1706                }
1707            }
1708            return systemResolve;
1709        }
1710
1711        private boolean wouldLaunchResolverActivity(ResolveInfo resolved,
1712                List<ResolveInfo> appList) {
1713            // If the list contains the above resolved activity, then it can't be
1714            // ResolverActivity itself.
1715            for (int i = 0; i < appList.size(); ++i) {
1716                ResolveInfo tmp = appList.get(i);
1717                if (tmp.activityInfo.name.equals(resolved.activityInfo.name)
1718                        && tmp.activityInfo.packageName.equals(resolved.activityInfo.packageName)) {
1719                    return false;
1720                }
1721            }
1722            return true;
1723        }
1724
1725        private long addAppShortcut(SQLiteDatabase db, ContentValues values,
1726                XmlResourceParser parser) {
1727            final String packageName = getAttributeValue(parser, ATTR_PACKAGE_NAME);
1728            final String className = getAttributeValue(parser, ATTR_CLASS_NAME);
1729            final String uri = getAttributeValue(parser, ATTR_URI);
1730
1731            if (!TextUtils.isEmpty(packageName) && !TextUtils.isEmpty(className)) {
1732                ActivityInfo info;
1733                try {
1734                    ComponentName cn;
1735                    try {
1736                        cn = new ComponentName(packageName, className);
1737                        info = mPackageManager.getActivityInfo(cn, 0);
1738                    } catch (PackageManager.NameNotFoundException nnfe) {
1739                        String[] packages = mPackageManager.currentToCanonicalPackageNames(
1740                                new String[] { packageName });
1741                        cn = new ComponentName(packages[0], className);
1742                        info = mPackageManager.getActivityInfo(cn, 0);
1743                    }
1744                    final Intent intent = buildMainIntent();
1745                    intent.setComponent(cn);
1746                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
1747                            Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
1748
1749                    return addAppShortcut(db, values, info.loadLabel(mPackageManager).toString(),
1750                            intent);
1751                } catch (PackageManager.NameNotFoundException e) {
1752                    Log.w(TAG, "Unable to add favorite: " + packageName +
1753                            "/" + className, e);
1754                }
1755                return -1;
1756            } else if (!TextUtils.isEmpty(uri)) {
1757                // If no component specified try to find a shortcut to add from the URI.
1758                return addAppShortcutByUri(db, values, uri);
1759            } else {
1760                Log.e(TAG, "Skipping invalid <favorite> with no component or uri");
1761                return -1;
1762            }
1763        }
1764
1765        private long addAppShortcut(SQLiteDatabase db, ContentValues values, String title,
1766                Intent intent) {
1767            long id = generateNewItemId();
1768            values.put(Favorites.INTENT, intent.toUri(0));
1769            values.put(Favorites.TITLE, title);
1770            values.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_APPLICATION);
1771            values.put(Favorites.SPANX, 1);
1772            values.put(Favorites.SPANY, 1);
1773            values.put(Favorites._ID, id);
1774            if (dbInsertAndCheck(this, db, TABLE_FAVORITES, null, values) < 0) {
1775                return -1;
1776            } else {
1777                return id;
1778            }
1779        }
1780
1781        private long addFolder(SQLiteDatabase db, ContentValues values) {
1782            values.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_FOLDER);
1783            values.put(Favorites.SPANX, 1);
1784            values.put(Favorites.SPANY, 1);
1785            long id = generateNewItemId();
1786            values.put(Favorites._ID, id);
1787            if (dbInsertAndCheck(this, db, TABLE_FAVORITES, null, values) <= 0) {
1788                return -1;
1789            } else {
1790                return id;
1791            }
1792        }
1793
1794        private ComponentName getSearchWidgetProvider() {
1795            SearchManager searchManager =
1796                    (SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE);
1797            ComponentName searchComponent = searchManager.getGlobalSearchActivity();
1798            if (searchComponent == null) return null;
1799            return getProviderInPackage(searchComponent.getPackageName());
1800        }
1801
1802        /**
1803         * Gets an appwidget provider from the given package. If the package contains more than
1804         * one appwidget provider, an arbitrary one is returned.
1805         */
1806        private ComponentName getProviderInPackage(String packageName) {
1807            AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext);
1808            List<AppWidgetProviderInfo> providers = appWidgetManager.getInstalledProviders();
1809            if (providers == null) return null;
1810            final int providerCount = providers.size();
1811            for (int i = 0; i < providerCount; i++) {
1812                ComponentName provider = providers.get(i).provider;
1813                if (provider != null && provider.getPackageName().equals(packageName)) {
1814                    return provider;
1815                }
1816            }
1817            return null;
1818        }
1819
1820        private boolean addAppWidget(XmlResourceParser parser, int type,
1821                SQLiteDatabase db, ContentValues values)
1822                throws XmlPullParserException, IOException {
1823
1824            String packageName = getAttributeValue(parser, ATTR_PACKAGE_NAME);
1825            String className = getAttributeValue(parser, ATTR_CLASS_NAME);
1826
1827            if (packageName == null || className == null) {
1828                return false;
1829            }
1830
1831            boolean hasPackage = true;
1832            ComponentName cn = new ComponentName(packageName, className);
1833            try {
1834                mPackageManager.getReceiverInfo(cn, 0);
1835            } catch (Exception e) {
1836                String[] packages = mPackageManager.currentToCanonicalPackageNames(
1837                        new String[] { packageName });
1838                cn = new ComponentName(packages[0], className);
1839                try {
1840                    mPackageManager.getReceiverInfo(cn, 0);
1841                } catch (Exception e1) {
1842                    System.out.println("Can't find widget provider: " + className);
1843                    hasPackage = false;
1844                }
1845            }
1846
1847            if (hasPackage) {
1848                String spanX = getAttributeValue(parser, ATTR_SPAN_X);
1849                String spanY = getAttributeValue(parser, ATTR_SPAN_Y);
1850
1851                values.put(Favorites.SPANX, spanX);
1852                values.put(Favorites.SPANY, spanY);
1853
1854                // Read the extras
1855                Bundle extras = new Bundle();
1856                int widgetDepth = parser.getDepth();
1857                while ((type = parser.next()) != XmlPullParser.END_TAG ||
1858                        parser.getDepth() > widgetDepth) {
1859                    if (type != XmlPullParser.START_TAG) {
1860                        continue;
1861                    }
1862
1863                    if (TAG_EXTRA.equals(parser.getName())) {
1864                        String key = getAttributeValue(parser, ATTR_KEY);
1865                        String value = getAttributeValue(parser, ATTR_VALUE);
1866                        if (key != null && value != null) {
1867                            extras.putString(key, value);
1868                        } else {
1869                            throw new RuntimeException("Widget extras must have a key and value");
1870                        }
1871                    } else {
1872                        throw new RuntimeException("Widgets can contain only extras");
1873                    }
1874                }
1875
1876                return addAppWidget(db, values, cn, extras);
1877            }
1878
1879            return false;
1880        }
1881
1882        private boolean addAppWidget(SQLiteDatabase db, ContentValues values, ComponentName cn,
1883               Bundle extras) {
1884            boolean allocatedAppWidgets = false;
1885            final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext);
1886
1887            try {
1888                int appWidgetId = mAppWidgetHost.allocateAppWidgetId();
1889
1890                values.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_APPWIDGET);
1891                values.put(Favorites.APPWIDGET_ID, appWidgetId);
1892                values.put(Favorites.APPWIDGET_PROVIDER, cn.flattenToString());
1893                values.put(Favorites._ID, generateNewItemId());
1894                dbInsertAndCheck(this, db, TABLE_FAVORITES, null, values);
1895
1896                allocatedAppWidgets = true;
1897
1898                // TODO: need to check return value
1899                appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId, cn);
1900
1901                // Send a broadcast to configure the widget
1902                if (extras != null && !extras.isEmpty()) {
1903                    Intent intent = new Intent(ACTION_APPWIDGET_DEFAULT_WORKSPACE_CONFIGURE);
1904                    intent.setComponent(cn);
1905                    intent.putExtras(extras);
1906                    intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
1907                    mContext.sendBroadcast(intent);
1908                }
1909            } catch (RuntimeException ex) {
1910                Log.e(TAG, "Problem allocating appWidgetId", ex);
1911            }
1912
1913            return allocatedAppWidgets;
1914        }
1915
1916        private long addUriShortcut(SQLiteDatabase db, ContentValues values, Resources res,
1917                XmlResourceParser parser) {
1918            final int iconResId = getAttributeResourceValue(parser, ATTR_ICON, 0);
1919            final int titleResId = getAttributeResourceValue(parser, ATTR_TITLE, 0);
1920
1921            Intent intent;
1922            String uri = null;
1923            try {
1924                uri = getAttributeValue(parser, ATTR_URI);
1925                intent = Intent.parseUri(uri, 0);
1926            } catch (URISyntaxException e) {
1927                Log.w(TAG, "Shortcut has malformed uri: " + uri);
1928                return -1; // Oh well
1929            }
1930
1931            if (iconResId == 0 || titleResId == 0) {
1932                Log.w(TAG, "Shortcut is missing title or icon resource ID");
1933                return -1;
1934            }
1935
1936            long id = generateNewItemId();
1937            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1938            values.put(Favorites.INTENT, intent.toUri(0));
1939            values.put(Favorites.TITLE, res.getString(titleResId));
1940            values.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_SHORTCUT);
1941            values.put(Favorites.SPANX, 1);
1942            values.put(Favorites.SPANY, 1);
1943            values.put(Favorites.ICON_TYPE, Favorites.ICON_TYPE_RESOURCE);
1944            values.put(Favorites.ICON_PACKAGE, res.getResourcePackageName(iconResId));
1945            values.put(Favorites.ICON_RESOURCE, res.getResourceName(iconResId));
1946            values.put(Favorites._ID, id);
1947
1948            if (dbInsertAndCheck(this, db, TABLE_FAVORITES, null, values) < 0) {
1949                return -1;
1950            }
1951            return id;
1952        }
1953
1954        private void migrateLauncher2Shortcuts(SQLiteDatabase db, Uri uri) {
1955            final ContentResolver resolver = mContext.getContentResolver();
1956            Cursor c = null;
1957            int count = 0;
1958            int curScreen = 0;
1959
1960            try {
1961                c = resolver.query(uri, null, null, null, "title ASC");
1962            } catch (Exception e) {
1963                // Ignore
1964            }
1965
1966            // We already have a favorites database in the old provider
1967            if (c != null) {
1968                try {
1969                    if (c.getCount() > 0) {
1970                        final int idIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID);
1971                        final int intentIndex
1972                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.INTENT);
1973                        final int titleIndex
1974                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.TITLE);
1975                        final int iconTypeIndex
1976                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_TYPE);
1977                        final int iconIndex
1978                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON);
1979                        final int iconPackageIndex
1980                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_PACKAGE);
1981                        final int iconResourceIndex
1982                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_RESOURCE);
1983                        final int containerIndex
1984                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CONTAINER);
1985                        final int itemTypeIndex
1986                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE);
1987                        final int screenIndex
1988                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SCREEN);
1989                        final int cellXIndex
1990                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLX);
1991                        final int cellYIndex
1992                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLY);
1993                        final int uriIndex
1994                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.URI);
1995                        final int displayModeIndex
1996                                = c.getColumnIndexOrThrow(LauncherSettings.Favorites.DISPLAY_MODE);
1997                        final int profileIndex
1998                                = c.getColumnIndex(LauncherSettings.Favorites.PROFILE_ID);
1999
2000                        int i = 0;
2001                        int curX = 0;
2002                        int curY = 0;
2003
2004                        final LauncherAppState app = LauncherAppState.getInstance();
2005                        final DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
2006                        final int width = (int) grid.numColumns;
2007                        final int height = (int) grid.numRows;
2008                        final int hotseatWidth = (int) grid.numHotseatIcons;
2009
2010                        final HashSet<String> seenIntents = new HashSet<String>(c.getCount());
2011
2012                        final ArrayList<ContentValues> shortcuts = new ArrayList<ContentValues>();
2013                        final ArrayList<ContentValues> folders = new ArrayList<ContentValues>();
2014                        final SparseArray<ContentValues> hotseat = new SparseArray<ContentValues>();
2015
2016                        while (c.moveToNext()) {
2017                            final int itemType = c.getInt(itemTypeIndex);
2018                            if (itemType != Favorites.ITEM_TYPE_APPLICATION
2019                                    && itemType != Favorites.ITEM_TYPE_SHORTCUT
2020                                    && itemType != Favorites.ITEM_TYPE_FOLDER) {
2021                                continue;
2022                            }
2023
2024                            final int cellX = c.getInt(cellXIndex);
2025                            final int cellY = c.getInt(cellYIndex);
2026                            final int screen = c.getInt(screenIndex);
2027                            int container = c.getInt(containerIndex);
2028                            final String intentStr = c.getString(intentIndex);
2029
2030                            UserManagerCompat userManager = UserManagerCompat.getInstance(mContext);
2031                            UserHandleCompat userHandle;
2032                            final long userSerialNumber;
2033                            if (profileIndex != -1 && !c.isNull(profileIndex)) {
2034                                userSerialNumber = c.getInt(profileIndex);
2035                                userHandle = userManager.getUserForSerialNumber(userSerialNumber);
2036                            } else {
2037                                // Default to the serial number of this user, for older
2038                                // shortcuts.
2039                                userHandle = UserHandleCompat.myUserHandle();
2040                                userSerialNumber = userManager.getSerialNumberForUser(userHandle);
2041                            }
2042                            Launcher.addDumpLog(TAG, "migrating \""
2043                                + c.getString(titleIndex) + "\" ("
2044                                + cellX + "," + cellY + "@"
2045                                + LauncherSettings.Favorites.containerToString(container)
2046                                + "/" + screen
2047                                + "): " + intentStr, true);
2048
2049                            if (itemType != Favorites.ITEM_TYPE_FOLDER) {
2050
2051                                final Intent intent;
2052                                final ComponentName cn;
2053                                try {
2054                                    intent = Intent.parseUri(intentStr, 0);
2055                                } catch (URISyntaxException e) {
2056                                    // bogus intent?
2057                                    Launcher.addDumpLog(TAG,
2058                                            "skipping invalid intent uri", true);
2059                                    continue;
2060                                }
2061
2062                                cn = intent.getComponent();
2063                                if (TextUtils.isEmpty(intentStr)) {
2064                                    // no intent? no icon
2065                                    Launcher.addDumpLog(TAG, "skipping empty intent", true);
2066                                    continue;
2067                                } else if (cn != null &&
2068                                        !LauncherModel.isValidPackageActivity(mContext, cn,
2069                                                userHandle)) {
2070                                    // component no longer exists.
2071                                    Launcher.addDumpLog(TAG, "skipping item whose component " +
2072                                            "no longer exists.", true);
2073                                    continue;
2074                                } else if (container ==
2075                                        LauncherSettings.Favorites.CONTAINER_DESKTOP) {
2076                                    // Dedupe icons directly on the workspace
2077
2078                                    // Canonicalize
2079                                    // the Play Store sets the package parameter, but Launcher
2080                                    // does not, so we clear that out to keep them the same
2081                                    intent.setPackage(null);
2082                                    final String key = intent.toUri(0);
2083                                    if (seenIntents.contains(key)) {
2084                                        Launcher.addDumpLog(TAG, "skipping duplicate", true);
2085                                        continue;
2086                                    } else {
2087                                        seenIntents.add(key);
2088                                    }
2089                                }
2090                            }
2091
2092                            ContentValues values = new ContentValues(c.getColumnCount());
2093                            values.put(LauncherSettings.Favorites._ID, c.getInt(idIndex));
2094                            values.put(LauncherSettings.Favorites.INTENT, intentStr);
2095                            values.put(LauncherSettings.Favorites.TITLE, c.getString(titleIndex));
2096                            values.put(LauncherSettings.Favorites.ICON_TYPE,
2097                                    c.getInt(iconTypeIndex));
2098                            values.put(LauncherSettings.Favorites.ICON, c.getBlob(iconIndex));
2099                            values.put(LauncherSettings.Favorites.ICON_PACKAGE,
2100                                    c.getString(iconPackageIndex));
2101                            values.put(LauncherSettings.Favorites.ICON_RESOURCE,
2102                                    c.getString(iconResourceIndex));
2103                            values.put(LauncherSettings.Favorites.ITEM_TYPE, itemType);
2104                            values.put(LauncherSettings.Favorites.APPWIDGET_ID, -1);
2105                            values.put(LauncherSettings.Favorites.URI, c.getString(uriIndex));
2106                            values.put(LauncherSettings.Favorites.DISPLAY_MODE,
2107                                    c.getInt(displayModeIndex));
2108                            values.put(LauncherSettings.Favorites.PROFILE_ID, userSerialNumber);
2109
2110                            if (container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
2111                                hotseat.put(screen, values);
2112                            }
2113
2114                            if (container != LauncherSettings.Favorites.CONTAINER_DESKTOP) {
2115                                // In a folder or in the hotseat, preserve position
2116                                values.put(LauncherSettings.Favorites.SCREEN, screen);
2117                                values.put(LauncherSettings.Favorites.CELLX, cellX);
2118                                values.put(LauncherSettings.Favorites.CELLY, cellY);
2119                            } else {
2120                                // For items contained directly on one of the workspace screen,
2121                                // we'll determine their location (screen, x, y) in a second pass.
2122                            }
2123
2124                            values.put(LauncherSettings.Favorites.CONTAINER, container);
2125
2126                            if (itemType != Favorites.ITEM_TYPE_FOLDER) {
2127                                shortcuts.add(values);
2128                            } else {
2129                                folders.add(values);
2130                            }
2131                        }
2132
2133                        // Now that we have all the hotseat icons, let's go through them left-right
2134                        // and assign valid locations for them in the new hotseat
2135                        final int N = hotseat.size();
2136                        for (int idx=0; idx<N; idx++) {
2137                            int hotseatX = hotseat.keyAt(idx);
2138                            ContentValues values = hotseat.valueAt(idx);
2139
2140                            if (hotseatX == grid.hotseatAllAppsRank) {
2141                                // let's drop this in the next available hole in the hotseat
2142                                while (++hotseatX < hotseatWidth) {
2143                                    if (hotseat.get(hotseatX) == null) {
2144                                        // found a spot! move it here
2145                                        values.put(LauncherSettings.Favorites.SCREEN,
2146                                                hotseatX);
2147                                        break;
2148                                    }
2149                                }
2150                            }
2151                            if (hotseatX >= hotseatWidth) {
2152                                // no room for you in the hotseat? it's off to the desktop with you
2153                                values.put(LauncherSettings.Favorites.CONTAINER,
2154                                           Favorites.CONTAINER_DESKTOP);
2155                            }
2156                        }
2157
2158                        final ArrayList<ContentValues> allItems = new ArrayList<ContentValues>();
2159                        // Folders first
2160                        allItems.addAll(folders);
2161                        // Then shortcuts
2162                        allItems.addAll(shortcuts);
2163
2164                        // Layout all the folders
2165                        for (ContentValues values: allItems) {
2166                            if (values.getAsInteger(LauncherSettings.Favorites.CONTAINER) !=
2167                                    LauncherSettings.Favorites.CONTAINER_DESKTOP) {
2168                                // Hotseat items and folder items have already had their
2169                                // location information set. Nothing to be done here.
2170                                continue;
2171                            }
2172                            values.put(LauncherSettings.Favorites.SCREEN, curScreen);
2173                            values.put(LauncherSettings.Favorites.CELLX, curX);
2174                            values.put(LauncherSettings.Favorites.CELLY, curY);
2175                            curX = (curX + 1) % width;
2176                            if (curX == 0) {
2177                                curY = (curY + 1);
2178                            }
2179                            // Leave the last row of icons blank on every screen
2180                            if (curY == height - 1) {
2181                                curScreen = (int) generateNewScreenId();
2182                                curY = 0;
2183                            }
2184                        }
2185
2186                        if (allItems.size() > 0) {
2187                            db.beginTransaction();
2188                            try {
2189                                for (ContentValues row: allItems) {
2190                                    if (row == null) continue;
2191                                    if (dbInsertAndCheck(this, db, TABLE_FAVORITES, null, row)
2192                                            < 0) {
2193                                        return;
2194                                    } else {
2195                                        count++;
2196                                    }
2197                                }
2198                                db.setTransactionSuccessful();
2199                            } finally {
2200                                db.endTransaction();
2201                            }
2202                        }
2203
2204                        db.beginTransaction();
2205                        try {
2206                            for (i=0; i<=curScreen; i++) {
2207                                final ContentValues values = new ContentValues();
2208                                values.put(LauncherSettings.WorkspaceScreens._ID, i);
2209                                values.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, i);
2210                                if (dbInsertAndCheck(this, db, TABLE_WORKSPACE_SCREENS, null, values)
2211                                        < 0) {
2212                                    return;
2213                                }
2214                            }
2215                            db.setTransactionSuccessful();
2216                        } finally {
2217                            db.endTransaction();
2218                        }
2219                    }
2220                } finally {
2221                    c.close();
2222                }
2223            }
2224
2225            Launcher.addDumpLog(TAG, "migrated " + count + " icons from Launcher2 into "
2226                    + (curScreen+1) + " screens", true);
2227
2228            // ensure that new screens are created to hold these icons
2229            setFlagJustLoadedOldDb();
2230
2231            // Update max IDs; very important since we just grabbed IDs from another database
2232            mMaxItemId = initializeMaxItemId(db);
2233            mMaxScreenId = initializeMaxScreenId(db);
2234            if (LOGD) Log.d(TAG, "mMaxItemId: " + mMaxItemId + " mMaxScreenId: " + mMaxScreenId);
2235        }
2236    }
2237
2238    /**
2239     * Build a query string that will match any row where the column matches
2240     * anything in the values list.
2241     */
2242    private static String buildOrWhereString(String column, int[] values) {
2243        StringBuilder selectWhere = new StringBuilder();
2244        for (int i = values.length - 1; i >= 0; i--) {
2245            selectWhere.append(column).append("=").append(values[i]);
2246            if (i > 0) {
2247                selectWhere.append(" OR ");
2248            }
2249        }
2250        return selectWhere.toString();
2251    }
2252
2253    /**
2254     * Return attribute value, attempting launcher-specific namespace first
2255     * before falling back to anonymous attribute.
2256     */
2257    private static String getAttributeValue(XmlResourceParser parser, String attribute) {
2258        String value = parser.getAttributeValue(
2259                "http://schemas.android.com/apk/res-auto/com.android.launcher3", attribute);
2260        if (value == null) {
2261            value = parser.getAttributeValue(null, attribute);
2262        }
2263        return value;
2264    }
2265
2266    /**
2267     * Return attribute resource value, attempting launcher-specific namespace
2268     * first before falling back to anonymous attribute.
2269     */
2270    private static int getAttributeResourceValue(XmlResourceParser parser, String attribute,
2271            int defaultValue) {
2272        int value = parser.getAttributeResourceValue(
2273                "http://schemas.android.com/apk/res-auto/com.android.launcher3", attribute,
2274                defaultValue);
2275        if (value == defaultValue) {
2276            value = parser.getAttributeResourceValue(null, attribute, defaultValue);
2277        }
2278        return value;
2279    }
2280
2281    private static void copyInteger(ContentValues from, ContentValues to, String key) {
2282        to.put(key, from.getAsInteger(key));
2283    }
2284
2285    static class SqlArguments {
2286        public final String table;
2287        public final String where;
2288        public final String[] args;
2289
2290        SqlArguments(Uri url, String where, String[] args) {
2291            if (url.getPathSegments().size() == 1) {
2292                this.table = url.getPathSegments().get(0);
2293                this.where = where;
2294                this.args = args;
2295            } else if (url.getPathSegments().size() != 2) {
2296                throw new IllegalArgumentException("Invalid URI: " + url);
2297            } else if (!TextUtils.isEmpty(where)) {
2298                throw new UnsupportedOperationException("WHERE clause not supported: " + url);
2299            } else {
2300                this.table = url.getPathSegments().get(0);
2301                this.where = "_id=" + ContentUris.parseId(url);
2302                this.args = null;
2303            }
2304        }
2305
2306        SqlArguments(Uri url) {
2307            if (url.getPathSegments().size() == 1) {
2308                table = url.getPathSegments().get(0);
2309                where = null;
2310                args = null;
2311            } else {
2312                throw new IllegalArgumentException("Invalid URI: " + url);
2313            }
2314        }
2315    }
2316
2317    static interface WorkspaceLoader {
2318        /**
2319         * @param screenIds A mutable list of screen its
2320         * @return the number of workspace items added.
2321         */
2322        int loadLayout(SQLiteDatabase db, ArrayList<Long> screenIds);
2323    }
2324
2325    private static class SimpleWorkspaceLoader implements WorkspaceLoader {
2326        private final Resources mRes;
2327        private final int mWorkspaceId;
2328        private final DatabaseHelper mHelper;
2329
2330        SimpleWorkspaceLoader(DatabaseHelper helper, Resources res, int workspaceId) {
2331            mHelper = helper;
2332            mRes = res;
2333            mWorkspaceId = workspaceId;
2334        }
2335
2336        @Override
2337        public int loadLayout(SQLiteDatabase db, ArrayList<Long> screenIds) {
2338            return mHelper.loadFavoritesRecursive(db, mRes, mWorkspaceId, screenIds);
2339        }
2340    }
2341}
2342