BrowserProvider2.java revision b0de9ba4fe3acdcd0766eb07175044c12a54bd82
1/*
2 * Copyright (C) 2010 he 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.browser.provider;
18
19import com.android.browser.R;
20import com.android.internal.content.SyncStateContentProviderHelper;
21
22import android.content.ContentResolver;
23import android.content.ContentUris;
24import android.content.ContentValues;
25import android.content.Context;
26import android.content.UriMatcher;
27import android.database.Cursor;
28import android.database.DatabaseUtils;
29import android.database.sqlite.SQLiteDatabase;
30import android.database.sqlite.SQLiteOpenHelper;
31import android.database.sqlite.SQLiteQueryBuilder;
32import android.net.Uri;
33import android.provider.BrowserContract;
34import android.provider.BrowserContract.Bookmarks;
35import android.provider.BrowserContract.ChromeSyncColumns;
36import android.provider.BrowserContract.History;
37import android.provider.BrowserContract.Searches;
38import android.provider.BrowserContract.SyncState;
39import android.provider.ContactsContract.RawContacts;
40import android.provider.SyncStateContract;
41import android.text.TextUtils;
42
43import java.util.HashMap;
44
45public class BrowserProvider2 extends SQLiteContentProvider {
46
47    static final Uri LEGACY_BROWSER_AUTHORITY_URI = Uri.parse("browser");
48
49    static final String TABLE_BOOKMARKS = "bookmarks";
50    static final String TABLE_HISTORY = "history";
51    static final String TABLE_SEARCHES = "searches";
52    static final String TABLE_SYNC_STATE = "syncstate";
53
54    static final String HISTORY_JOIN_BOOKMARKS =
55            "history LEFT OUTER JOIN bookmarks ON (history.url = bookmarks.url)";
56
57    static final int BOOKMARKS = 1000;
58    static final int BOOKMARKS_ID = 1001;
59    static final int BOOKMARKS_FOLDER = 1002;
60    static final int BOOKMARKS_FOLDER_ID = 1003;
61
62    static final int HISTORY = 2000;
63    static final int HISTORY_ID = 2001;
64
65    static final int SEARCHES = 3000;
66    static final int SEARCHES_ID = 3001;
67
68    static final int SYNCSTATE = 4000;
69    static final int SYNCSTATE_ID = 4001;
70
71    static final long FIXED_ID_CHROME_ROOT = 1;
72    static final long FIXED_ID_BOOKMARKS = 2;
73    static final long FIXED_ID_BOOKMARKS_BAR = 3;
74    static final long FIXED_ID_OTHER_BOOKMARKS = 4;
75
76    static final String DEFAULT_BOOKMARKS_SORT_ORDER = "position ASC, _id ASC";
77
78    static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
79
80    static final HashMap<String, String> BOOKMARKS_PROJECTION_MAP = new HashMap<String, String>();
81    static final HashMap<String, String> OTHER_BOOKMARKS_PROJECTION_MAP = new HashMap<String, String>();
82    static final HashMap<String, String> HISTORY_PROJECTION_MAP = new HashMap<String, String>();
83    static final HashMap<String, String> SEARCHES_PROJECTION_MAP = new HashMap<String, String>();
84    static final HashMap<String, String> SYNC_STATE_PROJECTION_MAP = new HashMap<String, String>();
85
86    static {
87        final UriMatcher matcher = URI_MATCHER;
88        matcher.addURI(BrowserContract.AUTHORITY, "bookmarks", BOOKMARKS);
89        matcher.addURI(BrowserContract.AUTHORITY, "bookmarks/#", BOOKMARKS_ID);
90        matcher.addURI(BrowserContract.AUTHORITY, "bookmarks/folder", BOOKMARKS_FOLDER);
91        matcher.addURI(BrowserContract.AUTHORITY, "bookmarks/folder/#", BOOKMARKS_FOLDER_ID);
92        matcher.addURI(BrowserContract.AUTHORITY, "history", HISTORY);
93        matcher.addURI(BrowserContract.AUTHORITY, "history/#", HISTORY_ID);
94        matcher.addURI(BrowserContract.AUTHORITY, "searches", SEARCHES);
95        matcher.addURI(BrowserContract.AUTHORITY, "searches/#", SEARCHES_ID);
96        matcher.addURI(BrowserContract.AUTHORITY, "syncstate", SYNCSTATE);
97        matcher.addURI(BrowserContract.AUTHORITY, "syncstate/#", SYNCSTATE_ID);
98
99        // Common BookmarkColumns
100        HashMap<String, String> bookmarksColumns = new HashMap();
101        bookmarksColumns.put(Bookmarks.TITLE, Bookmarks.TITLE);
102        bookmarksColumns.put(Bookmarks.URL, Bookmarks.URL);
103        bookmarksColumns.put(Bookmarks.FAVICON, Bookmarks.FAVICON);
104        bookmarksColumns.put(Bookmarks.THUMBNAIL, Bookmarks.THUMBNAIL);
105        bookmarksColumns.put(Bookmarks.TOUCH_ICON, Bookmarks.TOUCH_ICON);
106
107        // Bookmarks
108        HashMap<String, String> map = BOOKMARKS_PROJECTION_MAP;
109        map.putAll(bookmarksColumns);
110        map.put(Bookmarks._ID, TABLE_BOOKMARKS + "._id AS _id");
111        map.put(Bookmarks.IS_FOLDER, Bookmarks.IS_FOLDER);
112        map.put(Bookmarks.PARENT, Bookmarks.PARENT);
113        map.put(Bookmarks.POSITION, Bookmarks.POSITION);
114        map.put(Bookmarks.IS_DELETED, Bookmarks.IS_DELETED);
115        map.put(Bookmarks.ACCOUNT_NAME, Bookmarks.ACCOUNT_NAME);
116        map.put(Bookmarks.ACCOUNT_TYPE, Bookmarks.ACCOUNT_TYPE);
117        map.put(Bookmarks.SOURCE_ID, Bookmarks.SOURCE_ID);
118        map.put(Bookmarks.VERSION, Bookmarks.VERSION);
119        map.put(Bookmarks.DIRTY, Bookmarks.DIRTY);
120        map.put(Bookmarks.SYNC1, Bookmarks.SYNC1);
121        map.put(Bookmarks.SYNC2, Bookmarks.SYNC2);
122        map.put(Bookmarks.SYNC3, Bookmarks.SYNC3);
123        map.put(Bookmarks.SYNC4, Bookmarks.SYNC4);
124        map.put(Bookmarks.SYNC5, Bookmarks.SYNC5);
125
126        // Other bookmarks
127        OTHER_BOOKMARKS_PROJECTION_MAP.putAll(BOOKMARKS_PROJECTION_MAP);
128        OTHER_BOOKMARKS_PROJECTION_MAP.put(Bookmarks.POSITION,
129                Long.toString(Long.MAX_VALUE) + " AS " + Bookmarks.POSITION);
130
131        // History
132        map = HISTORY_PROJECTION_MAP;
133        map.putAll(bookmarksColumns);
134        map.put(History._ID, TABLE_HISTORY + "._id AS _id");
135        map.put(History.DATE_CREATED, History.DATE_CREATED);
136        map.put(History.DATE_LAST_VISITED, History.DATE_LAST_VISITED);
137        map.put(History.VISITS, History.VISITS);
138
139        // Sync state
140        map = SYNC_STATE_PROJECTION_MAP;
141        map.put(SyncState._ID, SyncState._ID);
142        map.put(SyncState.ACCOUNT_NAME, SyncState.ACCOUNT_NAME);
143        map.put(SyncState.ACCOUNT_TYPE, SyncState.ACCOUNT_TYPE);
144        map.put(SyncState.DATA, SyncState.DATA);
145    }
146
147    DatabaseHelper mOpenHelper;
148    SyncStateContentProviderHelper mSyncHelper = new SyncStateContentProviderHelper();
149
150    final class DatabaseHelper extends SQLiteOpenHelper {
151        static final String DATABASE_NAME = "browser2.db";
152        static final int DATABASE_VERSION = 11;
153        public DatabaseHelper(Context context) {
154            super(context, DATABASE_NAME, null, DATABASE_VERSION);
155        }
156
157        @Override
158        public void onCreate(SQLiteDatabase db) {
159            db.execSQL("CREATE TABLE " + TABLE_BOOKMARKS + "(" +
160                    Bookmarks._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
161                    Bookmarks.TITLE + " TEXT," +
162                    Bookmarks.URL + " TEXT," +
163                    Bookmarks.FAVICON + " BLOB," +
164                    Bookmarks.THUMBNAIL + " BLOB," +
165                    Bookmarks.TOUCH_ICON + " BLOB," +
166                    Bookmarks.IS_FOLDER + " INTEGER NOT NULL DEFAULT 0," +
167                    Bookmarks.PARENT + " INTEGER NOT NULL DEFAULT 0," +
168                    Bookmarks.POSITION + " INTEGER NOT NULL," +
169                    Bookmarks.INSERT_AFTER + " INTEGER," +
170                    Bookmarks.IS_DELETED + " INTEGER NOT NULL DEFAULT 0," +
171                    Bookmarks.ACCOUNT_NAME + " TEXT," +
172                    Bookmarks.ACCOUNT_TYPE + " TEXT," +
173                    Bookmarks.SOURCE_ID + " TEXT," +
174                    Bookmarks.VERSION + " INTEGER NOT NULL DEFAULT 1," +
175                    Bookmarks.DIRTY + " INTEGER NOT NULL DEFAULT 0," +
176                    Bookmarks.SYNC1 + " TEXT," +
177                    Bookmarks.SYNC2 + " TEXT," +
178                    Bookmarks.SYNC3 + " TEXT," +
179                    Bookmarks.SYNC4 + " TEXT," +
180                    Bookmarks.SYNC5 + " TEXT" +
181                    ");");
182
183            // TODO indices
184
185            createDefaultBookmarks(db);
186
187            db.execSQL("CREATE TABLE " + TABLE_HISTORY + "(" +
188                    History._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
189                    History.URL + " TEXT NOT NULL," +
190                    History.DATE_CREATED + " INTEGER," +
191                    History.DATE_LAST_VISITED + " INTEGER," +
192                    History.VISITS + " INTEGER NOT NULL DEFAULT 0" +
193                    ");");
194
195            db.execSQL("CREATE TABLE " + TABLE_SEARCHES + " (" +
196                    Searches._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
197                    Searches.SEARCH + " TEXT," +
198                    Searches.DATE + " LONG" +
199                    ");");
200
201            mSyncHelper.createDatabase(db);
202        }
203
204        @Override
205        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
206            // TODO write upgrade logic
207            db.execSQL("DROP TABLE IF EXISTS " + TABLE_BOOKMARKS);
208            db.execSQL("DROP TABLE IF EXISTS " + TABLE_HISTORY);
209            db.execSQL("DROP TABLE IF EXISTS " + TABLE_SEARCHES);
210            onCreate(db);
211        }
212
213        @Override
214        public void onOpen(SQLiteDatabase db) {
215            mSyncHelper.onDatabaseOpened(db);
216        }
217
218        private void createDefaultBookmarks(SQLiteDatabase db) {
219            ContentValues values = new ContentValues();
220            // TODO figure out how to deal with localization for the defaults
221
222            // Chrome sync root folder
223            values.put(Bookmarks._ID, FIXED_ID_CHROME_ROOT);
224            values.put(ChromeSyncColumns.SERVER_UNIQUE, ChromeSyncColumns.FOLDER_NAME_ROOT);
225            values.put(Bookmarks.TITLE, "Google Chrome");
226            values.put(Bookmarks.PARENT, 0);
227            values.put(Bookmarks.POSITION, 0);
228            values.put(Bookmarks.IS_FOLDER, true);
229            values.put(Bookmarks.DIRTY, true);
230            db.insertOrThrow(TABLE_BOOKMARKS, null, values);
231
232            // Bookmarks folder
233            values.put(Bookmarks._ID, FIXED_ID_BOOKMARKS);
234            values.put(ChromeSyncColumns.SERVER_UNIQUE, ChromeSyncColumns.FOLDER_NAME_BOOKMARKS);
235            values.put(Bookmarks.TITLE, "Bookmarks");
236            values.put(Bookmarks.PARENT, FIXED_ID_CHROME_ROOT);
237            values.put(Bookmarks.POSITION, 0);
238            values.put(Bookmarks.IS_FOLDER, true);
239            values.put(Bookmarks.DIRTY, true);
240            db.insertOrThrow(TABLE_BOOKMARKS, null, values);
241
242            // Bookmarks Bar folder
243            values.clear();
244            values.put(Bookmarks._ID, FIXED_ID_BOOKMARKS_BAR);
245            values.put(ChromeSyncColumns.SERVER_UNIQUE,
246                    ChromeSyncColumns.FOLDER_NAME_BOOKMARKS_BAR);
247            values.put(Bookmarks.TITLE, "Bookmarks Bar");
248            values.put(Bookmarks.PARENT, FIXED_ID_BOOKMARKS);
249            values.put(Bookmarks.POSITION, 0);
250            values.put(Bookmarks.IS_FOLDER, true);
251            values.put(Bookmarks.DIRTY, true);
252            db.insertOrThrow(TABLE_BOOKMARKS, null, values);
253
254            // Other Bookmarks folder
255            values.clear();
256            values.put(Bookmarks._ID, FIXED_ID_OTHER_BOOKMARKS);
257            values.put(ChromeSyncColumns.SERVER_UNIQUE,
258                    ChromeSyncColumns.FOLDER_NAME_OTHER_BOOKMARKS);
259            values.put(Bookmarks.TITLE, "Other Bookmarks");
260            values.put(Bookmarks.PARENT, FIXED_ID_BOOKMARKS);
261            values.put(Bookmarks.POSITION, 1000);
262            values.put(Bookmarks.IS_FOLDER, true);
263            values.put(Bookmarks.DIRTY, true);
264            db.insertOrThrow(TABLE_BOOKMARKS, null, values);
265
266            addDefaultBookmarks(db, FIXED_ID_BOOKMARKS_BAR);
267
268            // TODO remove this testing code
269            db.execSQL("INSERT INTO bookmarks (" +
270                    Bookmarks.TITLE + ", " +
271                    Bookmarks.URL + ", " +
272                    Bookmarks.IS_FOLDER + "," +
273                    Bookmarks.PARENT + "," +
274                    Bookmarks.POSITION +
275                ") VALUES (" +
276                    "'Google Reader', " +
277                    "'http://reader.google.com', " +
278                    "0," +
279                    Long.toString(FIXED_ID_OTHER_BOOKMARKS) + "," +
280                    0 +
281                    ");");
282        }
283
284        private void addDefaultBookmarks(SQLiteDatabase db, long parentId) {
285            final CharSequence[] bookmarks = getContext().getResources().getTextArray(
286                    R.array.bookmarks);
287            int size = bookmarks.length;
288            try {
289                for (int i = 0; i < size; i = i + 2) {
290                    CharSequence bookmarkDestination = replaceSystemPropertyInString(getContext(),
291                            bookmarks[i + 1]);
292                    db.execSQL("INSERT INTO bookmarks (" +
293                            Bookmarks.TITLE + ", " +
294                            Bookmarks.URL + ", " +
295                            Bookmarks.IS_FOLDER + "," +
296                            Bookmarks.PARENT + "," +
297                            Bookmarks.POSITION +
298                        ") VALUES (" +
299                            "'" + bookmarks[i] + "', " +
300                            "'" + bookmarkDestination + "', " +
301                            "0," +
302                            Long.toString(parentId) + "," +
303                            Integer.toString(i) +
304                            ");");
305                }
306            } catch (ArrayIndexOutOfBoundsException e) {
307            }
308        }
309
310        // XXX: This is a major hack to remove our dependency on gsf constants and
311        // its content provider. http://b/issue?id=2425179
312        private String getClientId(ContentResolver cr) {
313            String ret = "android-google";
314            Cursor c = null;
315            try {
316                c = cr.query(Uri.parse("content://com.google.settings/partner"),
317                        new String[] { "value" }, "name='client_id'", null, null);
318                if (c != null && c.moveToNext()) {
319                    ret = c.getString(0);
320                }
321            } catch (RuntimeException ex) {
322                // fall through to return the default
323            } finally {
324                if (c != null) {
325                    c.close();
326                }
327            }
328            return ret;
329        }
330
331        private CharSequence replaceSystemPropertyInString(Context context, CharSequence srcString) {
332            StringBuffer sb = new StringBuffer();
333            int lastCharLoc = 0;
334
335            final String client_id = getClientId(context.getContentResolver());
336
337            for (int i = 0; i < srcString.length(); ++i) {
338                char c = srcString.charAt(i);
339                if (c == '{') {
340                    sb.append(srcString.subSequence(lastCharLoc, i));
341                    lastCharLoc = i;
342              inner:
343                    for (int j = i; j < srcString.length(); ++j) {
344                        char k = srcString.charAt(j);
345                        if (k == '}') {
346                            String propertyKeyValue = srcString.subSequence(i + 1, j).toString();
347                            if (propertyKeyValue.equals("CLIENT_ID")) {
348                                sb.append(client_id);
349                            } else {
350                                sb.append("unknown");
351                            }
352                            lastCharLoc = j + 1;
353                            i = j;
354                            break inner;
355                        }
356                    }
357                }
358            }
359            if (srcString.length() - lastCharLoc > 0) {
360                // Put on the tail, if there is one
361                sb.append(srcString.subSequence(lastCharLoc, srcString.length()));
362            }
363            return sb;
364        }
365    }
366
367    @Override
368    public SQLiteOpenHelper getDatabaseHelper(Context context) {
369        synchronized (this) {
370            if (mOpenHelper == null) {
371                mOpenHelper = new DatabaseHelper(context);
372            }
373            return mOpenHelper;
374        }
375    }
376
377    @Override
378    public boolean isCallerSyncAdapter(Uri uri) {
379        return uri.getBooleanQueryParameter(BrowserContract.CALLER_IS_SYNCADAPTER, false);
380    }
381
382    @Override
383    public void notifyChange(boolean callerIsSyncAdapter) {
384        ContentResolver resolver = getContext().getContentResolver();
385        resolver.notifyChange(BrowserContract.AUTHORITY_URI, null, !callerIsSyncAdapter);
386        resolver.notifyChange(LEGACY_BROWSER_AUTHORITY_URI, null, !callerIsSyncAdapter);
387    }
388
389    @Override
390    public String getType(Uri uri) {
391        final int match = URI_MATCHER.match(uri);
392        switch (match) {
393            case BOOKMARKS:
394                return Bookmarks.CONTENT_TYPE;
395            case BOOKMARKS_ID:
396                return Bookmarks.CONTENT_ITEM_TYPE;
397            case HISTORY:
398                return History.CONTENT_TYPE;
399            case HISTORY_ID:
400                return History.CONTENT_ITEM_TYPE;
401            case SEARCHES:
402                return Searches.CONTENT_TYPE;
403            case SEARCHES_ID:
404                return Searches.CONTENT_ITEM_TYPE;
405//            case SUGGEST:
406//                return SearchManager.SUGGEST_MIME_TYPE;
407        }
408        return null;
409    }
410
411    @Override
412    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
413            String sortOrder) {
414        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
415        final int match = URI_MATCHER.match(uri);
416        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
417        switch (match) {
418            case BOOKMARKS_FOLDER_ID:
419            case BOOKMARKS_ID:
420            case BOOKMARKS: {
421                // Only show deleted bookmarks if requested to do so
422                if (!uri.getBooleanQueryParameter(Bookmarks.QUERY_PARAMETER_SHOW_DELETED, false)) {
423                    selection = DatabaseUtils.concatenateWhere(
424                            Bookmarks.IS_DELETED + "=0", selection);
425                }
426
427                if (match == BOOKMARKS_ID) {
428                    // Tack on the ID of the specific bookmark requested
429                    selection = DatabaseUtils.concatenateWhere(
430                            TABLE_BOOKMARKS + "." + Bookmarks._ID + "=?", selection);
431                    selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
432                            new String[] { Long.toString(ContentUris.parseId(uri)) });
433                } else if (match == BOOKMARKS_FOLDER_ID) {
434                    // Tack on the ID of the specific folder requested
435                    selection = DatabaseUtils.concatenateWhere(
436                            TABLE_BOOKMARKS + "." + Bookmarks.PARENT + "=?", selection);
437                    selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
438                            new String[] { Long.toString(ContentUris.parseId(uri)) });
439                }
440
441                if (TextUtils.isEmpty(sortOrder)) {
442                    sortOrder = DEFAULT_BOOKMARKS_SORT_ORDER;
443                }
444
445                qb.setProjectionMap(BOOKMARKS_PROJECTION_MAP);
446                qb.setTables(TABLE_BOOKMARKS);
447                break;
448            }
449
450            case BOOKMARKS_FOLDER: {
451                // Don't allow selections to be applied to the default folder
452                if (!TextUtils.isEmpty(selection) || selectionArgs != null) {
453                    throw new UnsupportedOperationException(
454                            "selections aren't supported on this URI");
455                }
456
457                qb.setTables(TABLE_BOOKMARKS);
458                qb.setProjectionMap(BOOKMARKS_PROJECTION_MAP);
459                String bookmarksBarQuery = qb.buildQuery(projection,
460                        Bookmarks.PARENT + "=?",
461                        null, null, null, null, null);
462
463                qb.setProjectionMap(OTHER_BOOKMARKS_PROJECTION_MAP);
464                String otherBookmarksQuery = qb.buildQuery(projection,
465                        Bookmarks._ID + "=?",
466                        null, null, null, null, null);
467
468                String query = qb.buildUnionQuery(
469                        new String[] { bookmarksBarQuery, otherBookmarksQuery },
470                        DEFAULT_BOOKMARKS_SORT_ORDER, null);
471
472                return db.rawQuery(query, new String[] {
473                        Long.toString(FIXED_ID_BOOKMARKS_BAR),
474                        Long.toString(FIXED_ID_OTHER_BOOKMARKS)});
475            }
476
477            case HISTORY_ID: {
478                selection = DatabaseUtils.concatenateWhere(
479                        TABLE_HISTORY + "._id=?", selection);
480                selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
481                        new String[] { Long.toString(ContentUris.parseId(uri)) });
482                // fall through
483            }
484            case HISTORY: {
485                qb.setProjectionMap(HISTORY_PROJECTION_MAP);
486                qb.setTables(HISTORY_JOIN_BOOKMARKS);
487                break;
488            }
489
490            case SYNCSTATE: {
491                return mSyncHelper.query(db, projection, selection, selectionArgs, sortOrder);
492            }
493
494            case SYNCSTATE_ID: {
495                selection = appendAccountToSelection(uri, selection);
496                String selectionWithId =
497                        (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ")
498                        + (selection == null ? "" : " AND (" + selection + ")");
499                return mSyncHelper.query(db, projection, selectionWithId, selectionArgs, sortOrder);
500            }
501
502            default: {
503                throw new UnsupportedOperationException("Unknown URL " + uri.toString());
504            }
505        }
506
507        Cursor cursor = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder);
508        cursor.setNotificationUri(getContext().getContentResolver(), BrowserContract.AUTHORITY_URI);
509        return cursor;
510    }
511
512    @Override
513    public int deleteInTransaction(Uri uri, String selection, String[] selectionArgs,
514            boolean callerIsSyncAdapter) {
515        final int match = URI_MATCHER.match(uri);
516        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
517        switch (match) {
518            case BOOKMARKS_ID:
519            case BOOKMARKS: {
520                //TODO cascade deletes down from folders
521                if (!callerIsSyncAdapter) {
522                    // If the caller isn't a sync adapter just go through and update all the
523                    // bookmarks to have the deleted flag set.
524                    ContentValues values = new ContentValues();
525                    values.put(Bookmarks.IS_DELETED, 1);
526                    return updateInTransaction(uri, values, selection, selectionArgs,
527                            callerIsSyncAdapter);
528                } else {
529                    // Sync adapters are allowed to actually delete things
530                    if (match == BOOKMARKS_ID) {
531                        selection = DatabaseUtils.concatenateWhere(selection,
532                                TABLE_BOOKMARKS + "._id=?");
533                        selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
534                                new String[] { Long.toString(ContentUris.parseId(uri)) });
535                    }
536                    return db.delete(TABLE_BOOKMARKS, selection, selectionArgs);
537                }
538            }
539
540            case HISTORY_ID: {
541                selection = DatabaseUtils.concatenateWhere(selection, TABLE_HISTORY + "._id=?");
542                selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
543                        new String[] { Long.toString(ContentUris.parseId(uri)) });
544                // fall through
545            }
546            case HISTORY: {
547                return db.delete(TABLE_HISTORY, selection, selectionArgs);
548            }
549
550            case SEARCHES_ID: {
551                selection = DatabaseUtils.concatenateWhere(selection, TABLE_SEARCHES + "._id=?");
552                selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
553                        new String[] { Long.toString(ContentUris.parseId(uri)) });
554                // fall through
555            }
556            case SEARCHES: {
557                return db.delete(TABLE_SEARCHES, selection, selectionArgs);
558            }
559
560            case SYNCSTATE: {
561                return mSyncHelper.delete(db, selection, selectionArgs);
562            }
563            case SYNCSTATE_ID: {
564                String selectionWithId =
565                        (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ")
566                        + (selection == null ? "" : " AND (" + selection + ")");
567                return mSyncHelper.delete(db, selectionWithId, selectionArgs);
568            }
569        }
570        throw new UnsupportedOperationException("Unknown update URI " + uri);
571    }
572
573    @Override
574    public Uri insertInTransaction(Uri uri, ContentValues values, boolean callerIsSyncAdapter) {
575        final int match = URI_MATCHER.match(uri);
576        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
577        long id = -1;
578        switch (match) {
579            case BOOKMARKS: {
580                // Mark rows dirty if they're not coming from a sync adapater
581                if (!callerIsSyncAdapter) {
582                    values.put(Bookmarks.DIRTY, 1);
583                }
584
585                // If no parent is set default to the "Bookmarks Bar" folder
586                if (!values.containsKey(Bookmarks.PARENT)) {
587                    values.put(Bookmarks.PARENT, FIXED_ID_BOOKMARKS_BAR);
588                }
589
590                // If no position is requested put the bookmark at the beginning of the list
591                if (!values.containsKey(Bookmarks.POSITION)) {
592                    values.put(Bookmarks.POSITION, Long.toString(Long.MIN_VALUE));
593                }
594
595                id = db.insertOrThrow(TABLE_BOOKMARKS, Bookmarks.DIRTY, values);
596                break;
597            }
598
599            case HISTORY: {
600                id = db.insertOrThrow(TABLE_HISTORY, History.VISITS, values);
601                break;
602            }
603
604            case SEARCHES: {
605                id = db.insertOrThrow(TABLE_SEARCHES, Searches.SEARCH, values);
606                break;
607            }
608
609            case SYNCSTATE: {
610                id = mSyncHelper.insert(mDb, values);
611                break;
612            }
613
614            default: {
615                throw new UnsupportedOperationException("Unknown insert URI " + uri);
616            }
617        }
618
619        if (id >= 0) {
620            return ContentUris.withAppendedId(uri, id);
621        } else {
622            return null;
623        }
624    }
625
626    @Override
627    public int updateInTransaction(Uri uri, ContentValues values, String selection,
628            String[] selectionArgs, boolean callerIsSyncAdapter) {
629        final int match = URI_MATCHER.match(uri);
630        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
631        switch (match) {
632            case BOOKMARKS_ID: {
633                // Mark the bookmark dirty if the caller isn't a sync adapter
634                if (!callerIsSyncAdapter) {
635                    values = new ContentValues(values);
636                    values.put(Bookmarks.DIRTY, 1);
637                }
638                selection = DatabaseUtils.concatenateWhere(selection,
639                        TABLE_BOOKMARKS + "._id=?");
640                selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
641                        new String[] { Long.toString(ContentUris.parseId(uri)) });
642                return db.update(TABLE_BOOKMARKS, values, selection, selectionArgs);
643            }
644
645            case BOOKMARKS: {
646                if (!callerIsSyncAdapter) {
647                    values = new ContentValues(values);
648                    values.put(Bookmarks.DIRTY, 1);
649                }
650                return updateBookmarksInTransaction(values, selection, selectionArgs);
651            }
652
653            case HISTORY_ID: {
654                selection = DatabaseUtils.concatenateWhere(selection, TABLE_HISTORY + "._id=?");
655                selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
656                        new String[] { Long.toString(ContentUris.parseId(uri)) });
657                // fall through
658            }
659            case HISTORY: {
660                return db.update(TABLE_HISTORY, values, selection, selectionArgs);
661            }
662
663            case SEARCHES_ID: {
664                selection = DatabaseUtils.concatenateWhere(selection, TABLE_SEARCHES + "._id=?");
665                selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
666                        new String[] { Long.toString(ContentUris.parseId(uri)) });
667                // fall through
668            }
669            case SEARCHES: {
670                return db.update(TABLE_SEARCHES, values, selection, selectionArgs);
671            }
672
673            case SYNCSTATE: {
674                return mSyncHelper.update(mDb, values,
675                        appendAccountToSelection(uri, selection), selectionArgs);
676            }
677
678            case SYNCSTATE_ID: {
679                selection = appendAccountToSelection(uri, selection);
680                String selectionWithId =
681                        (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ")
682                        + (selection == null ? "" : " AND (" + selection + ")");
683                return mSyncHelper.update(mDb, values,
684                        selectionWithId, selectionArgs);
685            }
686        }
687        throw new UnsupportedOperationException("Unknown update URI " + uri);
688    }
689
690    /**
691     * Does a query to find the matching bookmarks and updates each one with the provided values.
692     */
693    private int updateBookmarksInTransaction(ContentValues values, String selection,
694            String[] selectionArgs) {
695        int count = 0;
696        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
697        Cursor cursor = query(Bookmarks.CONTENT_URI, new String[] { Bookmarks._ID },
698                selection, selectionArgs, null);
699        try {
700            String[] args = new String[1];
701            while (cursor.moveToNext()) {
702                args[0] = cursor.getString(0);
703                count += db.update(TABLE_BOOKMARKS, values, "_id=?", args);
704            }
705        } finally {
706            if (cursor != null) cursor.close();
707        }
708        return count;
709    }
710
711    private String appendAccountToSelection(Uri uri, String selection) {
712        final String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
713        final String accountType = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE);
714
715        final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType);
716        if (partialUri) {
717            // Throw when either account is incomplete
718            throw new IllegalArgumentException(
719                    "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE for " + uri);
720        }
721
722        // Accounts are valid by only checking one parameter, since we've
723        // already ruled out partial accounts.
724        final boolean validAccount = !TextUtils.isEmpty(accountName);
725        if (validAccount) {
726            StringBuilder selectionSb = new StringBuilder(RawContacts.ACCOUNT_NAME + "="
727                    + DatabaseUtils.sqlEscapeString(accountName) + " AND "
728                    + RawContacts.ACCOUNT_TYPE + "="
729                    + DatabaseUtils.sqlEscapeString(accountType));
730            if (!TextUtils.isEmpty(selection)) {
731                selectionSb.append(" AND (");
732                selectionSb.append(selection);
733                selectionSb.append(')');
734            }
735            return selectionSb.toString();
736        } else {
737            return selection;
738        }
739    }
740}
741