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