BrowserProvider2.java revision f176bda08f46588667c185603e2ac648ec8291aa
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.common.content.SyncStateContentProviderHelper;
21
22import android.accounts.Account;
23import android.content.ContentResolver;
24import android.content.ContentUris;
25import android.content.ContentValues;
26import android.content.Context;
27import android.content.UriMatcher;
28import android.content.res.Resources;
29import android.content.res.TypedArray;
30import android.database.Cursor;
31import android.database.DatabaseUtils;
32import android.database.sqlite.SQLiteDatabase;
33import android.database.sqlite.SQLiteOpenHelper;
34import android.database.sqlite.SQLiteQueryBuilder;
35import android.net.Uri;
36import android.provider.BrowserContract;
37import android.provider.BrowserContract.Accounts;
38import android.provider.BrowserContract.Bookmarks;
39import android.provider.BrowserContract.ChromeSyncColumns;
40import android.provider.BrowserContract.Combined;
41import android.provider.BrowserContract.History;
42import android.provider.BrowserContract.Images;
43import android.provider.BrowserContract.Searches;
44import android.provider.BrowserContract.Settings;
45import android.provider.BrowserContract.SyncState;
46import android.provider.ContactsContract.RawContacts;
47import android.provider.SyncStateContract;
48import android.text.TextUtils;
49
50import java.io.ByteArrayOutputStream;
51import java.io.IOException;
52import java.io.InputStream;
53import java.util.HashMap;
54
55public class BrowserProvider2 extends SQLiteContentProvider {
56
57    static final String LEGACY_AUTHORITY = "browser";
58    static final Uri LEGACY_AUTHORITY_URI = new Uri.Builder().authority(LEGACY_AUTHORITY).build();
59
60    static final String TABLE_BOOKMARKS = "bookmarks";
61    static final String TABLE_HISTORY = "history";
62    static final String TABLE_IMAGES = "images";
63    static final String TABLE_SEARCHES = "searches";
64    static final String TABLE_SYNC_STATE = "syncstate";
65    static final String TABLE_SETTINGS = "settings";
66    static final String VIEW_COMBINED = "combined";
67
68    static final String TABLE_BOOKMARKS_JOIN_IMAGES = "bookmarks LEFT OUTER JOIN images " +
69            "ON bookmarks.url = images." + Images.URL;
70    static final String TABLE_HISTORY_JOIN_IMAGES = "history LEFT OUTER JOIN images " +
71            "ON history.url = images." + Images.URL;
72
73    static final String DEFAULT_SORT_HISTORY = History.DATE_LAST_VISITED + " DESC";
74
75    static final String DEFAULT_SORT_SEARCHES = Searches.DATE + " DESC";
76
77    static final int BOOKMARKS = 1000;
78    static final int BOOKMARKS_ID = 1001;
79    static final int BOOKMARKS_FOLDER = 1002;
80    static final int BOOKMARKS_FOLDER_ID = 1003;
81
82    static final int HISTORY = 2000;
83    static final int HISTORY_ID = 2001;
84
85    static final int SEARCHES = 3000;
86    static final int SEARCHES_ID = 3001;
87
88    static final int SYNCSTATE = 4000;
89    static final int SYNCSTATE_ID = 4001;
90
91    static final int IMAGES = 5000;
92
93    static final int COMBINED = 6000;
94    static final int COMBINED_ID = 6001;
95
96    static final int ACCOUNTS = 7000;
97
98    static final int SETTINGS = 8000;
99
100    public static final long FIXED_ID_ROOT = 1;
101
102    // BookmarkListWidgetService.ORDER_BY_CLAUSE has a copy of this default sort order
103    static final String DEFAULT_BOOKMARKS_SORT_ORDER = "position ASC, _id ASC";
104
105    static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
106
107    static final HashMap<String, String> ACCOUNTS_PROJECTION_MAP = new HashMap<String, String>();
108    static final HashMap<String, String> BOOKMARKS_PROJECTION_MAP = new HashMap<String, String>();
109    static final HashMap<String, String> OTHER_BOOKMARKS_PROJECTION_MAP =
110            new HashMap<String, String>();
111    static final HashMap<String, String> HISTORY_PROJECTION_MAP = new HashMap<String, String>();
112    static final HashMap<String, String> SYNC_STATE_PROJECTION_MAP = new HashMap<String, String>();
113    static final HashMap<String, String> IMAGES_PROJECTION_MAP = new HashMap<String, String>();
114    static final HashMap<String, String> COMBINED_PROJECTION_MAP = new HashMap<String, String>();
115    static final HashMap<String, String> SEARCHES_PROJECTION_MAP = new HashMap<String, String>();
116    static final HashMap<String, String> SETTINGS_PROJECTION_MAP = new HashMap<String, String>();
117
118    static {
119        final UriMatcher matcher = URI_MATCHER;
120        final String authority = BrowserContract.AUTHORITY;
121        matcher.addURI(authority, "accounts", ACCOUNTS);
122        matcher.addURI(authority, "bookmarks", BOOKMARKS);
123        matcher.addURI(authority, "bookmarks/#", BOOKMARKS_ID);
124        matcher.addURI(authority, "bookmarks/folder", BOOKMARKS_FOLDER);
125        matcher.addURI(authority, "bookmarks/folder/#", BOOKMARKS_FOLDER_ID);
126        matcher.addURI(authority, "history", HISTORY);
127        matcher.addURI(authority, "history/#", HISTORY_ID);
128        matcher.addURI(authority, "searches", SEARCHES);
129        matcher.addURI(authority, "searches/#", SEARCHES_ID);
130        matcher.addURI(authority, "syncstate", SYNCSTATE);
131        matcher.addURI(authority, "syncstate/#", SYNCSTATE_ID);
132        matcher.addURI(authority, "images", IMAGES);
133        matcher.addURI(authority, "combined", COMBINED);
134        matcher.addURI(authority, "combined/#", COMBINED_ID);
135        matcher.addURI(authority, "settings", SETTINGS);
136
137        // Projection maps
138        HashMap<String, String> map;
139
140        // Accounts
141        map = ACCOUNTS_PROJECTION_MAP;
142        map.put(Accounts.ACCOUNT_TYPE, Accounts.ACCOUNT_TYPE);
143        map.put(Accounts.ACCOUNT_NAME, Accounts.ACCOUNT_NAME);
144
145        // Bookmarks
146        map = BOOKMARKS_PROJECTION_MAP;
147        map.put(Bookmarks._ID, qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID));
148        map.put(Bookmarks.TITLE, Bookmarks.TITLE);
149        map.put(Bookmarks.URL, Bookmarks.URL);
150        map.put(Bookmarks.FAVICON, Bookmarks.FAVICON);
151        map.put(Bookmarks.THUMBNAIL, Bookmarks.THUMBNAIL);
152        map.put(Bookmarks.TOUCH_ICON, Bookmarks.TOUCH_ICON);
153        map.put(Bookmarks.IS_FOLDER, Bookmarks.IS_FOLDER);
154        map.put(Bookmarks.PARENT, Bookmarks.PARENT);
155        map.put(Bookmarks.POSITION, Bookmarks.POSITION);
156        map.put(Bookmarks.INSERT_AFTER, Bookmarks.INSERT_AFTER);
157        map.put(Bookmarks.IS_DELETED, Bookmarks.IS_DELETED);
158        map.put(Bookmarks.ACCOUNT_NAME, Bookmarks.ACCOUNT_NAME);
159        map.put(Bookmarks.ACCOUNT_TYPE, Bookmarks.ACCOUNT_TYPE);
160        map.put(Bookmarks.SOURCE_ID, Bookmarks.SOURCE_ID);
161        map.put(Bookmarks.VERSION, Bookmarks.VERSION);
162        map.put(Bookmarks.DATE_CREATED, Bookmarks.DATE_CREATED);
163        map.put(Bookmarks.DATE_MODIFIED, Bookmarks.DATE_MODIFIED);
164        map.put(Bookmarks.DIRTY, Bookmarks.DIRTY);
165        map.put(Bookmarks.SYNC1, Bookmarks.SYNC1);
166        map.put(Bookmarks.SYNC2, Bookmarks.SYNC2);
167        map.put(Bookmarks.SYNC3, Bookmarks.SYNC3);
168        map.put(Bookmarks.SYNC4, Bookmarks.SYNC4);
169        map.put(Bookmarks.SYNC5, Bookmarks.SYNC5);
170        map.put(Bookmarks.PARENT_SOURCE_ID, "(SELECT " + Bookmarks.SOURCE_ID +
171                " FROM " + TABLE_BOOKMARKS + " A WHERE " +
172                "A." + Bookmarks._ID + "=" + TABLE_BOOKMARKS + "." + Bookmarks.PARENT +
173                ") AS " + Bookmarks.PARENT_SOURCE_ID);
174        map.put(Bookmarks.INSERT_AFTER_SOURCE_ID, "(SELECT " + Bookmarks.SOURCE_ID +
175                " FROM " + TABLE_BOOKMARKS + " A WHERE " +
176                "A." + Bookmarks._ID + "=" + TABLE_BOOKMARKS + "." + Bookmarks.INSERT_AFTER +
177                ") AS " + Bookmarks.INSERT_AFTER_SOURCE_ID);
178
179        // Other bookmarks
180        OTHER_BOOKMARKS_PROJECTION_MAP.putAll(BOOKMARKS_PROJECTION_MAP);
181        OTHER_BOOKMARKS_PROJECTION_MAP.put(Bookmarks.POSITION,
182                Long.toString(Long.MAX_VALUE) + " AS " + Bookmarks.POSITION);
183
184        // History
185        map = HISTORY_PROJECTION_MAP;
186        map.put(History._ID, qualifyColumn(TABLE_HISTORY, History._ID));
187        map.put(History.TITLE, History.TITLE);
188        map.put(History.URL, History.URL);
189        map.put(History.FAVICON, History.FAVICON);
190        map.put(History.THUMBNAIL, History.THUMBNAIL);
191        map.put(History.TOUCH_ICON, History.TOUCH_ICON);
192        map.put(History.DATE_CREATED, History.DATE_CREATED);
193        map.put(History.DATE_LAST_VISITED, History.DATE_LAST_VISITED);
194        map.put(History.VISITS, History.VISITS);
195        map.put(History.USER_ENTERED, History.USER_ENTERED);
196
197        // Sync state
198        map = SYNC_STATE_PROJECTION_MAP;
199        map.put(SyncState._ID, SyncState._ID);
200        map.put(SyncState.ACCOUNT_NAME, SyncState.ACCOUNT_NAME);
201        map.put(SyncState.ACCOUNT_TYPE, SyncState.ACCOUNT_TYPE);
202        map.put(SyncState.DATA, SyncState.DATA);
203
204        // Images
205        map = IMAGES_PROJECTION_MAP;
206        map.put(Images.URL, Images.URL);
207        map.put(Images.FAVICON, Images.FAVICON);
208        map.put(Images.THUMBNAIL, Images.THUMBNAIL);
209        map.put(Images.TOUCH_ICON, Images.TOUCH_ICON);
210
211        // Combined history half
212        map = COMBINED_PROJECTION_MAP;
213        map.put(Combined._ID, Combined._ID);
214        map.put(Combined.TITLE, Combined.TITLE);
215        map.put(Combined.URL, Combined.URL);
216        map.put(Combined.DATE_CREATED, Combined.DATE_CREATED);
217        map.put(Combined.DATE_LAST_VISITED, Combined.DATE_LAST_VISITED);
218        map.put(Combined.IS_BOOKMARK, Combined.IS_BOOKMARK);
219        map.put(Combined.VISITS, Combined.VISITS);
220        map.put(Combined.FAVICON, Combined.FAVICON);
221        map.put(Combined.THUMBNAIL, Combined.THUMBNAIL);
222        map.put(Combined.TOUCH_ICON, Combined.TOUCH_ICON);
223        map.put(Combined.USER_ENTERED, Combined.USER_ENTERED);
224
225        // Searches
226        map = SEARCHES_PROJECTION_MAP;
227        map.put(Searches._ID, Searches._ID);
228        map.put(Searches.SEARCH, Searches.SEARCH);
229        map.put(Searches.DATE, Searches.DATE);
230
231        // Settings
232        map = SETTINGS_PROJECTION_MAP;
233        map.put(Settings.KEY, Settings.KEY);
234        map.put(Settings.VALUE, Settings.VALUE);
235    }
236
237    static final String bookmarkOrHistoryColumn(String column) {
238        return "CASE WHEN bookmarks." + column + " IS NOT NULL THEN " +
239                "bookmarks." + column + " ELSE history." + column + " END AS " + column;
240    }
241
242    static final String qualifyColumn(String table, String column) {
243        return table + "." + column + " AS " + column;
244    }
245
246    DatabaseHelper mOpenHelper;
247    SyncStateContentProviderHelper mSyncHelper = new SyncStateContentProviderHelper();
248
249    final class DatabaseHelper extends SQLiteOpenHelper {
250        static final String DATABASE_NAME = "browser2.db";
251        static final int DATABASE_VERSION = 25;
252        public DatabaseHelper(Context context) {
253            super(context, DATABASE_NAME, null, DATABASE_VERSION);
254        }
255
256        @Override
257        public void onCreate(SQLiteDatabase db) {
258            db.execSQL("CREATE TABLE " + TABLE_BOOKMARKS + "(" +
259                    Bookmarks._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
260                    Bookmarks.TITLE + " TEXT," +
261                    Bookmarks.URL + " TEXT," +
262                    Bookmarks.IS_FOLDER + " INTEGER NOT NULL DEFAULT 0," +
263                    Bookmarks.PARENT + " INTEGER," +
264                    Bookmarks.POSITION + " INTEGER NOT NULL," +
265                    Bookmarks.INSERT_AFTER + " INTEGER," +
266                    Bookmarks.IS_DELETED + " INTEGER NOT NULL DEFAULT 0," +
267                    Bookmarks.ACCOUNT_NAME + " TEXT," +
268                    Bookmarks.ACCOUNT_TYPE + " TEXT," +
269                    Bookmarks.SOURCE_ID + " TEXT," +
270                    Bookmarks.VERSION + " INTEGER NOT NULL DEFAULT 1," +
271                    Bookmarks.DATE_CREATED + " INTEGER," +
272                    Bookmarks.DATE_MODIFIED + " INTEGER," +
273                    Bookmarks.DIRTY + " INTEGER NOT NULL DEFAULT 0," +
274                    Bookmarks.SYNC1 + " TEXT," +
275                    Bookmarks.SYNC2 + " TEXT," +
276                    Bookmarks.SYNC3 + " TEXT," +
277                    Bookmarks.SYNC4 + " TEXT," +
278                    Bookmarks.SYNC5 + " TEXT" +
279                    ");");
280
281            // TODO indices
282
283            db.execSQL("CREATE TABLE " + TABLE_HISTORY + "(" +
284                    History._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
285                    History.TITLE + " TEXT," +
286                    History.URL + " TEXT NOT NULL," +
287                    History.DATE_CREATED + " INTEGER," +
288                    History.DATE_LAST_VISITED + " INTEGER," +
289                    History.VISITS + " INTEGER NOT NULL DEFAULT 0," +
290                    History.USER_ENTERED + " INTEGER" +
291                    ");");
292
293            db.execSQL("CREATE TABLE " + TABLE_IMAGES + " (" +
294                    Images.URL + " TEXT UNIQUE NOT NULL," +
295                    Images.FAVICON + " BLOB," +
296                    Images.THUMBNAIL + " BLOB," +
297                    Images.TOUCH_ICON + " BLOB" +
298                    ");");
299            db.execSQL("CREATE INDEX imagesUrlIndex ON " + TABLE_IMAGES +
300                    "(" + Images.URL + ")");
301
302            db.execSQL("CREATE TABLE " + TABLE_SEARCHES + " (" +
303                    Searches._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
304                    Searches.SEARCH + " TEXT," +
305                    Searches.DATE + " LONG" +
306                    ");");
307
308            db.execSQL("CREATE TABLE " + TABLE_SETTINGS + " (" +
309                    Settings.KEY + " TEXT PRIMARY KEY," +
310                    Settings.VALUE + " TEXT NOT NULL" +
311                    ");");
312
313            db.execSQL("CREATE VIEW " + VIEW_COMBINED + " AS " +
314                "SELECT " +
315                    bookmarkOrHistoryColumn(Combined._ID) + ", " +
316                    bookmarkOrHistoryColumn(Combined.TITLE) + ", " +
317                    qualifyColumn(TABLE_HISTORY, Combined.URL) + ", " +
318                    qualifyColumn(TABLE_HISTORY, Combined.DATE_CREATED) + ", " +
319                    Combined.DATE_LAST_VISITED + ", " +
320                    "CASE WHEN bookmarks._id IS NOT NULL THEN 1 ELSE 0 END AS " + Combined.IS_BOOKMARK + ", " +
321                    Combined.VISITS + ", " +
322                    Combined.FAVICON + ", " +
323                    Combined.THUMBNAIL + ", " +
324                    Combined.TOUCH_ICON + ", " +
325                    "NULL AS " + Combined.USER_ENTERED + " "+
326                "FROM history LEFT OUTER JOIN bookmarks ON history.url = bookmarks.url LEFT OUTER JOIN images ON history.url = images.url_key " +
327
328                "UNION ALL " +
329
330                "SELECT " +
331                    Combined._ID + ", " +
332                    Combined.TITLE + ", " +
333                    Combined.URL + ", " +
334                    Combined.DATE_CREATED + ", " +
335                    "NULL AS " + Combined.DATE_LAST_VISITED + ", "+
336                    "1 AS " + Combined.IS_BOOKMARK + ", " +
337                    "0 AS " + Combined.VISITS + ", "+
338                    Combined.FAVICON + ", " +
339                    Combined.THUMBNAIL + ", " +
340                    Combined.TOUCH_ICON + ", " +
341                    "NULL AS " + Combined.USER_ENTERED + " "+
342                "FROM bookmarks LEFT OUTER JOIN images ON bookmarks.url = images.url_key WHERE url NOT IN (SELECT url FROM history)");
343
344            mSyncHelper.createDatabase(db);
345
346            createDefaultBookmarks(db);
347        }
348
349        @Override
350        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
351            // TODO write upgrade logic
352            db.execSQL("DROP TABLE IF EXISTS " + TABLE_BOOKMARKS);
353            db.execSQL("DROP TABLE IF EXISTS " + TABLE_HISTORY);
354            db.execSQL("DROP TABLE IF EXISTS " + TABLE_SEARCHES);
355            db.execSQL("DROP TABLE IF EXISTS " + TABLE_IMAGES);
356            db.execSQL("DROP TABLE IF EXISTS " + TABLE_SETTINGS);
357            db.execSQL("DROP VIEW IF EXISTS " + VIEW_COMBINED);
358            mSyncHelper.onAccountsChanged(db, new Account[] {}); // remove all sync info
359            onCreate(db);
360        }
361
362        @Override
363        public void onOpen(SQLiteDatabase db) {
364            mSyncHelper.onDatabaseOpened(db);
365        }
366
367        private void createDefaultBookmarks(SQLiteDatabase db) {
368            ContentValues values = new ContentValues();
369            // TODO figure out how to deal with localization for the defaults
370
371            // Bookmarks folder
372            values.put(Bookmarks._ID, FIXED_ID_ROOT);
373            values.put(ChromeSyncColumns.SERVER_UNIQUE, ChromeSyncColumns.FOLDER_NAME_BOOKMARKS);
374            values.put(Bookmarks.TITLE, "Bookmarks");
375            values.putNull(Bookmarks.PARENT);
376            values.put(Bookmarks.POSITION, 0);
377            values.put(Bookmarks.IS_FOLDER, true);
378            values.put(Bookmarks.DIRTY, true);
379            db.insertOrThrow(TABLE_BOOKMARKS, null, values);
380
381            addDefaultBookmarks(db, FIXED_ID_ROOT);
382        }
383
384        private void addDefaultBookmarks(SQLiteDatabase db, long parentId) {
385            Resources res = getContext().getResources();
386            final CharSequence[] bookmarks = res.getTextArray(
387                    R.array.bookmarks);
388            int size = bookmarks.length;
389            TypedArray preloads = res.obtainTypedArray(R.array.bookmark_preloads);
390            try {
391                String parent = Long.toString(parentId);
392                String now = Long.toString(System.currentTimeMillis());
393                for (int i = 0; i < size; i = i + 2) {
394                    CharSequence bookmarkDestination = replaceSystemPropertyInString(getContext(),
395                            bookmarks[i + 1]);
396                    db.execSQL("INSERT INTO bookmarks (" +
397                            Bookmarks.TITLE + ", " +
398                            Bookmarks.URL + ", " +
399                            Bookmarks.IS_FOLDER + "," +
400                            Bookmarks.PARENT + "," +
401                            Bookmarks.POSITION + "," +
402                            Bookmarks.DATE_CREATED +
403                        ") VALUES (" +
404                            "'" + bookmarks[i] + "', " +
405                            "'" + bookmarkDestination + "', " +
406                            "0," +
407                            parent + "," +
408                            Integer.toString(i) + "," +
409                            now +
410                            ");");
411
412                    int faviconId = preloads.getResourceId(i, 0);
413                    int thumbId = preloads.getResourceId(i + 1, 0);
414                    byte[] thumb = null, favicon = null;
415                    try {
416                        thumb = readRaw(res, thumbId);
417                    } catch (IOException e) {
418                    }
419                    try {
420                        favicon = readRaw(res, faviconId);
421                    } catch (IOException e) {
422                    }
423                    if (thumb != null || favicon != null) {
424                        ContentValues imageValues = new ContentValues();
425                        imageValues.put(Images.URL, bookmarkDestination.toString());
426                        if (favicon != null) {
427                            imageValues.put(Images.FAVICON, favicon);
428                        }
429                        if (thumb != null) {
430                            imageValues.put(Images.THUMBNAIL, thumb);
431                        }
432                        db.insert(TABLE_IMAGES, Images.FAVICON, imageValues);
433                    }
434                }
435            } catch (ArrayIndexOutOfBoundsException e) {
436            }
437        }
438
439        private byte[] readRaw(Resources res, int id) throws IOException {
440            InputStream is = res.openRawResource(id);
441            try {
442                ByteArrayOutputStream bos = new ByteArrayOutputStream();
443                byte[] buf = new byte[4096];
444                int read;
445                while ((read = is.read(buf)) > 0) {
446                    bos.write(buf, 0, read);
447                }
448                bos.flush();
449                return bos.toByteArray();
450            } finally {
451                is.close();
452            }
453        }
454
455        // XXX: This is a major hack to remove our dependency on gsf constants and
456        // its content provider. http://b/issue?id=2425179
457        private String getClientId(ContentResolver cr) {
458            String ret = "android-google";
459            Cursor c = null;
460            try {
461                c = cr.query(Uri.parse("content://com.google.settings/partner"),
462                        new String[] { "value" }, "name='client_id'", null, null);
463                if (c != null && c.moveToNext()) {
464                    ret = c.getString(0);
465                }
466            } catch (RuntimeException ex) {
467                // fall through to return the default
468            } finally {
469                if (c != null) {
470                    c.close();
471                }
472            }
473            return ret;
474        }
475
476        private CharSequence replaceSystemPropertyInString(Context context, CharSequence srcString) {
477            StringBuffer sb = new StringBuffer();
478            int lastCharLoc = 0;
479
480            final String client_id = getClientId(context.getContentResolver());
481
482            for (int i = 0; i < srcString.length(); ++i) {
483                char c = srcString.charAt(i);
484                if (c == '{') {
485                    sb.append(srcString.subSequence(lastCharLoc, i));
486                    lastCharLoc = i;
487              inner:
488                    for (int j = i; j < srcString.length(); ++j) {
489                        char k = srcString.charAt(j);
490                        if (k == '}') {
491                            String propertyKeyValue = srcString.subSequence(i + 1, j).toString();
492                            if (propertyKeyValue.equals("CLIENT_ID")) {
493                                sb.append(client_id);
494                            } else {
495                                sb.append("unknown");
496                            }
497                            lastCharLoc = j + 1;
498                            i = j;
499                            break inner;
500                        }
501                    }
502                }
503            }
504            if (srcString.length() - lastCharLoc > 0) {
505                // Put on the tail, if there is one
506                sb.append(srcString.subSequence(lastCharLoc, srcString.length()));
507            }
508            return sb;
509        }
510    }
511
512    @Override
513    public SQLiteOpenHelper getDatabaseHelper(Context context) {
514        synchronized (this) {
515            if (mOpenHelper == null) {
516                mOpenHelper = new DatabaseHelper(context);
517            }
518            return mOpenHelper;
519        }
520    }
521
522    @Override
523    public boolean isCallerSyncAdapter(Uri uri) {
524        return uri.getBooleanQueryParameter(BrowserContract.CALLER_IS_SYNCADAPTER, false);
525    }
526
527    @Override
528    public void notifyChange(boolean callerIsSyncAdapter) {
529        ContentResolver resolver = getContext().getContentResolver();
530        resolver.notifyChange(BrowserContract.AUTHORITY_URI, null, !callerIsSyncAdapter);
531        resolver.notifyChange(LEGACY_AUTHORITY_URI, null, !callerIsSyncAdapter);
532    }
533
534    @Override
535    public String getType(Uri uri) {
536        final int match = URI_MATCHER.match(uri);
537        switch (match) {
538            case BOOKMARKS:
539                return Bookmarks.CONTENT_TYPE;
540            case BOOKMARKS_ID:
541                return Bookmarks.CONTENT_ITEM_TYPE;
542            case HISTORY:
543                return History.CONTENT_TYPE;
544            case HISTORY_ID:
545                return History.CONTENT_ITEM_TYPE;
546            case SEARCHES:
547                return Searches.CONTENT_TYPE;
548            case SEARCHES_ID:
549                return Searches.CONTENT_ITEM_TYPE;
550//            case SUGGEST:
551//                return SearchManager.SUGGEST_MIME_TYPE;
552        }
553        return null;
554    }
555
556    @Override
557    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
558            String sortOrder) {
559        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
560        final int match = URI_MATCHER.match(uri);
561        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
562        String limit = uri.getQueryParameter(BrowserContract.PARAM_LIMIT);
563        switch (match) {
564            case ACCOUNTS: {
565                qb.setTables(TABLE_BOOKMARKS);
566                qb.setProjectionMap(ACCOUNTS_PROJECTION_MAP);
567                qb.setDistinct(true);
568                qb.appendWhere(Bookmarks.ACCOUNT_NAME + " IS NOT NULL");
569                break;
570            }
571
572            case BOOKMARKS_FOLDER_ID:
573            case BOOKMARKS_ID:
574            case BOOKMARKS: {
575                // Only show deleted bookmarks if requested to do so
576                if (!uri.getBooleanQueryParameter(Bookmarks.QUERY_PARAMETER_SHOW_DELETED, false)) {
577                    selection = DatabaseUtils.concatenateWhere(
578                            Bookmarks.IS_DELETED + "=0", selection);
579                }
580
581                if (match == BOOKMARKS_ID) {
582                    // Tack on the ID of the specific bookmark requested
583                    selection = DatabaseUtils.concatenateWhere(selection,
584                            TABLE_BOOKMARKS + "." + Bookmarks._ID + "=?");
585                    selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
586                            new String[] { Long.toString(ContentUris.parseId(uri)) });
587                } else if (match == BOOKMARKS_FOLDER_ID) {
588                    // Tack on the ID of the specific folder requested
589                    selection = DatabaseUtils.concatenateWhere(selection,
590                            TABLE_BOOKMARKS + "." + Bookmarks.PARENT + "=?");
591                    selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
592                            new String[] { Long.toString(ContentUris.parseId(uri)) });
593                }
594
595                // Look for account info
596                String accountType = uri.getQueryParameter(Bookmarks.PARAM_ACCOUNT_TYPE);
597                String accountName = uri.getQueryParameter(Bookmarks.PARAM_ACCOUNT_NAME);
598                if (!TextUtils.isEmpty(accountType) && !TextUtils.isEmpty(accountName)) {
599                    selection = DatabaseUtils.concatenateWhere(selection,
600                            Bookmarks.ACCOUNT_TYPE + "=? AND " + Bookmarks.ACCOUNT_NAME + "=? ");
601                    selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
602                            new String[] { accountType, accountName });
603                }
604
605                // Set a default sort order if one isn't specified
606                if (TextUtils.isEmpty(sortOrder)) {
607                    sortOrder = DEFAULT_BOOKMARKS_SORT_ORDER;
608                }
609
610                qb.setProjectionMap(BOOKMARKS_PROJECTION_MAP);
611                qb.setTables(TABLE_BOOKMARKS_JOIN_IMAGES);
612                break;
613            }
614
615            case BOOKMARKS_FOLDER: {
616                // Don't allow selections to be applied to the default folder
617                if (!TextUtils.isEmpty(selection) || selectionArgs != null) {
618                    throw new UnsupportedOperationException(
619                            "selections aren't supported on this URI");
620                }
621
622                // Look for an account
623                boolean useAccount = false;
624                String accountType = uri.getQueryParameter(Bookmarks.PARAM_ACCOUNT_TYPE);
625                String accountName = uri.getQueryParameter(Bookmarks.PARAM_ACCOUNT_NAME);
626                if (!TextUtils.isEmpty(accountType) && !TextUtils.isEmpty(accountName)) {
627                    useAccount = true;
628                }
629
630                qb.setTables(TABLE_BOOKMARKS_JOIN_IMAGES);
631                String[] args;
632                String query;
633                if (TextUtils.isEmpty(sortOrder)) {
634                    sortOrder = DEFAULT_BOOKMARKS_SORT_ORDER;
635                }
636                if (!useAccount) {
637                    qb.setProjectionMap(BOOKMARKS_PROJECTION_MAP);
638                    query = qb.buildQuery(projection,
639                            Bookmarks.PARENT + "=? AND " + Bookmarks.IS_DELETED + "=0",
640                            null, null, null, sortOrder, null);
641
642                    args = new String[] { Long.toString(FIXED_ID_ROOT) };
643                } else {
644                    qb.setProjectionMap(BOOKMARKS_PROJECTION_MAP);
645                    String bookmarksBarQuery = qb.buildQuery(projection,
646                            Bookmarks.ACCOUNT_TYPE + "=? AND " + Bookmarks.ACCOUNT_NAME + "=? " +
647                                    "AND parent = " +
648                                        "(SELECT _id FROM " + TABLE_BOOKMARKS + " WHERE " +
649                                        ChromeSyncColumns.SERVER_UNIQUE + "=" +
650                                        "'" + ChromeSyncColumns.FOLDER_NAME_BOOKMARKS_BAR + "' " +
651                                        "AND account_type = ? AND account_name = ?) " +
652                                    "AND " + Bookmarks.IS_DELETED + "=0",
653                            null, null, null, null, null);
654
655                    qb.setProjectionMap(OTHER_BOOKMARKS_PROJECTION_MAP);
656                    String otherBookmarksQuery = qb.buildQuery(projection,
657                            Bookmarks.ACCOUNT_TYPE + "=? AND " + Bookmarks.ACCOUNT_NAME + "=?" +
658                                    " AND " + ChromeSyncColumns.SERVER_UNIQUE + "=?",
659                            null, null, null, null, null);
660
661                    query = qb.buildUnionQuery(
662                            new String[] { bookmarksBarQuery, otherBookmarksQuery },
663                            sortOrder, limit);
664
665                    args = new String[] {
666                            accountType, accountName, accountType, accountName,
667                            accountType, accountName, ChromeSyncColumns.FOLDER_NAME_OTHER_BOOKMARKS,
668                            };
669                }
670
671                Cursor cursor = db.rawQuery(query, args);
672                if (cursor != null) {
673                    cursor.setNotificationUri(getContext().getContentResolver(),
674                            BrowserContract.AUTHORITY_URI);
675                }
676                return cursor;
677            }
678
679            case HISTORY_ID: {
680                selection = DatabaseUtils.concatenateWhere(selection, TABLE_HISTORY + "._id=?");
681                selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
682                        new String[] { Long.toString(ContentUris.parseId(uri)) });
683                // fall through
684            }
685            case HISTORY: {
686                if (sortOrder == null) {
687                    sortOrder = DEFAULT_SORT_HISTORY;
688                }
689                qb.setProjectionMap(HISTORY_PROJECTION_MAP);
690                qb.setTables(TABLE_HISTORY_JOIN_IMAGES);
691                break;
692            }
693
694            case SEARCHES_ID: {
695                selection = DatabaseUtils.concatenateWhere(selection, TABLE_SEARCHES + "._id=?");
696                selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
697                        new String[] { Long.toString(ContentUris.parseId(uri)) });
698                // fall through
699            }
700            case SEARCHES: {
701                if (sortOrder == null) {
702                    sortOrder = DEFAULT_SORT_SEARCHES;
703                }
704                qb.setTables(TABLE_SEARCHES);
705                qb.setProjectionMap(SEARCHES_PROJECTION_MAP);
706                break;
707            }
708
709            case SYNCSTATE: {
710                return mSyncHelper.query(db, projection, selection, selectionArgs, sortOrder);
711            }
712
713            case SYNCSTATE_ID: {
714                selection = appendAccountToSelection(uri, selection);
715                String selectionWithId =
716                        (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ")
717                        + (selection == null ? "" : " AND (" + selection + ")");
718                return mSyncHelper.query(db, projection, selectionWithId, selectionArgs, sortOrder);
719            }
720
721            case IMAGES: {
722                qb.setTables(TABLE_IMAGES);
723                qb.setProjectionMap(IMAGES_PROJECTION_MAP);
724                break;
725            }
726
727            case COMBINED_ID: {
728                selection = DatabaseUtils.concatenateWhere(selection, VIEW_COMBINED + "._id=?");
729                selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
730                        new String[] { Long.toString(ContentUris.parseId(uri)) });
731                // fall through
732            }
733            case COMBINED: {
734                qb.setTables(VIEW_COMBINED);
735                qb.setProjectionMap(COMBINED_PROJECTION_MAP);
736                break;
737            }
738
739            case SETTINGS: {
740                qb.setTables(TABLE_SETTINGS);
741                qb.setProjectionMap(SETTINGS_PROJECTION_MAP);
742                break;
743            }
744
745            default: {
746                throw new UnsupportedOperationException("Unknown URL " + uri.toString());
747            }
748        }
749
750        Cursor cursor = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder,
751                limit);
752        cursor.setNotificationUri(getContext().getContentResolver(), BrowserContract.AUTHORITY_URI);
753        return cursor;
754    }
755
756    @Override
757    public int deleteInTransaction(Uri uri, String selection, String[] selectionArgs,
758            boolean callerIsSyncAdapter) {
759        final int match = URI_MATCHER.match(uri);
760        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
761        switch (match) {
762            case BOOKMARKS_ID:
763            case BOOKMARKS: {
764                //TODO cascade deletes down from folders
765                if (!callerIsSyncAdapter) {
766                    // If the caller isn't a sync adapter just go through and update all the
767                    // bookmarks to have the deleted flag set.
768                    ContentValues values = new ContentValues();
769                    values.put(Bookmarks.DATE_MODIFIED, System.currentTimeMillis());
770                    values.put(Bookmarks.IS_DELETED, 1);
771                    return updateInTransaction(uri, values, selection, selectionArgs,
772                            callerIsSyncAdapter);
773                } else {
774                    // Sync adapters are allowed to actually delete things
775                    if (match == BOOKMARKS_ID) {
776                        selection = DatabaseUtils.concatenateWhere(selection,
777                                TABLE_BOOKMARKS + "._id=?");
778                        selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
779                                new String[] { Long.toString(ContentUris.parseId(uri)) });
780                    }
781                    return db.delete(TABLE_BOOKMARKS, selection, selectionArgs);
782                }
783            }
784
785            case HISTORY_ID: {
786                selection = DatabaseUtils.concatenateWhere(selection, TABLE_HISTORY + "._id=?");
787                selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
788                        new String[] { Long.toString(ContentUris.parseId(uri)) });
789                // fall through
790            }
791            case HISTORY: {
792                return db.delete(TABLE_HISTORY, selection, selectionArgs);
793            }
794
795            case SEARCHES_ID: {
796                selection = DatabaseUtils.concatenateWhere(selection, TABLE_SEARCHES + "._id=?");
797                selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
798                        new String[] { Long.toString(ContentUris.parseId(uri)) });
799                // fall through
800            }
801            case SEARCHES: {
802                return db.delete(TABLE_SEARCHES, selection, selectionArgs);
803            }
804
805            case SYNCSTATE: {
806                return mSyncHelper.delete(db, selection, selectionArgs);
807            }
808            case SYNCSTATE_ID: {
809                String selectionWithId =
810                        (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ")
811                        + (selection == null ? "" : " AND (" + selection + ")");
812                return mSyncHelper.delete(db, selectionWithId, selectionArgs);
813            }
814        }
815        throw new UnsupportedOperationException("Unknown update URI " + uri);
816    }
817
818    @Override
819    public Uri insertInTransaction(Uri uri, ContentValues values, boolean callerIsSyncAdapter) {
820        final int match = URI_MATCHER.match(uri);
821        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
822        long id = -1;
823        switch (match) {
824            case BOOKMARKS: {
825                // Mark rows dirty if they're not coming from a sync adapter
826                if (!callerIsSyncAdapter) {
827                    long now = System.currentTimeMillis();
828                    values.put(Bookmarks.DATE_CREATED, now);
829                    values.put(Bookmarks.DATE_MODIFIED, now);
830                    values.put(Bookmarks.DIRTY, 1);
831
832                    // If no parent is set default to the "Bookmarks Bar" folder
833                    // TODO set the parent based on the account info
834                    if (!values.containsKey(Bookmarks.PARENT)) {
835                        values.put(Bookmarks.PARENT, FIXED_ID_ROOT);
836                    }
837                }
838
839                // If no position is requested put the bookmark at the beginning of the list
840                if (!values.containsKey(Bookmarks.POSITION)) {
841                    values.put(Bookmarks.POSITION, Long.toString(Long.MIN_VALUE));
842                }
843
844                // Extract out the image values so they can be inserted into the images table
845                String url = values.getAsString(Bookmarks.URL);
846                ContentValues imageValues = extractImageValues(values, url);
847                Boolean isFolder = values.getAsBoolean(Bookmarks.IS_FOLDER);
848                if ((isFolder == null || !isFolder)
849                        && imageValues != null && !TextUtils.isEmpty(url)) {
850                    int count = db.update(TABLE_IMAGES, imageValues, Images.URL + "=?",
851                            new String[] { url });
852                    if (count == 0) {
853                        db.insertOrThrow(TABLE_IMAGES, Images.FAVICON, imageValues);
854                    }
855                }
856
857                id = db.insertOrThrow(TABLE_BOOKMARKS, Bookmarks.DIRTY, values);
858                break;
859            }
860
861            case HISTORY: {
862                // If no created time is specified set it to now
863                if (!values.containsKey(History.DATE_CREATED)) {
864                    values.put(History.DATE_CREATED, System.currentTimeMillis());
865                }
866
867                // Extract out the image values so they can be inserted into the images table
868                ContentValues imageValues = extractImageValues(values,
869                        values.getAsString(History.URL));
870                if (imageValues != null) {
871                    db.insertOrThrow(TABLE_IMAGES, Images.FAVICON, imageValues);
872                }
873
874                id = db.insertOrThrow(TABLE_HISTORY, History.VISITS, values);
875                break;
876            }
877
878            case SEARCHES: {
879                id = insertSearchesInTransaction(db, values);
880                break;
881            }
882
883            case SYNCSTATE: {
884                id = mSyncHelper.insert(db, values);
885                break;
886            }
887
888            case SETTINGS: {
889                id = 0;
890                insertSettingsInTransaction(db, values);
891                break;
892            }
893
894            default: {
895                throw new UnsupportedOperationException("Unknown insert URI " + uri);
896            }
897        }
898
899        if (id >= 0) {
900            return ContentUris.withAppendedId(uri, id);
901        } else {
902            return null;
903        }
904    }
905
906    /**
907     * Searches are unique, so perform an UPSERT manually since SQLite doesn't support them.
908     */
909    private long insertSearchesInTransaction(SQLiteDatabase db, ContentValues values) {
910        String search = values.getAsString(Searches.SEARCH);
911        if (TextUtils.isEmpty(search)) {
912            throw new IllegalArgumentException("Must include the SEARCH field");
913        }
914        Cursor cursor = null;
915        try {
916            cursor = db.query(TABLE_SEARCHES, new String[] { Searches._ID },
917                    Searches.SEARCH + "=?", new String[] { search }, null, null, null);
918            if (cursor.moveToNext()) {
919                long id = cursor.getLong(0);
920                db.update(TABLE_SEARCHES, values, Searches._ID + "=?",
921                        new String[] { Long.toString(id) });
922                return id;
923            } else {
924                return db.insertOrThrow(TABLE_SEARCHES, Searches.SEARCH, values);
925            }
926        } finally {
927            if (cursor != null) cursor.close();
928        }
929    }
930
931    /**
932     * Settings are unique, so perform an UPSERT manually since SQLite doesn't support them.
933     */
934    private long insertSettingsInTransaction(SQLiteDatabase db, ContentValues values) {
935        String key = values.getAsString(Settings.KEY);
936        if (TextUtils.isEmpty(key)) {
937            throw new IllegalArgumentException("Must include the KEY field");
938        }
939        String[] keyArray = new String[] { key };
940        Cursor cursor = null;
941        try {
942            cursor = db.query(TABLE_SETTINGS, new String[] { Settings.KEY },
943                    Settings.KEY + "=?", keyArray, null, null, null);
944            if (cursor.moveToNext()) {
945                long id = cursor.getLong(0);
946                db.update(TABLE_SETTINGS, values, Settings.KEY + "=?", keyArray);
947                return id;
948            } else {
949                return db.insertOrThrow(TABLE_SETTINGS, Settings.VALUE, values);
950            }
951        } finally {
952            if (cursor != null) cursor.close();
953        }
954    }
955
956    @Override
957    public int updateInTransaction(Uri uri, ContentValues values, String selection,
958            String[] selectionArgs, boolean callerIsSyncAdapter) {
959        final int match = URI_MATCHER.match(uri);
960        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
961        switch (match) {
962            case BOOKMARKS_ID: {
963                selection = DatabaseUtils.concatenateWhere(selection,
964                        TABLE_BOOKMARKS + "._id=?");
965                selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
966                        new String[] { Long.toString(ContentUris.parseId(uri)) });
967                // fall through
968            }
969            case BOOKMARKS: {
970                return updateBookmarksInTransaction(values, selection, selectionArgs,
971                        callerIsSyncAdapter);
972            }
973
974            case HISTORY_ID: {
975                selection = DatabaseUtils.concatenateWhere(selection, TABLE_HISTORY + "._id=?");
976                selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
977                        new String[] { Long.toString(ContentUris.parseId(uri)) });
978                // fall through
979            }
980            case HISTORY: {
981                return updateHistoryInTransaction(values, selection, selectionArgs);
982            }
983
984            case SYNCSTATE: {
985                return mSyncHelper.update(mDb, values,
986                        appendAccountToSelection(uri, selection), selectionArgs);
987            }
988
989            case SYNCSTATE_ID: {
990                selection = appendAccountToSelection(uri, selection);
991                String selectionWithId =
992                        (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ")
993                        + (selection == null ? "" : " AND (" + selection + ")");
994                return mSyncHelper.update(mDb, values,
995                        selectionWithId, selectionArgs);
996            }
997
998            case IMAGES: {
999                String url = values.getAsString(Images.URL);
1000                if (TextUtils.isEmpty(url)) {
1001                    throw new IllegalArgumentException("Images.URL is required");
1002                }
1003                int count = db.update(TABLE_IMAGES, values, Images.URL + "=?",
1004                        new String[] { url });
1005                if (count == 0) {
1006                    db.insertOrThrow(TABLE_IMAGES, Images.FAVICON, values);
1007                    count = 1;
1008                }
1009                return count;
1010            }
1011        }
1012        throw new UnsupportedOperationException("Unknown update URI " + uri);
1013    }
1014
1015    /**
1016     * Does a query to find the matching bookmarks and updates each one with the provided values.
1017     */
1018    int updateBookmarksInTransaction(ContentValues values, String selection,
1019            String[] selectionArgs, boolean callerIsSyncAdapter) {
1020        int count = 0;
1021        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1022        Cursor cursor = query(Bookmarks.CONTENT_URI,
1023                new String[] { Bookmarks._ID, Bookmarks.VERSION, Bookmarks.URL },
1024                selection, selectionArgs, null);
1025        try {
1026            String[] args = new String[1];
1027            // Mark the bookmark dirty if the caller isn't a sync adapter
1028            if (!callerIsSyncAdapter) {
1029                values.put(Bookmarks.DATE_MODIFIED, System.currentTimeMillis());
1030                values.put(Bookmarks.DIRTY, 1);
1031            }
1032
1033            boolean updatingUrl = values.containsKey(Bookmarks.URL);
1034            String url = null;
1035            if (updatingUrl) {
1036                url = values.getAsString(Bookmarks.URL);
1037            }
1038            ContentValues imageValues = extractImageValues(values, url);
1039
1040            while (cursor.moveToNext()) {
1041                args[0] = cursor.getString(0);
1042                if (!callerIsSyncAdapter) {
1043                    // increase the local version for non-sync changes
1044                    values.put(Bookmarks.VERSION, cursor.getLong(1) + 1);
1045                }
1046                count += db.update(TABLE_BOOKMARKS, values, "_id=?", args);
1047
1048                // Update the images over in their table
1049                if (imageValues != null) {
1050                    if (!updatingUrl) {
1051                        url = cursor.getString(2);
1052                        imageValues.put(Images.URL, url);
1053                    }
1054
1055                    if (!TextUtils.isEmpty(url)) {
1056                        args[0] = url;
1057                        if (db.update(TABLE_IMAGES, imageValues, Images.URL + "=?", args) == 0) {
1058                            db.insert(TABLE_IMAGES, Images.FAVICON, imageValues);
1059                        }
1060                    }
1061                }
1062            }
1063        } finally {
1064            if (cursor != null) cursor.close();
1065        }
1066        return count;
1067    }
1068
1069    /**
1070     * Does a query to find the matching bookmarks and updates each one with the provided values.
1071     */
1072    int updateHistoryInTransaction(ContentValues values, String selection, String[] selectionArgs) {
1073        int count = 0;
1074        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1075        Cursor cursor = query(History.CONTENT_URI,
1076                new String[] { History._ID, History.URL },
1077                selection, selectionArgs, null);
1078        try {
1079            String[] args = new String[1];
1080
1081            boolean updatingUrl = values.containsKey(History.URL);
1082            String url = null;
1083            if (updatingUrl) {
1084                url = values.getAsString(History.URL);
1085            }
1086            ContentValues imageValues = extractImageValues(values, url);
1087
1088            while (cursor.moveToNext()) {
1089                args[0] = cursor.getString(0);
1090                count += db.update(TABLE_HISTORY, values, "_id=?", args);
1091
1092                // Update the images over in their table
1093                if (imageValues != null) {
1094                    if (!updatingUrl) {
1095                        url = cursor.getString(1);
1096                        imageValues.put(Images.URL, url);
1097                    }
1098                    args[0] = url;
1099                    if (db.update(TABLE_IMAGES, imageValues, Images.URL + "=?", args) == 0) {
1100                        db.insert(TABLE_IMAGES, Images.FAVICON, imageValues);
1101                    }
1102                }
1103            }
1104        } finally {
1105            if (cursor != null) cursor.close();
1106        }
1107        return count;
1108    }
1109
1110    String appendAccountToSelection(Uri uri, String selection) {
1111        final String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
1112        final String accountType = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE);
1113
1114        final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType);
1115        if (partialUri) {
1116            // Throw when either account is incomplete
1117            throw new IllegalArgumentException(
1118                    "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE for " + uri);
1119        }
1120
1121        // Accounts are valid by only checking one parameter, since we've
1122        // already ruled out partial accounts.
1123        final boolean validAccount = !TextUtils.isEmpty(accountName);
1124        if (validAccount) {
1125            StringBuilder selectionSb = new StringBuilder(RawContacts.ACCOUNT_NAME + "="
1126                    + DatabaseUtils.sqlEscapeString(accountName) + " AND "
1127                    + RawContacts.ACCOUNT_TYPE + "="
1128                    + DatabaseUtils.sqlEscapeString(accountType));
1129            if (!TextUtils.isEmpty(selection)) {
1130                selectionSb.append(" AND (");
1131                selectionSb.append(selection);
1132                selectionSb.append(')');
1133            }
1134            return selectionSb.toString();
1135        } else {
1136            return selection;
1137        }
1138    }
1139
1140    ContentValues extractImageValues(ContentValues values, String url) {
1141        ContentValues imageValues = null;
1142        // favicon
1143        if (values.containsKey(Bookmarks.FAVICON)) {
1144            imageValues = new ContentValues();
1145            imageValues.put(Images.FAVICON, values.getAsByteArray(Bookmarks.FAVICON));
1146            values.remove(Bookmarks.FAVICON);
1147        }
1148
1149        // thumbnail
1150        if (values.containsKey(Bookmarks.THUMBNAIL)) {
1151            if (imageValues == null) {
1152                imageValues = new ContentValues();
1153            }
1154            imageValues.put(Images.THUMBNAIL, values.getAsByteArray(Bookmarks.THUMBNAIL));
1155            values.remove(Bookmarks.THUMBNAIL);
1156        }
1157
1158        // touch icon
1159        if (values.containsKey(Bookmarks.TOUCH_ICON)) {
1160            if (imageValues == null) {
1161                imageValues = new ContentValues();
1162            }
1163            imageValues.put(Images.TOUCH_ICON, values.getAsByteArray(Bookmarks.TOUCH_ICON));
1164            values.remove(Bookmarks.TOUCH_ICON);
1165        }
1166
1167        if (imageValues != null) {
1168            imageValues.put(Images.URL,  url);
1169        }
1170        return imageValues;
1171    }
1172}
1173