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