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