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.ContentResolver;
26import android.content.ContentUris;
27import android.content.ContentValues;
28import android.content.Context;
29import android.content.Intent;
30import android.content.SharedPreferences;
31import android.content.pm.ActivityInfo;
32import android.content.pm.PackageManager;
33import android.content.res.Resources;
34import android.content.res.TypedArray;
35import android.content.res.XmlResourceParser;
36import android.database.Cursor;
37import android.database.SQLException;
38import android.database.sqlite.SQLiteDatabase;
39import android.database.sqlite.SQLiteOpenHelper;
40import android.database.sqlite.SQLiteQueryBuilder;
41import android.database.sqlite.SQLiteStatement;
42import android.graphics.Bitmap;
43import android.graphics.BitmapFactory;
44import android.net.Uri;
45import android.os.Bundle;
46import android.provider.Settings;
47import android.text.TextUtils;
48import android.util.AttributeSet;
49import android.util.Log;
50import android.util.Xml;
51
52import com.android.launcher3.LauncherSettings.Favorites;
53import com.android.launcher3.config.ProviderConfig;
54
55import org.xmlpull.v1.XmlPullParser;
56import org.xmlpull.v1.XmlPullParserException;
57
58import java.io.IOException;
59import java.net.URISyntaxException;
60import java.util.ArrayList;
61import java.util.List;
62
63public class LauncherProvider extends ContentProvider {
64    private static final String TAG = "Launcher.LauncherProvider";
65    private static final boolean LOGD = false;
66
67    private static final String DATABASE_NAME = "launcher.db";
68
69    private static final int DATABASE_VERSION = 15;
70
71    static final String OLD_AUTHORITY = "com.android.launcher2.settings";
72    static final String AUTHORITY = ProviderConfig.AUTHORITY;
73
74    static final String TABLE_FAVORITES = "favorites";
75    static final String TABLE_WORKSPACE_SCREENS = "workspaceScreens";
76    static final String PARAMETER_NOTIFY = "notify";
77    static final String UPGRADED_FROM_OLD_DATABASE =
78            "UPGRADED_FROM_OLD_DATABASE";
79    static final String EMPTY_DATABASE_CREATED =
80            "EMPTY_DATABASE_CREATED";
81    static final String DEFAULT_WORKSPACE_RESOURCE_ID =
82            "DEFAULT_WORKSPACE_RESOURCE_ID";
83
84    private static final String ACTION_APPWIDGET_DEFAULT_WORKSPACE_CONFIGURE =
85            "com.android.launcher.action.APPWIDGET_DEFAULT_WORKSPACE_CONFIGURE";
86
87    /**
88     * {@link Uri} triggered at any registered {@link android.database.ContentObserver} when
89     * {@link AppWidgetHost#deleteHost()} is called during database creation.
90     * Use this to recall {@link AppWidgetHost#startListening()} if needed.
91     */
92    static final Uri CONTENT_APPWIDGET_RESET_URI =
93            Uri.parse("content://" + AUTHORITY + "/appWidgetReset");
94
95    private DatabaseHelper mOpenHelper;
96    private static boolean sJustLoadedFromOldDb;
97
98    @Override
99    public boolean onCreate() {
100        final Context context = getContext();
101        mOpenHelper = new DatabaseHelper(context);
102        LauncherAppState.setLauncherProvider(this);
103        return true;
104    }
105
106    @Override
107    public String getType(Uri uri) {
108        SqlArguments args = new SqlArguments(uri, null, null);
109        if (TextUtils.isEmpty(args.where)) {
110            return "vnd.android.cursor.dir/" + args.table;
111        } else {
112            return "vnd.android.cursor.item/" + args.table;
113        }
114    }
115
116    @Override
117    public Cursor query(Uri uri, String[] projection, String selection,
118            String[] selectionArgs, String sortOrder) {
119
120        SqlArguments args = new SqlArguments(uri, selection, selectionArgs);
121        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
122        qb.setTables(args.table);
123
124        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
125        Cursor result = qb.query(db, projection, args.where, args.args, null, null, sortOrder);
126        result.setNotificationUri(getContext().getContentResolver(), uri);
127
128        return result;
129    }
130
131    private static long dbInsertAndCheck(DatabaseHelper helper,
132            SQLiteDatabase db, String table, String nullColumnHack, ContentValues values) {
133        if (!values.containsKey(LauncherSettings.Favorites._ID)) {
134            throw new RuntimeException("Error: attempting to add item without specifying an id");
135        }
136        return db.insert(table, nullColumnHack, values);
137    }
138
139    private static void deleteId(SQLiteDatabase db, long id) {
140        Uri uri = LauncherSettings.Favorites.getContentUri(id, false);
141        SqlArguments args = new SqlArguments(uri, null, null);
142        db.delete(args.table, args.where, args.args);
143    }
144
145    @Override
146    public Uri insert(Uri uri, ContentValues initialValues) {
147        SqlArguments args = new SqlArguments(uri);
148
149        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
150        addModifiedTime(initialValues);
151        final long rowId = dbInsertAndCheck(mOpenHelper, db, args.table, null, initialValues);
152        if (rowId <= 0) return null;
153
154        uri = ContentUris.withAppendedId(uri, rowId);
155        sendNotify(uri);
156
157        return uri;
158    }
159
160    @Override
161    public int bulkInsert(Uri uri, ContentValues[] values) {
162        SqlArguments args = new SqlArguments(uri);
163
164        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
165        db.beginTransaction();
166        try {
167            int numValues = values.length;
168            for (int i = 0; i < numValues; i++) {
169                addModifiedTime(values[i]);
170                if (dbInsertAndCheck(mOpenHelper, db, args.table, null, values[i]) < 0) {
171                    return 0;
172                }
173            }
174            db.setTransactionSuccessful();
175        } finally {
176            db.endTransaction();
177        }
178
179        sendNotify(uri);
180        return values.length;
181    }
182
183    @Override
184    public int delete(Uri uri, String selection, String[] selectionArgs) {
185        SqlArguments args = new SqlArguments(uri, selection, selectionArgs);
186
187        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
188        int count = db.delete(args.table, args.where, args.args);
189        if (count > 0) sendNotify(uri);
190
191        return count;
192    }
193
194    @Override
195    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
196        SqlArguments args = new SqlArguments(uri, selection, selectionArgs);
197
198        addModifiedTime(values);
199        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
200        int count = db.update(args.table, values, args.where, args.args);
201        if (count > 0) sendNotify(uri);
202
203        return count;
204    }
205
206    private void sendNotify(Uri uri) {
207        String notify = uri.getQueryParameter(PARAMETER_NOTIFY);
208        if (notify == null || "true".equals(notify)) {
209            getContext().getContentResolver().notifyChange(uri, null);
210        }
211
212        // always notify the backup agent
213        LauncherBackupAgentHelper.dataChanged(getContext());
214    }
215
216    private void addModifiedTime(ContentValues values) {
217        values.put(LauncherSettings.ChangeLogColumns.MODIFIED, System.currentTimeMillis());
218    }
219
220    public long generateNewItemId() {
221        return mOpenHelper.generateNewItemId();
222    }
223
224    public void updateMaxItemId(long id) {
225        mOpenHelper.updateMaxItemId(id);
226    }
227
228    public long generateNewScreenId() {
229        return mOpenHelper.generateNewScreenId();
230    }
231
232    // This is only required one time while loading the workspace during the
233    // upgrade path, and should never be called from anywhere else.
234    public void updateMaxScreenId(long maxScreenId) {
235        mOpenHelper.updateMaxScreenId(maxScreenId);
236    }
237
238    /**
239     * @param Should we load the old db for upgrade? first run only.
240     */
241    synchronized public boolean justLoadedOldDb() {
242        String spKey = LauncherAppState.getSharedPreferencesKey();
243        SharedPreferences sp = getContext().getSharedPreferences(spKey, Context.MODE_PRIVATE);
244
245        boolean loadedOldDb = false || sJustLoadedFromOldDb;
246
247        sJustLoadedFromOldDb = false;
248        if (sp.getBoolean(UPGRADED_FROM_OLD_DATABASE, false)) {
249
250            SharedPreferences.Editor editor = sp.edit();
251            editor.remove(UPGRADED_FROM_OLD_DATABASE);
252            editor.commit();
253            loadedOldDb = true;
254        }
255        return loadedOldDb;
256    }
257
258    /**
259     * @param workspaceResId that can be 0 to use default or non-zero for specific resource
260     */
261    synchronized public void loadDefaultFavoritesIfNecessary(int origWorkspaceResId) {
262        String spKey = LauncherAppState.getSharedPreferencesKey();
263        SharedPreferences sp = getContext().getSharedPreferences(spKey, Context.MODE_PRIVATE);
264
265        if (sp.getBoolean(EMPTY_DATABASE_CREATED, false)) {
266            int workspaceResId = origWorkspaceResId;
267
268            // Use default workspace resource if none provided
269            if (workspaceResId == 0) {
270                workspaceResId = sp.getInt(DEFAULT_WORKSPACE_RESOURCE_ID, R.xml.default_workspace);
271            }
272
273            // Populate favorites table with initial favorites
274            SharedPreferences.Editor editor = sp.edit();
275            editor.remove(EMPTY_DATABASE_CREATED);
276            if (origWorkspaceResId != 0) {
277                editor.putInt(DEFAULT_WORKSPACE_RESOURCE_ID, origWorkspaceResId);
278            }
279
280            mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), workspaceResId);
281            mOpenHelper.setFlagJustLoadedOldDb();
282            editor.commit();
283        }
284    }
285
286    private static interface ContentValuesCallback {
287        public void onRow(ContentValues values);
288    }
289
290    private static class DatabaseHelper extends SQLiteOpenHelper {
291        private static final String TAG_FAVORITES = "favorites";
292        private static final String TAG_FAVORITE = "favorite";
293        private static final String TAG_CLOCK = "clock";
294        private static final String TAG_SEARCH = "search";
295        private static final String TAG_APPWIDGET = "appwidget";
296        private static final String TAG_SHORTCUT = "shortcut";
297        private static final String TAG_FOLDER = "folder";
298        private static final String TAG_EXTRA = "extra";
299        private static final String TAG_INCLUDE = "include";
300
301        private final Context mContext;
302        private final AppWidgetHost mAppWidgetHost;
303        private long mMaxItemId = -1;
304        private long mMaxScreenId = -1;
305
306        DatabaseHelper(Context context) {
307            super(context, DATABASE_NAME, null, DATABASE_VERSION);
308            mContext = context;
309            mAppWidgetHost = new AppWidgetHost(context, Launcher.APPWIDGET_HOST_ID);
310
311            // In the case where neither onCreate nor onUpgrade gets called, we read the maxId from
312            // the DB here
313            if (mMaxItemId == -1) {
314                mMaxItemId = initializeMaxItemId(getWritableDatabase());
315            }
316            if (mMaxScreenId == -1) {
317                mMaxScreenId = initializeMaxScreenId(getWritableDatabase());
318            }
319        }
320
321        /**
322         * Send notification that we've deleted the {@link AppWidgetHost},
323         * probably as part of the initial database creation. The receiver may
324         * want to re-call {@link AppWidgetHost#startListening()} to ensure
325         * callbacks are correctly set.
326         */
327        private void sendAppWidgetResetNotify() {
328            final ContentResolver resolver = mContext.getContentResolver();
329            resolver.notifyChange(CONTENT_APPWIDGET_RESET_URI, null);
330        }
331
332        @Override
333        public void onCreate(SQLiteDatabase db) {
334            if (LOGD) Log.d(TAG, "creating new launcher database");
335
336            mMaxItemId = 1;
337            mMaxScreenId = 0;
338
339            db.execSQL("CREATE TABLE favorites (" +
340                    "_id INTEGER PRIMARY KEY," +
341                    "title TEXT," +
342                    "intent TEXT," +
343                    "container INTEGER," +
344                    "screen INTEGER," +
345                    "cellX INTEGER," +
346                    "cellY INTEGER," +
347                    "spanX INTEGER," +
348                    "spanY INTEGER," +
349                    "itemType INTEGER," +
350                    "appWidgetId INTEGER NOT NULL DEFAULT -1," +
351                    "isShortcut INTEGER," +
352                    "iconType INTEGER," +
353                    "iconPackage TEXT," +
354                    "iconResource TEXT," +
355                    "icon BLOB," +
356                    "uri TEXT," +
357                    "displayMode INTEGER," +
358                    "appWidgetProvider TEXT," +
359                    "modified INTEGER NOT NULL DEFAULT 0" +
360                    ");");
361            addWorkspacesTable(db);
362
363            // Database was just created, so wipe any previous widgets
364            if (mAppWidgetHost != null) {
365                mAppWidgetHost.deleteHost();
366                sendAppWidgetResetNotify();
367            }
368
369            // Try converting the old database
370            ContentValuesCallback permuteScreensCb = new ContentValuesCallback() {
371                public void onRow(ContentValues values) {
372                    int container = values.getAsInteger(LauncherSettings.Favorites.CONTAINER);
373                    if (container == Favorites.CONTAINER_DESKTOP) {
374                        int screen = values.getAsInteger(LauncherSettings.Favorites.SCREEN);
375                        screen = (int) upgradeLauncherDb_permuteScreens(screen);
376                        values.put(LauncherSettings.Favorites.SCREEN, screen);
377                    }
378                }
379            };
380            Uri uri = Uri.parse("content://" + Settings.AUTHORITY +
381                    "/old_favorites?notify=true");
382            if (!convertDatabase(db, uri, permuteScreensCb, true)) {
383                // Try and upgrade from the Launcher2 db
384                uri = LauncherSettings.Favorites.OLD_CONTENT_URI;
385                if (!convertDatabase(db, uri, permuteScreensCb, false)) {
386                    // If we fail, then set a flag to load the default workspace
387                    setFlagEmptyDbCreated();
388                    return;
389                }
390            }
391            // Right now, in non-default workspace cases, we want to run the final
392            // upgrade code (ie. to fix workspace screen indices -> ids, etc.), so
393            // set that flag too.
394            setFlagJustLoadedOldDb();
395        }
396
397        private void addWorkspacesTable(SQLiteDatabase db) {
398            db.execSQL("CREATE TABLE " + TABLE_WORKSPACE_SCREENS + " (" +
399                    LauncherSettings.WorkspaceScreens._ID + " INTEGER," +
400                    LauncherSettings.WorkspaceScreens.SCREEN_RANK + " INTEGER," +
401                    LauncherSettings.ChangeLogColumns.MODIFIED + " INTEGER NOT NULL DEFAULT 0" +
402                    ");");
403        }
404
405        private void setFlagJustLoadedOldDb() {
406            String spKey = LauncherAppState.getSharedPreferencesKey();
407            SharedPreferences sp = mContext.getSharedPreferences(spKey, Context.MODE_PRIVATE);
408            SharedPreferences.Editor editor = sp.edit();
409            editor.putBoolean(UPGRADED_FROM_OLD_DATABASE, true);
410            editor.putBoolean(EMPTY_DATABASE_CREATED, false);
411            editor.commit();
412        }
413
414        private void setFlagEmptyDbCreated() {
415            String spKey = LauncherAppState.getSharedPreferencesKey();
416            SharedPreferences sp = mContext.getSharedPreferences(spKey, Context.MODE_PRIVATE);
417            SharedPreferences.Editor editor = sp.edit();
418            editor.putBoolean(EMPTY_DATABASE_CREATED, true);
419            editor.putBoolean(UPGRADED_FROM_OLD_DATABASE, false);
420            editor.commit();
421        }
422
423        // We rearrange the screens from the old launcher
424        // 12345 -> 34512
425        private long upgradeLauncherDb_permuteScreens(long screen) {
426            if (screen >= 2) {
427                return screen - 2;
428            } else {
429                return screen + 3;
430            }
431        }
432
433        private boolean convertDatabase(SQLiteDatabase db, Uri uri,
434                                        ContentValuesCallback cb, boolean deleteRows) {
435            if (LOGD) Log.d(TAG, "converting database from an older format, but not onUpgrade");
436            boolean converted = false;
437
438            final ContentResolver resolver = mContext.getContentResolver();
439            Cursor cursor = null;
440
441            try {
442                cursor = resolver.query(uri, null, null, null, null);
443            } catch (Exception e) {
444                // Ignore
445            }
446
447            // We already have a favorites database in the old provider
448            if (cursor != null) {
449                try {
450                     if (cursor.getCount() > 0) {
451                        converted = copyFromCursor(db, cursor, cb) > 0;
452                        if (converted && deleteRows) {
453                            resolver.delete(uri, null, null);
454                        }
455                    }
456                } finally {
457                    cursor.close();
458                }
459            }
460
461            if (converted) {
462                // Convert widgets from this import into widgets
463                if (LOGD) Log.d(TAG, "converted and now triggering widget upgrade");
464                convertWidgets(db);
465
466                // Update max item id
467                mMaxItemId = initializeMaxItemId(db);
468                if (LOGD) Log.d(TAG, "mMaxItemId: " + mMaxItemId);
469            }
470
471            return converted;
472        }
473
474        private int copyFromCursor(SQLiteDatabase db, Cursor c, ContentValuesCallback cb) {
475            final int idIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID);
476            final int intentIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.INTENT);
477            final int titleIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.TITLE);
478            final int iconTypeIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_TYPE);
479            final int iconIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON);
480            final int iconPackageIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_PACKAGE);
481            final int iconResourceIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_RESOURCE);
482            final int containerIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CONTAINER);
483            final int itemTypeIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE);
484            final int screenIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SCREEN);
485            final int cellXIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLX);
486            final int cellYIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLY);
487            final int uriIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.URI);
488            final int displayModeIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.DISPLAY_MODE);
489
490            ContentValues[] rows = new ContentValues[c.getCount()];
491            int i = 0;
492            while (c.moveToNext()) {
493                ContentValues values = new ContentValues(c.getColumnCount());
494                values.put(LauncherSettings.Favorites._ID, c.getLong(idIndex));
495                values.put(LauncherSettings.Favorites.INTENT, c.getString(intentIndex));
496                values.put(LauncherSettings.Favorites.TITLE, c.getString(titleIndex));
497                values.put(LauncherSettings.Favorites.ICON_TYPE, c.getInt(iconTypeIndex));
498                values.put(LauncherSettings.Favorites.ICON, c.getBlob(iconIndex));
499                values.put(LauncherSettings.Favorites.ICON_PACKAGE, c.getString(iconPackageIndex));
500                values.put(LauncherSettings.Favorites.ICON_RESOURCE, c.getString(iconResourceIndex));
501                values.put(LauncherSettings.Favorites.CONTAINER, c.getInt(containerIndex));
502                values.put(LauncherSettings.Favorites.ITEM_TYPE, c.getInt(itemTypeIndex));
503                values.put(LauncherSettings.Favorites.APPWIDGET_ID, -1);
504                values.put(LauncherSettings.Favorites.SCREEN, c.getInt(screenIndex));
505                values.put(LauncherSettings.Favorites.CELLX, c.getInt(cellXIndex));
506                values.put(LauncherSettings.Favorites.CELLY, c.getInt(cellYIndex));
507                values.put(LauncherSettings.Favorites.URI, c.getString(uriIndex));
508                values.put(LauncherSettings.Favorites.DISPLAY_MODE, c.getInt(displayModeIndex));
509                if (cb != null) {
510                    cb.onRow(values);
511                }
512                rows[i++] = values;
513            }
514
515            int total = 0;
516            if (i > 0) {
517                db.beginTransaction();
518                try {
519                    int numValues = rows.length;
520                    for (i = 0; i < numValues; i++) {
521                        if (dbInsertAndCheck(this, db, TABLE_FAVORITES, null, rows[i]) < 0) {
522                            return 0;
523                        } else {
524                            total++;
525                        }
526                    }
527                    db.setTransactionSuccessful();
528                } finally {
529                    db.endTransaction();
530                }
531            }
532
533            return total;
534        }
535
536        @Override
537        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
538            if (LOGD) Log.d(TAG, "onUpgrade triggered: " + oldVersion);
539
540            int version = oldVersion;
541            if (version < 3) {
542                // upgrade 1,2 -> 3 added appWidgetId column
543                db.beginTransaction();
544                try {
545                    // Insert new column for holding appWidgetIds
546                    db.execSQL("ALTER TABLE favorites " +
547                        "ADD COLUMN appWidgetId INTEGER NOT NULL DEFAULT -1;");
548                    db.setTransactionSuccessful();
549                    version = 3;
550                } catch (SQLException ex) {
551                    // Old version remains, which means we wipe old data
552                    Log.e(TAG, ex.getMessage(), ex);
553                } finally {
554                    db.endTransaction();
555                }
556
557                // Convert existing widgets only if table upgrade was successful
558                if (version == 3) {
559                    convertWidgets(db);
560                }
561            }
562
563            if (version < 4) {
564                version = 4;
565            }
566
567            // Where's version 5?
568            // - Donut and sholes on 2.0 shipped with version 4 of launcher1.
569            // - Passion shipped on 2.1 with version 6 of launcher3
570            // - Sholes shipped on 2.1r1 (aka Mr. 3) with version 5 of launcher 1
571            //   but version 5 on there was the updateContactsShortcuts change
572            //   which was version 6 in launcher 2 (first shipped on passion 2.1r1).
573            // The updateContactsShortcuts change is idempotent, so running it twice
574            // is okay so we'll do that when upgrading the devices that shipped with it.
575            if (version < 6) {
576                // We went from 3 to 5 screens. Move everything 1 to the right
577                db.beginTransaction();
578                try {
579                    db.execSQL("UPDATE favorites SET screen=(screen + 1);");
580                    db.setTransactionSuccessful();
581                } catch (SQLException ex) {
582                    // Old version remains, which means we wipe old data
583                    Log.e(TAG, ex.getMessage(), ex);
584                } finally {
585                    db.endTransaction();
586                }
587
588               // We added the fast track.
589                if (updateContactsShortcuts(db)) {
590                    version = 6;
591                }
592            }
593
594            if (version < 7) {
595                // Version 7 gets rid of the special search widget.
596                convertWidgets(db);
597                version = 7;
598            }
599
600            if (version < 8) {
601                // Version 8 (froyo) has the icons all normalized.  This should
602                // already be the case in practice, but we now rely on it and don't
603                // resample the images each time.
604                normalizeIcons(db);
605                version = 8;
606            }
607
608            if (version < 9) {
609                // The max id is not yet set at this point (onUpgrade is triggered in the ctor
610                // before it gets a change to get set, so we need to read it here when we use it)
611                if (mMaxItemId == -1) {
612                    mMaxItemId = initializeMaxItemId(db);
613                }
614
615                // Add default hotseat icons
616                loadFavorites(db, R.xml.update_workspace);
617                version = 9;
618            }
619
620            // We bumped the version three time during JB, once to update the launch flags, once to
621            // update the override for the default launch animation and once to set the mimetype
622            // to improve startup performance
623            if (version < 12) {
624                // Contact shortcuts need a different set of flags to be launched now
625                // The updateContactsShortcuts change is idempotent, so we can keep using it like
626                // back in the Donut days
627                updateContactsShortcuts(db);
628                version = 12;
629            }
630
631            if (version < 13) {
632                // With the new shrink-wrapped and re-orderable workspaces, it makes sense
633                // to persist workspace screens and their relative order.
634                mMaxScreenId = 0;
635
636                // This will never happen in the wild, but when we switch to using workspace
637                // screen ids, redo the import from old launcher.
638                sJustLoadedFromOldDb = true;
639
640                addWorkspacesTable(db);
641                version = 13;
642            }
643
644            if (version < 14) {
645                db.beginTransaction();
646                try {
647                    // Insert new column for holding widget provider name
648                    db.execSQL("ALTER TABLE favorites " +
649                            "ADD COLUMN appWidgetProvider TEXT;");
650                    db.setTransactionSuccessful();
651                    version = 14;
652                } catch (SQLException ex) {
653                    // Old version remains, which means we wipe old data
654                    Log.e(TAG, ex.getMessage(), ex);
655                } finally {
656                    db.endTransaction();
657                }
658            }
659
660
661            if (version < 15) {
662                db.beginTransaction();
663                try {
664                    // Insert new column for holding update timestamp
665                    db.execSQL("ALTER TABLE favorites " +
666                            "ADD COLUMN modified INTEGER NOT NULL DEFAULT 0;");
667                    db.execSQL("ALTER TABLE workspaceScreens " +
668                            "ADD COLUMN modified INTEGER NOT NULL DEFAULT 0;");
669                    db.setTransactionSuccessful();
670                    version = 15;
671                } catch (SQLException ex) {
672                    // Old version remains, which means we wipe old data
673                    Log.e(TAG, ex.getMessage(), ex);
674                } finally {
675                    db.endTransaction();
676                }
677            }
678
679            if (version != DATABASE_VERSION) {
680                Log.w(TAG, "Destroying all old data.");
681                db.execSQL("DROP TABLE IF EXISTS " + TABLE_FAVORITES);
682                db.execSQL("DROP TABLE IF EXISTS " + TABLE_WORKSPACE_SCREENS);
683
684                onCreate(db);
685            }
686        }
687
688        private boolean updateContactsShortcuts(SQLiteDatabase db) {
689            final String selectWhere = buildOrWhereString(Favorites.ITEM_TYPE,
690                    new int[] { Favorites.ITEM_TYPE_SHORTCUT });
691
692            Cursor c = null;
693            final String actionQuickContact = "com.android.contacts.action.QUICK_CONTACT";
694            db.beginTransaction();
695            try {
696                // Select and iterate through each matching widget
697                c = db.query(TABLE_FAVORITES,
698                        new String[] { Favorites._ID, Favorites.INTENT },
699                        selectWhere, null, null, null, null);
700                if (c == null) return false;
701
702                if (LOGD) Log.d(TAG, "found upgrade cursor count=" + c.getCount());
703
704                final int idIndex = c.getColumnIndex(Favorites._ID);
705                final int intentIndex = c.getColumnIndex(Favorites.INTENT);
706
707                while (c.moveToNext()) {
708                    long favoriteId = c.getLong(idIndex);
709                    final String intentUri = c.getString(intentIndex);
710                    if (intentUri != null) {
711                        try {
712                            final Intent intent = Intent.parseUri(intentUri, 0);
713                            android.util.Log.d("Home", intent.toString());
714                            final Uri uri = intent.getData();
715                            if (uri != null) {
716                                final String data = uri.toString();
717                                if ((Intent.ACTION_VIEW.equals(intent.getAction()) ||
718                                        actionQuickContact.equals(intent.getAction())) &&
719                                        (data.startsWith("content://contacts/people/") ||
720                                        data.startsWith("content://com.android.contacts/" +
721                                                "contacts/lookup/"))) {
722
723                                    final Intent newIntent = new Intent(actionQuickContact);
724                                    // When starting from the launcher, start in a new, cleared task
725                                    // CLEAR_WHEN_TASK_RESET cannot reset the root of a task, so we
726                                    // clear the whole thing preemptively here since
727                                    // QuickContactActivity will finish itself when launching other
728                                    // detail activities.
729                                    newIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
730                                            Intent.FLAG_ACTIVITY_CLEAR_TASK);
731                                    newIntent.putExtra(
732                                            Launcher.INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION, true);
733                                    newIntent.setData(uri);
734                                    // Determine the type and also put that in the shortcut
735                                    // (that can speed up launch a bit)
736                                    newIntent.setDataAndType(uri, newIntent.resolveType(mContext));
737
738                                    final ContentValues values = new ContentValues();
739                                    values.put(LauncherSettings.Favorites.INTENT,
740                                            newIntent.toUri(0));
741
742                                    String updateWhere = Favorites._ID + "=" + favoriteId;
743                                    db.update(TABLE_FAVORITES, values, updateWhere, null);
744                                }
745                            }
746                        } catch (RuntimeException ex) {
747                            Log.e(TAG, "Problem upgrading shortcut", ex);
748                        } catch (URISyntaxException e) {
749                            Log.e(TAG, "Problem upgrading shortcut", e);
750                        }
751                    }
752                }
753
754                db.setTransactionSuccessful();
755            } catch (SQLException ex) {
756                Log.w(TAG, "Problem while upgrading contacts", ex);
757                return false;
758            } finally {
759                db.endTransaction();
760                if (c != null) {
761                    c.close();
762                }
763            }
764
765            return true;
766        }
767
768        private void normalizeIcons(SQLiteDatabase db) {
769            Log.d(TAG, "normalizing icons");
770
771            db.beginTransaction();
772            Cursor c = null;
773            SQLiteStatement update = null;
774            try {
775                boolean logged = false;
776                update = db.compileStatement("UPDATE favorites "
777                        + "SET icon=? WHERE _id=?");
778
779                c = db.rawQuery("SELECT _id, icon FROM favorites WHERE iconType=" +
780                        Favorites.ICON_TYPE_BITMAP, null);
781
782                final int idIndex = c.getColumnIndexOrThrow(Favorites._ID);
783                final int iconIndex = c.getColumnIndexOrThrow(Favorites.ICON);
784
785                while (c.moveToNext()) {
786                    long id = c.getLong(idIndex);
787                    byte[] data = c.getBlob(iconIndex);
788                    try {
789                        Bitmap bitmap = Utilities.resampleIconBitmap(
790                                BitmapFactory.decodeByteArray(data, 0, data.length),
791                                mContext);
792                        if (bitmap != null) {
793                            update.bindLong(1, id);
794                            data = ItemInfo.flattenBitmap(bitmap);
795                            if (data != null) {
796                                update.bindBlob(2, data);
797                                update.execute();
798                            }
799                            bitmap.recycle();
800                        }
801                    } catch (Exception e) {
802                        if (!logged) {
803                            Log.e(TAG, "Failed normalizing icon " + id, e);
804                        } else {
805                            Log.e(TAG, "Also failed normalizing icon " + id);
806                        }
807                        logged = true;
808                    }
809                }
810                db.setTransactionSuccessful();
811            } catch (SQLException ex) {
812                Log.w(TAG, "Problem while allocating appWidgetIds for existing widgets", ex);
813            } finally {
814                db.endTransaction();
815                if (update != null) {
816                    update.close();
817                }
818                if (c != null) {
819                    c.close();
820                }
821            }
822        }
823
824        // Generates a new ID to use for an object in your database. This method should be only
825        // called from the main UI thread. As an exception, we do call it when we call the
826        // constructor from the worker thread; however, this doesn't extend until after the
827        // constructor is called, and we only pass a reference to LauncherProvider to LauncherApp
828        // after that point
829        public long generateNewItemId() {
830            if (mMaxItemId < 0) {
831                throw new RuntimeException("Error: max item id was not initialized");
832            }
833            mMaxItemId += 1;
834            return mMaxItemId;
835        }
836
837        public void updateMaxItemId(long id) {
838            mMaxItemId = id + 1;
839        }
840
841        private long initializeMaxItemId(SQLiteDatabase db) {
842            Cursor c = db.rawQuery("SELECT MAX(_id) FROM favorites", null);
843
844            // get the result
845            final int maxIdIndex = 0;
846            long id = -1;
847            if (c != null && c.moveToNext()) {
848                id = c.getLong(maxIdIndex);
849            }
850            if (c != null) {
851                c.close();
852            }
853
854            if (id == -1) {
855                throw new RuntimeException("Error: could not query max item id");
856            }
857
858            return id;
859        }
860
861        // Generates a new ID to use for an workspace screen in your database. This method
862        // should be only called from the main UI thread. As an exception, we do call it when we
863        // call the constructor from the worker thread; however, this doesn't extend until after the
864        // constructor is called, and we only pass a reference to LauncherProvider to LauncherApp
865        // after that point
866        public long generateNewScreenId() {
867            if (mMaxScreenId < 0) {
868                throw new RuntimeException("Error: max screen id was not initialized");
869            }
870            mMaxScreenId += 1;
871            return mMaxScreenId;
872        }
873
874        public void updateMaxScreenId(long maxScreenId) {
875            mMaxScreenId = maxScreenId;
876        }
877
878        private long initializeMaxScreenId(SQLiteDatabase db) {
879            Cursor c = db.rawQuery("SELECT MAX(" + LauncherSettings.WorkspaceScreens._ID + ") FROM " + TABLE_WORKSPACE_SCREENS, null);
880
881            // get the result
882            final int maxIdIndex = 0;
883            long id = -1;
884            if (c != null && c.moveToNext()) {
885                id = c.getLong(maxIdIndex);
886            }
887            if (c != null) {
888                c.close();
889            }
890
891            if (id == -1) {
892                throw new RuntimeException("Error: could not query max screen id");
893            }
894
895            return id;
896        }
897
898        /**
899         * Upgrade existing clock and photo frame widgets into their new widget
900         * equivalents.
901         */
902        private void convertWidgets(SQLiteDatabase db) {
903            final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext);
904            final int[] bindSources = new int[] {
905                    Favorites.ITEM_TYPE_WIDGET_CLOCK,
906                    Favorites.ITEM_TYPE_WIDGET_PHOTO_FRAME,
907                    Favorites.ITEM_TYPE_WIDGET_SEARCH,
908            };
909
910            final String selectWhere = buildOrWhereString(Favorites.ITEM_TYPE, bindSources);
911
912            Cursor c = null;
913
914            db.beginTransaction();
915            try {
916                // Select and iterate through each matching widget
917                c = db.query(TABLE_FAVORITES, new String[] { Favorites._ID, Favorites.ITEM_TYPE },
918                        selectWhere, null, null, null, null);
919
920                if (LOGD) Log.d(TAG, "found upgrade cursor count=" + c.getCount());
921
922                final ContentValues values = new ContentValues();
923                while (c != null && c.moveToNext()) {
924                    long favoriteId = c.getLong(0);
925                    int favoriteType = c.getInt(1);
926
927                    // Allocate and update database with new appWidgetId
928                    try {
929                        int appWidgetId = mAppWidgetHost.allocateAppWidgetId();
930
931                        if (LOGD) {
932                            Log.d(TAG, "allocated appWidgetId=" + appWidgetId
933                                    + " for favoriteId=" + favoriteId);
934                        }
935                        values.clear();
936                        values.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_APPWIDGET);
937                        values.put(Favorites.APPWIDGET_ID, appWidgetId);
938
939                        // Original widgets might not have valid spans when upgrading
940                        if (favoriteType == Favorites.ITEM_TYPE_WIDGET_SEARCH) {
941                            values.put(LauncherSettings.Favorites.SPANX, 4);
942                            values.put(LauncherSettings.Favorites.SPANY, 1);
943                        } else {
944                            values.put(LauncherSettings.Favorites.SPANX, 2);
945                            values.put(LauncherSettings.Favorites.SPANY, 2);
946                        }
947
948                        String updateWhere = Favorites._ID + "=" + favoriteId;
949                        db.update(TABLE_FAVORITES, values, updateWhere, null);
950
951                        if (favoriteType == Favorites.ITEM_TYPE_WIDGET_CLOCK) {
952                            // TODO: check return value
953                            appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId,
954                                    new ComponentName("com.android.alarmclock",
955                                    "com.android.alarmclock.AnalogAppWidgetProvider"));
956                        } else if (favoriteType == Favorites.ITEM_TYPE_WIDGET_PHOTO_FRAME) {
957                            // TODO: check return value
958                            appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId,
959                                    new ComponentName("com.android.camera",
960                                    "com.android.camera.PhotoAppWidgetProvider"));
961                        } else if (favoriteType == Favorites.ITEM_TYPE_WIDGET_SEARCH) {
962                            // TODO: check return value
963                            appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId,
964                                    getSearchWidgetProvider());
965                        }
966                    } catch (RuntimeException ex) {
967                        Log.e(TAG, "Problem allocating appWidgetId", ex);
968                    }
969                }
970
971                db.setTransactionSuccessful();
972            } catch (SQLException ex) {
973                Log.w(TAG, "Problem while allocating appWidgetIds for existing widgets", ex);
974            } finally {
975                db.endTransaction();
976                if (c != null) {
977                    c.close();
978                }
979            }
980
981            // Update max item id
982            mMaxItemId = initializeMaxItemId(db);
983            if (LOGD) Log.d(TAG, "mMaxItemId: " + mMaxItemId);
984        }
985
986        private static final void beginDocument(XmlPullParser parser, String firstElementName)
987                throws XmlPullParserException, IOException {
988            int type;
989            while ((type = parser.next()) != XmlPullParser.START_TAG
990                    && type != XmlPullParser.END_DOCUMENT) {
991                ;
992            }
993
994            if (type != XmlPullParser.START_TAG) {
995                throw new XmlPullParserException("No start tag found");
996            }
997
998            if (!parser.getName().equals(firstElementName)) {
999                throw new XmlPullParserException("Unexpected start tag: found " + parser.getName() +
1000                        ", expected " + firstElementName);
1001            }
1002        }
1003
1004        /**
1005         * Loads the default set of favorite packages from an xml file.
1006         *
1007         * @param db The database to write the values into
1008         * @param filterContainerId The specific container id of items to load
1009         */
1010        private int loadFavorites(SQLiteDatabase db, int workspaceResourceId) {
1011            Intent intent = new Intent(Intent.ACTION_MAIN, null);
1012            intent.addCategory(Intent.CATEGORY_LAUNCHER);
1013            ContentValues values = new ContentValues();
1014
1015            if (LOGD) Log.v(TAG, String.format("Loading favorites from resid=0x%08x", workspaceResourceId));
1016
1017            PackageManager packageManager = mContext.getPackageManager();
1018            int i = 0;
1019            try {
1020                XmlResourceParser parser = mContext.getResources().getXml(workspaceResourceId);
1021                AttributeSet attrs = Xml.asAttributeSet(parser);
1022                beginDocument(parser, TAG_FAVORITES);
1023
1024                final int depth = parser.getDepth();
1025
1026                int type;
1027                while (((type = parser.next()) != XmlPullParser.END_TAG ||
1028                        parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
1029
1030                    if (type != XmlPullParser.START_TAG) {
1031                        continue;
1032                    }
1033
1034                    boolean added = false;
1035                    final String name = parser.getName();
1036
1037                    if (TAG_INCLUDE.equals(name)) {
1038                        final TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.Include);
1039
1040                        final int resId = a.getResourceId(R.styleable.Include_workspace, 0);
1041
1042                        if (LOGD) Log.v(TAG, String.format(("%" + (2*(depth+1)) + "s<include workspace=%08x>"),
1043                                "", resId));
1044
1045                        if (resId != 0 && resId != workspaceResourceId) {
1046                            // recursively load some more favorites, why not?
1047                            i += loadFavorites(db, resId);
1048                            added = false;
1049                            mMaxItemId = -1;
1050                        } else {
1051                            Log.w(TAG, String.format("Skipping <include workspace=0x%08x>", resId));
1052                        }
1053
1054                        a.recycle();
1055
1056                        if (LOGD) Log.v(TAG, String.format(("%" + (2*(depth+1)) + "s</include>"), ""));
1057                        continue;
1058                    }
1059
1060                    // Assuming it's a <favorite> at this point
1061                    TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.Favorite);
1062
1063                    long container = LauncherSettings.Favorites.CONTAINER_DESKTOP;
1064                    if (a.hasValue(R.styleable.Favorite_container)) {
1065                        container = Long.valueOf(a.getString(R.styleable.Favorite_container));
1066                    }
1067
1068                    String screen = a.getString(R.styleable.Favorite_screen);
1069                    String x = a.getString(R.styleable.Favorite_x);
1070                    String y = a.getString(R.styleable.Favorite_y);
1071
1072                    values.clear();
1073                    values.put(LauncherSettings.Favorites.CONTAINER, container);
1074                    values.put(LauncherSettings.Favorites.SCREEN, screen);
1075                    values.put(LauncherSettings.Favorites.CELLX, x);
1076                    values.put(LauncherSettings.Favorites.CELLY, y);
1077
1078                    if (LOGD) {
1079                        final String title = a.getString(R.styleable.Favorite_title);
1080                        final String pkg = a.getString(R.styleable.Favorite_packageName);
1081                        final String something = title != null ? title : pkg;
1082                        Log.v(TAG, String.format(
1083                                ("%" + (2*(depth+1)) + "s<%s%s c=%d s=%s x=%s y=%s>"),
1084                                "", name,
1085                                (something == null ? "" : (" \"" + something + "\"")),
1086                                container, screen, x, y));
1087                    }
1088
1089                    if (TAG_FAVORITE.equals(name)) {
1090                        long id = addAppShortcut(db, values, a, packageManager, intent);
1091                        added = id >= 0;
1092                    } else if (TAG_SEARCH.equals(name)) {
1093                        added = addSearchWidget(db, values);
1094                    } else if (TAG_CLOCK.equals(name)) {
1095                        added = addClockWidget(db, values);
1096                    } else if (TAG_APPWIDGET.equals(name)) {
1097                        added = addAppWidget(parser, attrs, type, db, values, a, packageManager);
1098                    } else if (TAG_SHORTCUT.equals(name)) {
1099                        long id = addUriShortcut(db, values, a);
1100                        added = id >= 0;
1101                    } else if (TAG_FOLDER.equals(name)) {
1102                        String title;
1103                        int titleResId =  a.getResourceId(R.styleable.Favorite_title, -1);
1104                        if (titleResId != -1) {
1105                            title = mContext.getResources().getString(titleResId);
1106                        } else {
1107                            title = mContext.getResources().getString(R.string.folder_name);
1108                        }
1109                        values.put(LauncherSettings.Favorites.TITLE, title);
1110                        long folderId = addFolder(db, values);
1111                        added = folderId >= 0;
1112
1113                        ArrayList<Long> folderItems = new ArrayList<Long>();
1114
1115                        int folderDepth = parser.getDepth();
1116                        while ((type = parser.next()) != XmlPullParser.END_TAG ||
1117                                parser.getDepth() > folderDepth) {
1118                            if (type != XmlPullParser.START_TAG) {
1119                                continue;
1120                            }
1121                            final String folder_item_name = parser.getName();
1122
1123                            TypedArray ar = mContext.obtainStyledAttributes(attrs,
1124                                    R.styleable.Favorite);
1125                            values.clear();
1126                            values.put(LauncherSettings.Favorites.CONTAINER, folderId);
1127
1128                            if (LOGD) {
1129                                final String pkg = ar.getString(R.styleable.Favorite_packageName);
1130                                final String uri = ar.getString(R.styleable.Favorite_uri);
1131                                Log.v(TAG, String.format(("%" + (2*(folderDepth+1)) + "s<%s \"%s\">"), "",
1132                                        folder_item_name, uri != null ? uri : pkg));
1133                            }
1134
1135                            if (TAG_FAVORITE.equals(folder_item_name) && folderId >= 0) {
1136                                long id =
1137                                    addAppShortcut(db, values, ar, packageManager, intent);
1138                                if (id >= 0) {
1139                                    folderItems.add(id);
1140                                }
1141                            } else if (TAG_SHORTCUT.equals(folder_item_name) && folderId >= 0) {
1142                                long id = addUriShortcut(db, values, ar);
1143                                if (id >= 0) {
1144                                    folderItems.add(id);
1145                                }
1146                            } else {
1147                                throw new RuntimeException("Folders can " +
1148                                        "contain only shortcuts");
1149                            }
1150                            ar.recycle();
1151                        }
1152                        // We can only have folders with >= 2 items, so we need to remove the
1153                        // folder and clean up if less than 2 items were included, or some
1154                        // failed to add, and less than 2 were actually added
1155                        if (folderItems.size() < 2 && folderId >= 0) {
1156                            // We just delete the folder and any items that made it
1157                            deleteId(db, folderId);
1158                            if (folderItems.size() > 0) {
1159                                deleteId(db, folderItems.get(0));
1160                            }
1161                            added = false;
1162                        }
1163                    }
1164                    if (added) i++;
1165                    a.recycle();
1166                }
1167            } catch (XmlPullParserException e) {
1168                Log.w(TAG, "Got exception parsing favorites.", e);
1169            } catch (IOException e) {
1170                Log.w(TAG, "Got exception parsing favorites.", e);
1171            } catch (RuntimeException e) {
1172                Log.w(TAG, "Got exception parsing favorites.", e);
1173            }
1174
1175            // Update the max item id after we have loaded the database
1176            if (mMaxItemId == -1) {
1177                mMaxItemId = initializeMaxItemId(db);
1178            }
1179
1180            return i;
1181        }
1182
1183        private long addAppShortcut(SQLiteDatabase db, ContentValues values, TypedArray a,
1184                PackageManager packageManager, Intent intent) {
1185            long id = -1;
1186            ActivityInfo info;
1187            String packageName = a.getString(R.styleable.Favorite_packageName);
1188            String className = a.getString(R.styleable.Favorite_className);
1189            try {
1190                ComponentName cn;
1191                try {
1192                    cn = new ComponentName(packageName, className);
1193                    info = packageManager.getActivityInfo(cn, 0);
1194                } catch (PackageManager.NameNotFoundException nnfe) {
1195                    String[] packages = packageManager.currentToCanonicalPackageNames(
1196                        new String[] { packageName });
1197                    cn = new ComponentName(packages[0], className);
1198                    info = packageManager.getActivityInfo(cn, 0);
1199                }
1200                id = generateNewItemId();
1201                intent.setComponent(cn);
1202                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
1203                        Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
1204                values.put(Favorites.INTENT, intent.toUri(0));
1205                values.put(Favorites.TITLE, info.loadLabel(packageManager).toString());
1206                values.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_APPLICATION);
1207                values.put(Favorites.SPANX, 1);
1208                values.put(Favorites.SPANY, 1);
1209                values.put(Favorites._ID, generateNewItemId());
1210                if (dbInsertAndCheck(this, db, TABLE_FAVORITES, null, values) < 0) {
1211                    return -1;
1212                }
1213            } catch (PackageManager.NameNotFoundException e) {
1214                Log.w(TAG, "Unable to add favorite: " + packageName +
1215                        "/" + className, e);
1216            }
1217            return id;
1218        }
1219
1220        private long addFolder(SQLiteDatabase db, ContentValues values) {
1221            values.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_FOLDER);
1222            values.put(Favorites.SPANX, 1);
1223            values.put(Favorites.SPANY, 1);
1224            long id = generateNewItemId();
1225            values.put(Favorites._ID, id);
1226            if (dbInsertAndCheck(this, db, TABLE_FAVORITES, null, values) <= 0) {
1227                return -1;
1228            } else {
1229                return id;
1230            }
1231        }
1232
1233        private ComponentName getSearchWidgetProvider() {
1234            SearchManager searchManager =
1235                    (SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE);
1236            ComponentName searchComponent = searchManager.getGlobalSearchActivity();
1237            if (searchComponent == null) return null;
1238            return getProviderInPackage(searchComponent.getPackageName());
1239        }
1240
1241        /**
1242         * Gets an appwidget provider from the given package. If the package contains more than
1243         * one appwidget provider, an arbitrary one is returned.
1244         */
1245        private ComponentName getProviderInPackage(String packageName) {
1246            AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext);
1247            List<AppWidgetProviderInfo> providers = appWidgetManager.getInstalledProviders();
1248            if (providers == null) return null;
1249            final int providerCount = providers.size();
1250            for (int i = 0; i < providerCount; i++) {
1251                ComponentName provider = providers.get(i).provider;
1252                if (provider != null && provider.getPackageName().equals(packageName)) {
1253                    return provider;
1254                }
1255            }
1256            return null;
1257        }
1258
1259        private boolean addSearchWidget(SQLiteDatabase db, ContentValues values) {
1260            ComponentName cn = getSearchWidgetProvider();
1261            return addAppWidget(db, values, cn, 4, 1, null);
1262        }
1263
1264        private boolean addClockWidget(SQLiteDatabase db, ContentValues values) {
1265            ComponentName cn = new ComponentName("com.android.alarmclock",
1266                    "com.android.alarmclock.AnalogAppWidgetProvider");
1267            return addAppWidget(db, values, cn, 2, 2, null);
1268        }
1269
1270        private boolean addAppWidget(XmlResourceParser parser, AttributeSet attrs, int type,
1271                SQLiteDatabase db, ContentValues values, TypedArray a,
1272                PackageManager packageManager) throws XmlPullParserException, IOException {
1273
1274            String packageName = a.getString(R.styleable.Favorite_packageName);
1275            String className = a.getString(R.styleable.Favorite_className);
1276
1277            if (packageName == null || className == null) {
1278                return false;
1279            }
1280
1281            boolean hasPackage = true;
1282            ComponentName cn = new ComponentName(packageName, className);
1283            try {
1284                packageManager.getReceiverInfo(cn, 0);
1285            } catch (Exception e) {
1286                String[] packages = packageManager.currentToCanonicalPackageNames(
1287                        new String[] { packageName });
1288                cn = new ComponentName(packages[0], className);
1289                try {
1290                    packageManager.getReceiverInfo(cn, 0);
1291                } catch (Exception e1) {
1292                    hasPackage = false;
1293                }
1294            }
1295
1296            if (hasPackage) {
1297                int spanX = a.getInt(R.styleable.Favorite_spanX, 0);
1298                int spanY = a.getInt(R.styleable.Favorite_spanY, 0);
1299
1300                // Read the extras
1301                Bundle extras = new Bundle();
1302                int widgetDepth = parser.getDepth();
1303                while ((type = parser.next()) != XmlPullParser.END_TAG ||
1304                        parser.getDepth() > widgetDepth) {
1305                    if (type != XmlPullParser.START_TAG) {
1306                        continue;
1307                    }
1308
1309                    TypedArray ar = mContext.obtainStyledAttributes(attrs, R.styleable.Extra);
1310                    if (TAG_EXTRA.equals(parser.getName())) {
1311                        String key = ar.getString(R.styleable.Extra_key);
1312                        String value = ar.getString(R.styleable.Extra_value);
1313                        if (key != null && value != null) {
1314                            extras.putString(key, value);
1315                        } else {
1316                            throw new RuntimeException("Widget extras must have a key and value");
1317                        }
1318                    } else {
1319                        throw new RuntimeException("Widgets can contain only extras");
1320                    }
1321                    ar.recycle();
1322                }
1323
1324                return addAppWidget(db, values, cn, spanX, spanY, extras);
1325            }
1326
1327            return false;
1328        }
1329        private boolean addAppWidget(SQLiteDatabase db, ContentValues values, ComponentName cn,
1330                int spanX, int spanY, Bundle extras) {
1331            boolean allocatedAppWidgets = false;
1332            final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext);
1333
1334            try {
1335                int appWidgetId = mAppWidgetHost.allocateAppWidgetId();
1336
1337                values.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_APPWIDGET);
1338                values.put(Favorites.SPANX, spanX);
1339                values.put(Favorites.SPANY, spanY);
1340                values.put(Favorites.APPWIDGET_ID, appWidgetId);
1341                values.put(Favorites.APPWIDGET_PROVIDER, cn.flattenToString());
1342                values.put(Favorites._ID, generateNewItemId());
1343                dbInsertAndCheck(this, db, TABLE_FAVORITES, null, values);
1344
1345                allocatedAppWidgets = true;
1346
1347                // TODO: need to check return value
1348                appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId, cn);
1349
1350                // Send a broadcast to configure the widget
1351                if (extras != null && !extras.isEmpty()) {
1352                    Intent intent = new Intent(ACTION_APPWIDGET_DEFAULT_WORKSPACE_CONFIGURE);
1353                    intent.setComponent(cn);
1354                    intent.putExtras(extras);
1355                    intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
1356                    mContext.sendBroadcast(intent);
1357                }
1358            } catch (RuntimeException ex) {
1359                Log.e(TAG, "Problem allocating appWidgetId", ex);
1360            }
1361
1362            return allocatedAppWidgets;
1363        }
1364
1365        private long addUriShortcut(SQLiteDatabase db, ContentValues values,
1366                TypedArray a) {
1367            Resources r = mContext.getResources();
1368
1369            final int iconResId = a.getResourceId(R.styleable.Favorite_icon, 0);
1370            final int titleResId = a.getResourceId(R.styleable.Favorite_title, 0);
1371
1372            Intent intent;
1373            String uri = null;
1374            try {
1375                uri = a.getString(R.styleable.Favorite_uri);
1376                intent = Intent.parseUri(uri, 0);
1377            } catch (URISyntaxException e) {
1378                Log.w(TAG, "Shortcut has malformed uri: " + uri);
1379                return -1; // Oh well
1380            }
1381
1382            if (iconResId == 0 || titleResId == 0) {
1383                Log.w(TAG, "Shortcut is missing title or icon resource ID");
1384                return -1;
1385            }
1386
1387            long id = generateNewItemId();
1388            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1389            values.put(Favorites.INTENT, intent.toUri(0));
1390            values.put(Favorites.TITLE, r.getString(titleResId));
1391            values.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_SHORTCUT);
1392            values.put(Favorites.SPANX, 1);
1393            values.put(Favorites.SPANY, 1);
1394            values.put(Favorites.ICON_TYPE, Favorites.ICON_TYPE_RESOURCE);
1395            values.put(Favorites.ICON_PACKAGE, mContext.getPackageName());
1396            values.put(Favorites.ICON_RESOURCE, r.getResourceName(iconResId));
1397            values.put(Favorites._ID, id);
1398
1399            if (dbInsertAndCheck(this, db, TABLE_FAVORITES, null, values) < 0) {
1400                return -1;
1401            }
1402            return id;
1403        }
1404    }
1405
1406    /**
1407     * Build a query string that will match any row where the column matches
1408     * anything in the values list.
1409     */
1410    static String buildOrWhereString(String column, int[] values) {
1411        StringBuilder selectWhere = new StringBuilder();
1412        for (int i = values.length - 1; i >= 0; i--) {
1413            selectWhere.append(column).append("=").append(values[i]);
1414            if (i > 0) {
1415                selectWhere.append(" OR ");
1416            }
1417        }
1418        return selectWhere.toString();
1419    }
1420
1421    static class SqlArguments {
1422        public final String table;
1423        public final String where;
1424        public final String[] args;
1425
1426        SqlArguments(Uri url, String where, String[] args) {
1427            if (url.getPathSegments().size() == 1) {
1428                this.table = url.getPathSegments().get(0);
1429                this.where = where;
1430                this.args = args;
1431            } else if (url.getPathSegments().size() != 2) {
1432                throw new IllegalArgumentException("Invalid URI: " + url);
1433            } else if (!TextUtils.isEmpty(where)) {
1434                throw new UnsupportedOperationException("WHERE clause not supported: " + url);
1435            } else {
1436                this.table = url.getPathSegments().get(0);
1437                this.where = "_id=" + ContentUris.parseId(url);
1438                this.args = null;
1439            }
1440        }
1441
1442        SqlArguments(Uri url) {
1443            if (url.getPathSegments().size() == 1) {
1444                table = url.getPathSegments().get(0);
1445                where = null;
1446                args = null;
1447            } else {
1448                throw new IllegalArgumentException("Invalid URI: " + url);
1449            }
1450        }
1451    }
1452}
1453