LauncherProvider.java revision a8c760d395e1d2a78522427738302fbca3a72453
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.launcher2;
18
19import android.app.SearchManager;
20import android.appwidget.AppWidgetHost;
21import android.appwidget.AppWidgetManager;
22import android.appwidget.AppWidgetProviderInfo;
23import android.content.ContentProvider;
24import android.content.Context;
25import android.content.ContentValues;
26import android.content.Intent;
27import android.content.ComponentName;
28import android.content.ContentUris;
29import android.content.ContentResolver;
30import android.content.res.Resources;
31import android.content.res.XmlResourceParser;
32import android.content.res.TypedArray;
33import android.content.pm.PackageManager;
34import android.content.pm.ActivityInfo;
35import android.content.SharedPreferences;
36import android.database.sqlite.SQLiteOpenHelper;
37import android.database.sqlite.SQLiteDatabase;
38import android.database.sqlite.SQLiteStatement;
39import android.database.sqlite.SQLiteQueryBuilder;
40import android.database.Cursor;
41import android.database.SQLException;
42import android.graphics.Bitmap;
43import android.graphics.BitmapFactory;
44import android.util.Log;
45import android.util.Xml;
46import android.util.AttributeSet;
47import android.net.Uri;
48import android.text.TextUtils;
49import android.provider.Settings;
50
51import java.io.IOException;
52import java.net.URISyntaxException;
53import java.util.List;
54
55import org.xmlpull.v1.XmlPullParserException;
56import org.xmlpull.v1.XmlPullParser;
57
58import com.android.internal.util.XmlUtils;
59import com.android.launcher2.LauncherSettings.Favorites;
60
61import com.android.launcher.R;
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 = 8;
70
71    static final String AUTHORITY = "com.android.launcher2.settings";
72
73    static final String TABLE_FAVORITES = "favorites";
74    static final String PARAMETER_NOTIFY = "notify";
75
76    /**
77     * {@link Uri} triggered at any registered {@link android.database.ContentObserver} when
78     * {@link AppWidgetHost#deleteHost()} is called during database creation.
79     * Use this to recall {@link AppWidgetHost#startListening()} if needed.
80     */
81    static final Uri CONTENT_APPWIDGET_RESET_URI =
82            Uri.parse("content://" + AUTHORITY + "/appWidgetReset");
83
84    private DatabaseHelper mOpenHelper;
85
86    @Override
87    public boolean onCreate() {
88        mOpenHelper = new DatabaseHelper(getContext());
89        ((LauncherApplication) getContext()).setLauncherProvider(this);
90        return true;
91    }
92
93    @Override
94    public String getType(Uri uri) {
95        SqlArguments args = new SqlArguments(uri, null, null);
96        if (TextUtils.isEmpty(args.where)) {
97            return "vnd.android.cursor.dir/" + args.table;
98        } else {
99            return "vnd.android.cursor.item/" + args.table;
100        }
101    }
102
103    @Override
104    public Cursor query(Uri uri, String[] projection, String selection,
105            String[] selectionArgs, String sortOrder) {
106
107        SqlArguments args = new SqlArguments(uri, selection, selectionArgs);
108        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
109        qb.setTables(args.table);
110
111        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
112        Cursor result = qb.query(db, projection, args.where, args.args, null, null, sortOrder);
113        result.setNotificationUri(getContext().getContentResolver(), uri);
114
115        return result;
116    }
117
118    private static long dbInsertAndCheck(DatabaseHelper helper,
119            SQLiteDatabase db, String table, String nullColumnHack, ContentValues values) {
120        if (!values.containsKey(LauncherSettings.Favorites._ID)) {
121            throw new RuntimeException("Error: attempting to add item without specifying an id");
122        }
123        return db.insert(table, nullColumnHack, values);
124    }
125
126    @Override
127    public Uri insert(Uri uri, ContentValues initialValues) {
128        SqlArguments args = new SqlArguments(uri);
129
130        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
131        final long rowId = dbInsertAndCheck(mOpenHelper, db, args.table, null, initialValues);
132        if (rowId <= 0) return null;
133
134        uri = ContentUris.withAppendedId(uri, rowId);
135        sendNotify(uri);
136
137        return uri;
138    }
139
140    @Override
141    public int bulkInsert(Uri uri, ContentValues[] values) {
142        SqlArguments args = new SqlArguments(uri);
143
144        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
145        db.beginTransaction();
146        try {
147            int numValues = values.length;
148            for (int i = 0; i < numValues; i++) {
149                if (dbInsertAndCheck(mOpenHelper, db, args.table, null, values[i]) < 0) {
150                    return 0;
151                }
152            }
153            db.setTransactionSuccessful();
154        } finally {
155            db.endTransaction();
156        }
157
158        sendNotify(uri);
159        return values.length;
160    }
161
162    @Override
163    public int delete(Uri uri, String selection, String[] selectionArgs) {
164        SqlArguments args = new SqlArguments(uri, selection, selectionArgs);
165
166        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
167        int count = db.delete(args.table, args.where, args.args);
168        if (count > 0) sendNotify(uri);
169
170        return count;
171    }
172
173    @Override
174    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
175        SqlArguments args = new SqlArguments(uri, selection, selectionArgs);
176
177        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
178        int count = db.update(args.table, values, args.where, args.args);
179        if (count > 0) sendNotify(uri);
180
181        return count;
182    }
183
184    private void sendNotify(Uri uri) {
185        String notify = uri.getQueryParameter(PARAMETER_NOTIFY);
186        if (notify == null || "true".equals(notify)) {
187            getContext().getContentResolver().notifyChange(uri, null);
188        }
189    }
190
191    public long generateNewId() {
192        return mOpenHelper.generateNewId();
193    }
194
195    private static class DatabaseHelper extends SQLiteOpenHelper {
196        private static final String TAG_FAVORITES = "favorites";
197        private static final String TAG_FAVORITE = "favorite";
198        private static final String TAG_CLOCK = "clock";
199        private static final String TAG_SEARCH = "search";
200        private static final String TAG_APPWIDGET = "appwidget";
201        private static final String TAG_SHORTCUT = "shortcut";
202
203        private final Context mContext;
204        private final AppWidgetHost mAppWidgetHost;
205        private long mMaxId = -1;
206
207        DatabaseHelper(Context context) {
208            super(context, DATABASE_NAME, null, DATABASE_VERSION);
209            mContext = context;
210            mAppWidgetHost = new AppWidgetHost(context, Launcher.APPWIDGET_HOST_ID);
211            mMaxId = initializeMaxId(getWritableDatabase());
212        }
213
214        /**
215         * Send notification that we've deleted the {@link AppWidgetHost},
216         * probably as part of the initial database creation. The receiver may
217         * want to re-call {@link AppWidgetHost#startListening()} to ensure
218         * callbacks are correctly set.
219         */
220        private void sendAppWidgetResetNotify() {
221            final ContentResolver resolver = mContext.getContentResolver();
222            resolver.notifyChange(CONTENT_APPWIDGET_RESET_URI, null);
223        }
224
225        @Override
226        public void onCreate(SQLiteDatabase db) {
227            if (LOGD) Log.d(TAG, "creating new launcher database");
228
229            mMaxId = 1;
230
231            db.execSQL("CREATE TABLE favorites (" +
232                    "_id INTEGER PRIMARY KEY," +
233                    "title TEXT," +
234                    "intent TEXT," +
235                    "container INTEGER," +
236                    "screen INTEGER," +
237                    "cellX INTEGER," +
238                    "cellY INTEGER," +
239                    "spanX INTEGER," +
240                    "spanY INTEGER," +
241                    "itemType INTEGER," +
242                    "appWidgetId INTEGER NOT NULL DEFAULT -1," +
243                    "isShortcut INTEGER," +
244                    "iconType INTEGER," +
245                    "iconPackage TEXT," +
246                    "iconResource TEXT," +
247                    "icon BLOB," +
248                    "uri TEXT," +
249                    "displayMode INTEGER" +
250                    ");");
251
252            // Database was just created, so wipe any previous widgets
253            if (mAppWidgetHost != null) {
254                mAppWidgetHost.deleteHost();
255                sendAppWidgetResetNotify();
256            }
257
258            if (!convertDatabase(db)) {
259                // Populate favorites table with initial favorites
260                loadFavorites(db);
261            }
262        }
263
264        private boolean convertDatabase(SQLiteDatabase db) {
265            if (LOGD) Log.d(TAG, "converting database from an older format, but not onUpgrade");
266            boolean converted = false;
267
268            final Uri uri = Uri.parse("content://" + Settings.AUTHORITY +
269                    "/old_favorites?notify=true");
270            final ContentResolver resolver = mContext.getContentResolver();
271            Cursor cursor = null;
272
273            try {
274                cursor = resolver.query(uri, null, null, null, null);
275            } catch (Exception e) {
276                // Ignore
277            }
278
279            // We already have a favorites database in the old provider
280            if (cursor != null && cursor.getCount() > 0) {
281                try {
282                    converted = copyFromCursor(db, cursor) > 0;
283                } finally {
284                    cursor.close();
285                }
286
287                if (converted) {
288                    resolver.delete(uri, null, null);
289                }
290            }
291
292            if (converted) {
293                // Convert widgets from this import into widgets
294                if (LOGD) Log.d(TAG, "converted and now triggering widget upgrade");
295                convertWidgets(db);
296            }
297
298            return converted;
299        }
300
301        private int copyFromCursor(SQLiteDatabase db, Cursor c) {
302            final int idIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID);
303            final int intentIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.INTENT);
304            final int titleIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.TITLE);
305            final int iconTypeIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_TYPE);
306            final int iconIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON);
307            final int iconPackageIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_PACKAGE);
308            final int iconResourceIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_RESOURCE);
309            final int containerIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CONTAINER);
310            final int itemTypeIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE);
311            final int screenIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SCREEN);
312            final int cellXIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLX);
313            final int cellYIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLY);
314            final int uriIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.URI);
315            final int displayModeIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.DISPLAY_MODE);
316
317            ContentValues[] rows = new ContentValues[c.getCount()];
318            int i = 0;
319            while (c.moveToNext()) {
320                ContentValues values = new ContentValues(c.getColumnCount());
321                values.put(LauncherSettings.Favorites._ID, c.getLong(idIndex));
322                values.put(LauncherSettings.Favorites.INTENT, c.getString(intentIndex));
323                values.put(LauncherSettings.Favorites.TITLE, c.getString(titleIndex));
324                values.put(LauncherSettings.Favorites.ICON_TYPE, c.getInt(iconTypeIndex));
325                values.put(LauncherSettings.Favorites.ICON, c.getBlob(iconIndex));
326                values.put(LauncherSettings.Favorites.ICON_PACKAGE, c.getString(iconPackageIndex));
327                values.put(LauncherSettings.Favorites.ICON_RESOURCE, c.getString(iconResourceIndex));
328                values.put(LauncherSettings.Favorites.CONTAINER, c.getInt(containerIndex));
329                values.put(LauncherSettings.Favorites.ITEM_TYPE, c.getInt(itemTypeIndex));
330                values.put(LauncherSettings.Favorites.APPWIDGET_ID, -1);
331                values.put(LauncherSettings.Favorites.SCREEN, c.getInt(screenIndex));
332                values.put(LauncherSettings.Favorites.CELLX, c.getInt(cellXIndex));
333                values.put(LauncherSettings.Favorites.CELLY, c.getInt(cellYIndex));
334                values.put(LauncherSettings.Favorites.URI, c.getString(uriIndex));
335                values.put(LauncherSettings.Favorites.DISPLAY_MODE, c.getInt(displayModeIndex));
336                rows[i++] = values;
337            }
338
339            db.beginTransaction();
340            int total = 0;
341            try {
342                int numValues = rows.length;
343                for (i = 0; i < numValues; i++) {
344                    if (dbInsertAndCheck(this, db, TABLE_FAVORITES, null, rows[i]) < 0) {
345                        return 0;
346                    } else {
347                        total++;
348                    }
349                }
350                db.setTransactionSuccessful();
351            } finally {
352                db.endTransaction();
353            }
354
355            return total;
356        }
357
358        @Override
359        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
360            if (LOGD) Log.d(TAG, "onUpgrade triggered");
361
362            int version = oldVersion;
363            if (version < 3) {
364                // upgrade 1,2 -> 3 added appWidgetId column
365                db.beginTransaction();
366                try {
367                    // Insert new column for holding appWidgetIds
368                    db.execSQL("ALTER TABLE favorites " +
369                        "ADD COLUMN appWidgetId INTEGER NOT NULL DEFAULT -1;");
370                    db.setTransactionSuccessful();
371                    version = 3;
372                } catch (SQLException ex) {
373                    // Old version remains, which means we wipe old data
374                    Log.e(TAG, ex.getMessage(), ex);
375                } finally {
376                    db.endTransaction();
377                }
378
379                // Convert existing widgets only if table upgrade was successful
380                if (version == 3) {
381                    convertWidgets(db);
382                }
383            }
384
385            if (version < 4) {
386                version = 4;
387            }
388
389            // Where's version 5?
390            // - Donut and sholes on 2.0 shipped with version 4 of launcher1.
391            // - Passion shipped on 2.1 with version 6 of launcher2
392            // - Sholes shipped on 2.1r1 (aka Mr. 3) with version 5 of launcher 1
393            //   but version 5 on there was the updateContactsShortcuts change
394            //   which was version 6 in launcher 2 (first shipped on passion 2.1r1).
395            // The updateContactsShortcuts change is idempotent, so running it twice
396            // is okay so we'll do that when upgrading the devices that shipped with it.
397            if (version < 6) {
398                // We went from 3 to 5 screens. Move everything 1 to the right
399                db.beginTransaction();
400                try {
401                    db.execSQL("UPDATE favorites SET screen=(screen + 1);");
402                    db.setTransactionSuccessful();
403                } catch (SQLException ex) {
404                    // Old version remains, which means we wipe old data
405                    Log.e(TAG, ex.getMessage(), ex);
406                } finally {
407                    db.endTransaction();
408                }
409
410               // We added the fast track.
411                if (updateContactsShortcuts(db)) {
412                    version = 6;
413                }
414            }
415
416            if (version < 7) {
417                // Version 7 gets rid of the special search widget.
418                convertWidgets(db);
419                version = 7;
420            }
421
422            if (version < 8) {
423                // Version 8 (froyo) has the icons all normalized.  This should
424                // already be the case in practice, but we now rely on it and don't
425                // resample the images each time.
426                normalizeIcons(db);
427                version = 8;
428            }
429
430            if (version != DATABASE_VERSION) {
431                Log.w(TAG, "Destroying all old data.");
432                db.execSQL("DROP TABLE IF EXISTS " + TABLE_FAVORITES);
433                onCreate(db);
434            }
435        }
436
437        private boolean updateContactsShortcuts(SQLiteDatabase db) {
438            Cursor c = null;
439            final String selectWhere = buildOrWhereString(Favorites.ITEM_TYPE,
440                    new int[] { Favorites.ITEM_TYPE_SHORTCUT });
441
442            db.beginTransaction();
443            try {
444                // Select and iterate through each matching widget
445                c = db.query(TABLE_FAVORITES, new String[] { Favorites._ID, Favorites.INTENT },
446                        selectWhere, null, null, null, null);
447
448                if (LOGD) Log.d(TAG, "found upgrade cursor count=" + c.getCount());
449
450                final ContentValues values = new ContentValues();
451                final int idIndex = c.getColumnIndex(Favorites._ID);
452                final int intentIndex = c.getColumnIndex(Favorites.INTENT);
453
454                while (c != null && c.moveToNext()) {
455                    long favoriteId = c.getLong(idIndex);
456                    final String intentUri = c.getString(intentIndex);
457                    if (intentUri != null) {
458                        try {
459                            Intent intent = Intent.parseUri(intentUri, 0);
460                            android.util.Log.d("Home", intent.toString());
461                            final Uri uri = intent.getData();
462                            final String data = uri.toString();
463                            if (Intent.ACTION_VIEW.equals(intent.getAction()) &&
464                                    (data.startsWith("content://contacts/people/") ||
465                                    data.startsWith("content://com.android.contacts/contacts/lookup/"))) {
466
467                                intent = new Intent("com.android.contacts.action.QUICK_CONTACT");
468                                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
469                                        Intent.FLAG_ACTIVITY_CLEAR_TOP |
470                                        Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
471
472                                intent.setData(uri);
473                                intent.putExtra("mode", 3);
474                                intent.putExtra("exclude_mimes", (String[]) null);
475
476                                values.clear();
477                                values.put(LauncherSettings.Favorites.INTENT, intent.toUri(0));
478
479                                String updateWhere = Favorites._ID + "=" + favoriteId;
480                                db.update(TABLE_FAVORITES, values, updateWhere, null);
481                            }
482                        } catch (RuntimeException ex) {
483                            Log.e(TAG, "Problem upgrading shortcut", ex);
484                        } catch (URISyntaxException e) {
485                            Log.e(TAG, "Problem upgrading shortcut", e);
486                        }
487                    }
488                }
489
490                db.setTransactionSuccessful();
491            } catch (SQLException ex) {
492                Log.w(TAG, "Problem while upgrading contacts", ex);
493                return false;
494            } finally {
495                db.endTransaction();
496                if (c != null) {
497                    c.close();
498                }
499            }
500
501            return true;
502        }
503
504        private void normalizeIcons(SQLiteDatabase db) {
505            Log.d(TAG, "normalizing icons");
506
507            db.beginTransaction();
508            Cursor c = null;
509            SQLiteStatement update = null;
510            try {
511                boolean logged = false;
512                update = db.compileStatement("UPDATE favorites "
513                        + "SET icon=? WHERE _id=?");
514
515                c = db.rawQuery("SELECT _id, icon FROM favorites WHERE iconType=" +
516                        Favorites.ICON_TYPE_BITMAP, null);
517
518                final int idIndex = c.getColumnIndexOrThrow(Favorites._ID);
519                final int iconIndex = c.getColumnIndexOrThrow(Favorites.ICON);
520
521                while (c.moveToNext()) {
522                    long id = c.getLong(idIndex);
523                    byte[] data = c.getBlob(iconIndex);
524                    try {
525                        Bitmap bitmap = Utilities.resampleIconBitmap(
526                                BitmapFactory.decodeByteArray(data, 0, data.length),
527                                mContext);
528                        if (bitmap != null) {
529                            update.bindLong(1, id);
530                            data = ItemInfo.flattenBitmap(bitmap);
531                            if (data != null) {
532                                update.bindBlob(2, data);
533                                update.execute();
534                            }
535                            bitmap.recycle();
536                        }
537                    } catch (Exception e) {
538                        if (!logged) {
539                            Log.e(TAG, "Failed normalizing icon " + id, e);
540                        } else {
541                            Log.e(TAG, "Also failed normalizing icon " + id);
542                        }
543                        logged = true;
544                    }
545                }
546                db.setTransactionSuccessful();
547            } catch (SQLException ex) {
548                Log.w(TAG, "Problem while allocating appWidgetIds for existing widgets", ex);
549            } finally {
550                db.endTransaction();
551                if (update != null) {
552                    update.close();
553                }
554                if (c != null) {
555                    c.close();
556                }
557            }
558        }
559
560        // Generates a new ID to use for an object in your database. This method should be only
561        // called from the main UI thread. As an exception, we do call it when we call the
562        // constructor from the worker thread; however, this doesn't extend until after the
563        // constructor is called, and we only pass a reference to LauncherProvider to LauncherApp
564        // after that point
565        public long generateNewId() {
566            if (mMaxId < 0) {
567                throw new RuntimeException("Error: max id was not initialized");
568            }
569            mMaxId += 1;
570            return mMaxId;
571        }
572
573        private long initializeMaxId(SQLiteDatabase db) {
574            Cursor c = db.rawQuery("SELECT MAX(_id) FROM favorites", null);
575
576            // get the result
577            final int maxIdIndex = 0;
578            long id = -1;
579            if (c != null && c.moveToNext()) {
580                id = c.getLong(maxIdIndex);
581            }
582
583            if (id == -1) {
584                throw new RuntimeException("Error: could not query max id");
585            }
586
587            return id;
588        }
589
590        /**
591         * Upgrade existing clock and photo frame widgets into their new widget
592         * equivalents.
593         */
594        private void convertWidgets(SQLiteDatabase db) {
595            final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext);
596            final int[] bindSources = new int[] {
597                    Favorites.ITEM_TYPE_WIDGET_CLOCK,
598                    Favorites.ITEM_TYPE_WIDGET_PHOTO_FRAME,
599                    Favorites.ITEM_TYPE_WIDGET_SEARCH,
600            };
601
602            final String selectWhere = buildOrWhereString(Favorites.ITEM_TYPE, bindSources);
603
604            Cursor c = null;
605
606            db.beginTransaction();
607            try {
608                // Select and iterate through each matching widget
609                c = db.query(TABLE_FAVORITES, new String[] { Favorites._ID, Favorites.ITEM_TYPE },
610                        selectWhere, null, null, null, null);
611
612                if (LOGD) Log.d(TAG, "found upgrade cursor count=" + c.getCount());
613
614                final ContentValues values = new ContentValues();
615                while (c != null && c.moveToNext()) {
616                    long favoriteId = c.getLong(0);
617                    int favoriteType = c.getInt(1);
618
619                    // Allocate and update database with new appWidgetId
620                    try {
621                        int appWidgetId = mAppWidgetHost.allocateAppWidgetId();
622
623                        if (LOGD) {
624                            Log.d(TAG, "allocated appWidgetId=" + appWidgetId
625                                    + " for favoriteId=" + favoriteId);
626                        }
627                        values.clear();
628                        values.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_APPWIDGET);
629                        values.put(Favorites.APPWIDGET_ID, appWidgetId);
630
631                        // Original widgets might not have valid spans when upgrading
632                        if (favoriteType == Favorites.ITEM_TYPE_WIDGET_SEARCH) {
633                            values.put(LauncherSettings.Favorites.SPANX, 4);
634                            values.put(LauncherSettings.Favorites.SPANY, 1);
635                        } else {
636                            values.put(LauncherSettings.Favorites.SPANX, 2);
637                            values.put(LauncherSettings.Favorites.SPANY, 2);
638                        }
639
640                        String updateWhere = Favorites._ID + "=" + favoriteId;
641                        db.update(TABLE_FAVORITES, values, updateWhere, null);
642
643                        if (favoriteType == Favorites.ITEM_TYPE_WIDGET_CLOCK) {
644                            appWidgetManager.bindAppWidgetId(appWidgetId,
645                                    new ComponentName("com.android.alarmclock",
646                                    "com.android.alarmclock.AnalogAppWidgetProvider"));
647                        } else if (favoriteType == Favorites.ITEM_TYPE_WIDGET_PHOTO_FRAME) {
648                            appWidgetManager.bindAppWidgetId(appWidgetId,
649                                    new ComponentName("com.android.camera",
650                                    "com.android.camera.PhotoAppWidgetProvider"));
651                        } else if (favoriteType == Favorites.ITEM_TYPE_WIDGET_SEARCH) {
652                            appWidgetManager.bindAppWidgetId(appWidgetId,
653                                    getSearchWidgetProvider());
654                        }
655                    } catch (RuntimeException ex) {
656                        Log.e(TAG, "Problem allocating appWidgetId", ex);
657                    }
658                }
659
660                db.setTransactionSuccessful();
661            } catch (SQLException ex) {
662                Log.w(TAG, "Problem while allocating appWidgetIds for existing widgets", ex);
663            } finally {
664                db.endTransaction();
665                if (c != null) {
666                    c.close();
667                }
668            }
669        }
670
671        /**
672         * Loads the default set of favorite packages from an xml file.
673         *
674         * @param db The database to write the values into
675         */
676        private int loadFavorites(SQLiteDatabase db) {
677            Intent intent = new Intent(Intent.ACTION_MAIN, null);
678            intent.addCategory(Intent.CATEGORY_LAUNCHER);
679            ContentValues values = new ContentValues();
680
681            PackageManager packageManager = mContext.getPackageManager();
682            int i = 0;
683            try {
684                XmlResourceParser parser = mContext.getResources().getXml(R.xml.default_workspace);
685                AttributeSet attrs = Xml.asAttributeSet(parser);
686                XmlUtils.beginDocument(parser, TAG_FAVORITES);
687
688                final int depth = parser.getDepth();
689
690                int type;
691                while (((type = parser.next()) != XmlPullParser.END_TAG ||
692                        parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
693
694                    if (type != XmlPullParser.START_TAG) {
695                        continue;
696                    }
697
698                    boolean added = false;
699                    final String name = parser.getName();
700
701                    TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.Favorite);
702
703                    values.clear();
704                    values.put(LauncherSettings.Favorites.CONTAINER,
705                            LauncherSettings.Favorites.CONTAINER_DESKTOP);
706                    values.put(LauncherSettings.Favorites.SCREEN,
707                            a.getString(R.styleable.Favorite_screen));
708                    values.put(LauncherSettings.Favorites.CELLX,
709                            a.getString(R.styleable.Favorite_x));
710                    values.put(LauncherSettings.Favorites.CELLY,
711                            a.getString(R.styleable.Favorite_y));
712
713                    if (TAG_FAVORITE.equals(name)) {
714                        added = addAppShortcut(db, values, a, packageManager, intent);
715                    } else if (TAG_SEARCH.equals(name)) {
716                        added = addSearchWidget(db, values);
717                    } else if (TAG_CLOCK.equals(name)) {
718                        added = addClockWidget(db, values);
719                    } else if (TAG_APPWIDGET.equals(name)) {
720                        added = addAppWidget(db, values, a, packageManager);
721                    } else if (TAG_SHORTCUT.equals(name)) {
722                        added = addUriShortcut(db, values, a);
723                    }
724
725                    if (added) i++;
726
727                    a.recycle();
728                }
729            } catch (XmlPullParserException e) {
730                Log.w(TAG, "Got exception parsing favorites.", e);
731            } catch (IOException e) {
732                Log.w(TAG, "Got exception parsing favorites.", e);
733            }
734
735            return i;
736        }
737
738        private boolean addAppShortcut(SQLiteDatabase db, ContentValues values, TypedArray a,
739                PackageManager packageManager, Intent intent) {
740
741            ActivityInfo info;
742            String packageName = a.getString(R.styleable.Favorite_packageName);
743            String className = a.getString(R.styleable.Favorite_className);
744            try {
745                ComponentName cn;
746                try {
747                    cn = new ComponentName(packageName, className);
748                    info = packageManager.getActivityInfo(cn, 0);
749                } catch (PackageManager.NameNotFoundException nnfe) {
750                    String[] packages = packageManager.currentToCanonicalPackageNames(
751                        new String[] { packageName });
752                    cn = new ComponentName(packages[0], className);
753                    info = packageManager.getActivityInfo(cn, 0);
754                }
755
756                intent.setComponent(cn);
757                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
758                        Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
759                values.put(Favorites.INTENT, intent.toUri(0));
760                values.put(Favorites.TITLE, info.loadLabel(packageManager).toString());
761                values.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_APPLICATION);
762                values.put(Favorites.SPANX, 1);
763                values.put(Favorites.SPANY, 1);
764                values.put(Favorites._ID, generateNewId());
765                dbInsertAndCheck(this, db, TABLE_FAVORITES, null, values);
766            } catch (PackageManager.NameNotFoundException e) {
767                Log.w(TAG, "Unable to add favorite: " + packageName +
768                        "/" + className, e);
769                return false;
770            }
771            return true;
772        }
773
774        private ComponentName getSearchWidgetProvider() {
775            SearchManager searchManager =
776                    (SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE);
777            ComponentName searchComponent = searchManager.getGlobalSearchActivity();
778            if (searchComponent == null) return null;
779            return getProviderInPackage(searchComponent.getPackageName());
780        }
781
782        /**
783         * Gets an appwidget provider from the given package. If the package contains more than
784         * one appwidget provider, an arbitrary one is returned.
785         */
786        private ComponentName getProviderInPackage(String packageName) {
787            AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext);
788            List<AppWidgetProviderInfo> providers = appWidgetManager.getInstalledProviders();
789            if (providers == null) return null;
790            final int providerCount = providers.size();
791            for (int i = 0; i < providerCount; i++) {
792                ComponentName provider = providers.get(i).provider;
793                if (provider != null && provider.getPackageName().equals(packageName)) {
794                    return provider;
795                }
796            }
797            return null;
798        }
799
800        private boolean addSearchWidget(SQLiteDatabase db, ContentValues values) {
801            ComponentName cn = getSearchWidgetProvider();
802            return addAppWidget(db, values, cn, 4, 1);
803        }
804
805        private boolean addClockWidget(SQLiteDatabase db, ContentValues values) {
806            ComponentName cn = new ComponentName("com.android.alarmclock",
807                    "com.android.alarmclock.AnalogAppWidgetProvider");
808            return addAppWidget(db, values, cn, 2, 2);
809        }
810
811        private boolean addAppWidget(SQLiteDatabase db, ContentValues values, TypedArray a,
812                PackageManager packageManager) {
813
814            String packageName = a.getString(R.styleable.Favorite_packageName);
815            String className = a.getString(R.styleable.Favorite_className);
816
817            if (packageName == null || className == null) {
818                return false;
819            }
820
821            boolean hasPackage = true;
822            ComponentName cn = new ComponentName(packageName, className);
823            try {
824                packageManager.getReceiverInfo(cn, 0);
825            } catch (Exception e) {
826                String[] packages = packageManager.currentToCanonicalPackageNames(
827                        new String[] { packageName });
828                cn = new ComponentName(packages[0], className);
829                try {
830                    packageManager.getReceiverInfo(cn, 0);
831                } catch (Exception e1) {
832                    hasPackage = false;
833                }
834            }
835
836            if (hasPackage) {
837                int spanX = a.getInt(R.styleable.Favorite_spanX, 0);
838                int spanY = a.getInt(R.styleable.Favorite_spanY, 0);
839                return addAppWidget(db, values, cn, spanX, spanY);
840            }
841
842            return false;
843        }
844
845        private boolean addAppWidget(SQLiteDatabase db, ContentValues values, ComponentName cn,
846                int spanX, int spanY) {
847            boolean allocatedAppWidgets = false;
848            final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext);
849
850            try {
851                int appWidgetId = mAppWidgetHost.allocateAppWidgetId();
852
853                values.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_APPWIDGET);
854                values.put(Favorites.SPANX, spanX);
855                values.put(Favorites.SPANY, spanY);
856                values.put(Favorites.APPWIDGET_ID, appWidgetId);
857                values.put(Favorites._ID, generateNewId());
858                dbInsertAndCheck(this, db, TABLE_FAVORITES, null, values);
859
860                allocatedAppWidgets = true;
861
862                appWidgetManager.bindAppWidgetId(appWidgetId, cn);
863            } catch (RuntimeException ex) {
864                Log.e(TAG, "Problem allocating appWidgetId", ex);
865            }
866
867            return allocatedAppWidgets;
868        }
869
870        private boolean addUriShortcut(SQLiteDatabase db, ContentValues values,
871                TypedArray a) {
872            Resources r = mContext.getResources();
873
874            final int iconResId = a.getResourceId(R.styleable.Favorite_icon, 0);
875            final int titleResId = a.getResourceId(R.styleable.Favorite_title, 0);
876
877            Intent intent;
878            String uri = null;
879            try {
880                uri = a.getString(R.styleable.Favorite_uri);
881                intent = Intent.parseUri(uri, 0);
882            } catch (URISyntaxException e) {
883                Log.w(TAG, "Shortcut has malformed uri: " + uri);
884                return false; // Oh well
885            }
886
887            if (iconResId == 0 || titleResId == 0) {
888                Log.w(TAG, "Shortcut is missing title or icon resource ID");
889                return false;
890            }
891
892            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
893            values.put(Favorites.INTENT, intent.toUri(0));
894            values.put(Favorites.TITLE, r.getString(titleResId));
895            values.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_SHORTCUT);
896            values.put(Favorites.SPANX, 1);
897            values.put(Favorites.SPANY, 1);
898            values.put(Favorites.ICON_TYPE, Favorites.ICON_TYPE_RESOURCE);
899            values.put(Favorites.ICON_PACKAGE, mContext.getPackageName());
900            values.put(Favorites.ICON_RESOURCE, r.getResourceName(iconResId));
901            values.put(Favorites._ID, generateNewId());
902            dbInsertAndCheck(this, db, TABLE_FAVORITES, null, values);
903
904            return true;
905        }
906    }
907
908    /**
909     * Build a query string that will match any row where the column matches
910     * anything in the values list.
911     */
912    static String buildOrWhereString(String column, int[] values) {
913        StringBuilder selectWhere = new StringBuilder();
914        for (int i = values.length - 1; i >= 0; i--) {
915            selectWhere.append(column).append("=").append(values[i]);
916            if (i > 0) {
917                selectWhere.append(" OR ");
918            }
919        }
920        return selectWhere.toString();
921    }
922
923    static class SqlArguments {
924        public final String table;
925        public final String where;
926        public final String[] args;
927
928        SqlArguments(Uri url, String where, String[] args) {
929            if (url.getPathSegments().size() == 1) {
930                this.table = url.getPathSegments().get(0);
931                this.where = where;
932                this.args = args;
933            } else if (url.getPathSegments().size() != 2) {
934                throw new IllegalArgumentException("Invalid URI: " + url);
935            } else if (!TextUtils.isEmpty(where)) {
936                throw new UnsupportedOperationException("WHERE clause not supported: " + url);
937            } else {
938                this.table = url.getPathSegments().get(0);
939                this.where = "_id=" + ContentUris.parseId(url);
940                this.args = null;
941            }
942        }
943
944        SqlArguments(Uri url) {
945            if (url.getPathSegments().size() == 1) {
946                table = url.getPathSegments().get(0);
947                where = null;
948                args = null;
949            } else {
950                throw new IllegalArgumentException("Invalid URI: " + url);
951            }
952        }
953    }
954}
955