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