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