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