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