BrowserProvider2.java revision 7dd30cb0bd011e5fbd817fa64863b4c266dd76b7
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.google.common.annotations.VisibleForTesting;
20
21import com.android.browser.BookmarkUtils;
22import com.android.browser.BrowserBookmarksPage;
23import com.android.browser.R;
24import com.android.browser.widget.BookmarkThumbnailWidgetProvider;
25import com.android.common.content.SyncStateContentProviderHelper;
26
27import android.accounts.Account;
28import android.app.SearchManager;
29import android.content.ContentResolver;
30import android.content.ContentUris;
31import android.content.ContentValues;
32import android.content.Context;
33import android.content.Intent;
34import android.content.SharedPreferences;
35import android.content.UriMatcher;
36import android.content.res.Resources;
37import android.content.res.TypedArray;
38import android.database.AbstractCursor;
39import android.database.ContentObserver;
40import android.database.Cursor;
41import android.database.DatabaseUtils;
42import android.database.MatrixCursor;
43import android.database.sqlite.SQLiteDatabase;
44import android.database.sqlite.SQLiteOpenHelper;
45import android.database.sqlite.SQLiteQueryBuilder;
46import android.net.Uri;
47import android.preference.PreferenceManager;
48import android.provider.BaseColumns;
49import android.provider.Browser;
50import android.provider.Browser.BookmarkColumns;
51import android.provider.BrowserContract;
52import android.provider.BrowserContract.Accounts;
53import android.provider.BrowserContract.Bookmarks;
54import android.provider.BrowserContract.ChromeSyncColumns;
55import android.provider.BrowserContract.Combined;
56import android.provider.BrowserContract.History;
57import android.provider.BrowserContract.Images;
58import android.provider.BrowserContract.Searches;
59import android.provider.BrowserContract.Settings;
60import android.provider.BrowserContract.SyncState;
61import android.provider.ContactsContract.RawContacts;
62import android.provider.SyncStateContract;
63import android.text.TextUtils;
64
65import java.io.ByteArrayOutputStream;
66import java.io.IOException;
67import java.io.InputStream;
68import java.util.Arrays;
69import java.util.HashMap;
70
71public class BrowserProvider2 extends SQLiteContentProvider {
72
73    public static final String LEGACY_AUTHORITY = "browser";
74    static final Uri LEGACY_AUTHORITY_URI = new Uri.Builder()
75            .authority(LEGACY_AUTHORITY).scheme("content").build();
76
77    static final String TABLE_BOOKMARKS = "bookmarks";
78    static final String TABLE_HISTORY = "history";
79    static final String TABLE_IMAGES = "images";
80    static final String TABLE_SEARCHES = "searches";
81    static final String TABLE_SYNC_STATE = "syncstate";
82    static final String TABLE_SETTINGS = "settings";
83
84    static final String TABLE_BOOKMARKS_JOIN_IMAGES = "bookmarks LEFT OUTER JOIN images " +
85            "ON bookmarks.url = images." + Images.URL;
86    static final String TABLE_HISTORY_JOIN_IMAGES = "history LEFT OUTER JOIN images " +
87            "ON history.url = images." + Images.URL;
88
89    static final String FORMAT_COMBINED_JOIN_SUBQUERY_JOIN_IMAGES =
90            "history LEFT OUTER JOIN (%s) bookmarks " +
91            "ON history.url = bookmarks.url LEFT OUTER JOIN images " +
92            "ON history.url = images.url_key";
93
94    static final String DEFAULT_SORT_HISTORY = History.DATE_LAST_VISITED + " DESC";
95
96    private static final String[] SUGGEST_PROJECTION = new String[] {
97            Bookmarks._ID,
98            Bookmarks.URL,
99            Bookmarks.TITLE};
100
101    private static final String SUGGEST_SELECTION =
102            "url LIKE ? OR url LIKE ? OR url LIKE ? OR url LIKE ?"
103            + " OR title LIKE ?";
104
105    private static final String IMAGE_PRUNE =
106            "url_key NOT IN (SELECT url FROM bookmarks " +
107            "WHERE url IS NOT NULL AND deleted == 0) AND url_key NOT IN " +
108            "(SELECT url FROM history WHERE url IS NOT NULL)";
109
110    static final int BOOKMARKS = 1000;
111    static final int BOOKMARKS_ID = 1001;
112    static final int BOOKMARKS_FOLDER = 1002;
113    static final int BOOKMARKS_FOLDER_ID = 1003;
114    static final int BOOKMARKS_SUGGESTIONS = 1004;
115    static final int BOOKMARKS_DEFAULT_FOLDER_ID = 1005;
116
117    static final int HISTORY = 2000;
118    static final int HISTORY_ID = 2001;
119
120    static final int SEARCHES = 3000;
121    static final int SEARCHES_ID = 3001;
122
123    static final int SYNCSTATE = 4000;
124    static final int SYNCSTATE_ID = 4001;
125
126    static final int IMAGES = 5000;
127
128    static final int COMBINED = 6000;
129    static final int COMBINED_ID = 6001;
130
131    static final int ACCOUNTS = 7000;
132
133    static final int SETTINGS = 8000;
134
135    static final int LEGACY = 9000;
136    static final int LEGACY_ID = 9001;
137
138    public static final long FIXED_ID_ROOT = 1;
139
140    // Default sort order for unsync'd bookmarks
141    static final String DEFAULT_BOOKMARKS_SORT_ORDER =
142            Bookmarks.IS_FOLDER + " DESC, position ASC, _id ASC";
143
144    // Default sort order for sync'd bookmarks
145    static final String DEFAULT_BOOKMARKS_SORT_ORDER_SYNC = "position ASC, _id ASC";
146
147    static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
148
149    static final HashMap<String, String> ACCOUNTS_PROJECTION_MAP = new HashMap<String, String>();
150    static final HashMap<String, String> BOOKMARKS_PROJECTION_MAP = new HashMap<String, String>();
151    static final HashMap<String, String> OTHER_BOOKMARKS_PROJECTION_MAP =
152            new HashMap<String, String>();
153    static final HashMap<String, String> HISTORY_PROJECTION_MAP = new HashMap<String, String>();
154    static final HashMap<String, String> SYNC_STATE_PROJECTION_MAP = new HashMap<String, String>();
155    static final HashMap<String, String> IMAGES_PROJECTION_MAP = new HashMap<String, String>();
156    static final HashMap<String, String> COMBINED_HISTORY_PROJECTION_MAP = new HashMap<String, String>();
157    static final HashMap<String, String> COMBINED_BOOKMARK_PROJECTION_MAP = new HashMap<String, String>();
158    static final HashMap<String, String> SEARCHES_PROJECTION_MAP = new HashMap<String, String>();
159    static final HashMap<String, String> SETTINGS_PROJECTION_MAP = new HashMap<String, String>();
160
161    static {
162        final UriMatcher matcher = URI_MATCHER;
163        final String authority = BrowserContract.AUTHORITY;
164        matcher.addURI(authority, "accounts", ACCOUNTS);
165        matcher.addURI(authority, "bookmarks", BOOKMARKS);
166        matcher.addURI(authority, "bookmarks/#", BOOKMARKS_ID);
167        matcher.addURI(authority, "bookmarks/folder", BOOKMARKS_FOLDER);
168        matcher.addURI(authority, "bookmarks/folder/#", BOOKMARKS_FOLDER_ID);
169        matcher.addURI(authority, "bookmarks/folder/id", BOOKMARKS_DEFAULT_FOLDER_ID);
170        matcher.addURI(authority,
171                SearchManager.SUGGEST_URI_PATH_QUERY,
172                BOOKMARKS_SUGGESTIONS);
173        matcher.addURI(authority,
174                "bookmarks/" + SearchManager.SUGGEST_URI_PATH_QUERY,
175                BOOKMARKS_SUGGESTIONS);
176        matcher.addURI(authority, "history", HISTORY);
177        matcher.addURI(authority, "history/#", HISTORY_ID);
178        matcher.addURI(authority, "searches", SEARCHES);
179        matcher.addURI(authority, "searches/#", SEARCHES_ID);
180        matcher.addURI(authority, "syncstate", SYNCSTATE);
181        matcher.addURI(authority, "syncstate/#", SYNCSTATE_ID);
182        matcher.addURI(authority, "images", IMAGES);
183        matcher.addURI(authority, "combined", COMBINED);
184        matcher.addURI(authority, "combined/#", COMBINED_ID);
185        matcher.addURI(authority, "settings", SETTINGS);
186
187        // Legacy
188        matcher.addURI(LEGACY_AUTHORITY, "searches", SEARCHES);
189        matcher.addURI(LEGACY_AUTHORITY, "searches/#", SEARCHES_ID);
190        matcher.addURI(LEGACY_AUTHORITY, "bookmarks", LEGACY);
191        matcher.addURI(LEGACY_AUTHORITY, "bookmarks/#", LEGACY_ID);
192        matcher.addURI(LEGACY_AUTHORITY,
193                SearchManager.SUGGEST_URI_PATH_QUERY,
194                BOOKMARKS_SUGGESTIONS);
195        matcher.addURI(LEGACY_AUTHORITY,
196                "bookmarks/" + SearchManager.SUGGEST_URI_PATH_QUERY,
197                BOOKMARKS_SUGGESTIONS);
198
199        // Projection maps
200        HashMap<String, String> map;
201
202        // Accounts
203        map = ACCOUNTS_PROJECTION_MAP;
204        map.put(Accounts.ACCOUNT_TYPE, Accounts.ACCOUNT_TYPE);
205        map.put(Accounts.ACCOUNT_NAME, Accounts.ACCOUNT_NAME);
206
207        // Bookmarks
208        map = BOOKMARKS_PROJECTION_MAP;
209        map.put(Bookmarks._ID, qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID));
210        map.put(Bookmarks.TITLE, Bookmarks.TITLE);
211        map.put(Bookmarks.URL, Bookmarks.URL);
212        map.put(Bookmarks.FAVICON, Bookmarks.FAVICON);
213        map.put(Bookmarks.THUMBNAIL, Bookmarks.THUMBNAIL);
214        map.put(Bookmarks.TOUCH_ICON, Bookmarks.TOUCH_ICON);
215        map.put(Bookmarks.IS_FOLDER, Bookmarks.IS_FOLDER);
216        map.put(Bookmarks.PARENT, Bookmarks.PARENT);
217        map.put(Bookmarks.POSITION, Bookmarks.POSITION);
218        map.put(Bookmarks.INSERT_AFTER, Bookmarks.INSERT_AFTER);
219        map.put(Bookmarks.IS_DELETED, Bookmarks.IS_DELETED);
220        map.put(Bookmarks.ACCOUNT_NAME, Bookmarks.ACCOUNT_NAME);
221        map.put(Bookmarks.ACCOUNT_TYPE, Bookmarks.ACCOUNT_TYPE);
222        map.put(Bookmarks.SOURCE_ID, Bookmarks.SOURCE_ID);
223        map.put(Bookmarks.VERSION, Bookmarks.VERSION);
224        map.put(Bookmarks.DATE_CREATED, Bookmarks.DATE_CREATED);
225        map.put(Bookmarks.DATE_MODIFIED, Bookmarks.DATE_MODIFIED);
226        map.put(Bookmarks.DIRTY, Bookmarks.DIRTY);
227        map.put(Bookmarks.SYNC1, Bookmarks.SYNC1);
228        map.put(Bookmarks.SYNC2, Bookmarks.SYNC2);
229        map.put(Bookmarks.SYNC3, Bookmarks.SYNC3);
230        map.put(Bookmarks.SYNC4, Bookmarks.SYNC4);
231        map.put(Bookmarks.SYNC5, Bookmarks.SYNC5);
232        map.put(Bookmarks.PARENT_SOURCE_ID, "(SELECT " + Bookmarks.SOURCE_ID +
233                " FROM " + TABLE_BOOKMARKS + " A WHERE " +
234                "A." + Bookmarks._ID + "=" + TABLE_BOOKMARKS + "." + Bookmarks.PARENT +
235                ") AS " + Bookmarks.PARENT_SOURCE_ID);
236        map.put(Bookmarks.INSERT_AFTER_SOURCE_ID, "(SELECT " + Bookmarks.SOURCE_ID +
237                " FROM " + TABLE_BOOKMARKS + " A WHERE " +
238                "A." + Bookmarks._ID + "=" + TABLE_BOOKMARKS + "." + Bookmarks.INSERT_AFTER +
239                ") AS " + Bookmarks.INSERT_AFTER_SOURCE_ID);
240
241        // Other bookmarks
242        OTHER_BOOKMARKS_PROJECTION_MAP.putAll(BOOKMARKS_PROJECTION_MAP);
243        OTHER_BOOKMARKS_PROJECTION_MAP.put(Bookmarks.POSITION,
244                Long.toString(Long.MAX_VALUE) + " AS " + Bookmarks.POSITION);
245
246        // History
247        map = HISTORY_PROJECTION_MAP;
248        map.put(History._ID, qualifyColumn(TABLE_HISTORY, History._ID));
249        map.put(History.TITLE, History.TITLE);
250        map.put(History.URL, History.URL);
251        map.put(History.FAVICON, History.FAVICON);
252        map.put(History.THUMBNAIL, History.THUMBNAIL);
253        map.put(History.TOUCH_ICON, History.TOUCH_ICON);
254        map.put(History.DATE_CREATED, History.DATE_CREATED);
255        map.put(History.DATE_LAST_VISITED, History.DATE_LAST_VISITED);
256        map.put(History.VISITS, History.VISITS);
257        map.put(History.USER_ENTERED, History.USER_ENTERED);
258
259        // Sync state
260        map = SYNC_STATE_PROJECTION_MAP;
261        map.put(SyncState._ID, SyncState._ID);
262        map.put(SyncState.ACCOUNT_NAME, SyncState.ACCOUNT_NAME);
263        map.put(SyncState.ACCOUNT_TYPE, SyncState.ACCOUNT_TYPE);
264        map.put(SyncState.DATA, SyncState.DATA);
265
266        // Images
267        map = IMAGES_PROJECTION_MAP;
268        map.put(Images.URL, Images.URL);
269        map.put(Images.FAVICON, Images.FAVICON);
270        map.put(Images.THUMBNAIL, Images.THUMBNAIL);
271        map.put(Images.TOUCH_ICON, Images.TOUCH_ICON);
272
273        // Combined history half
274        map = COMBINED_HISTORY_PROJECTION_MAP;
275        map.put(Combined._ID, bookmarkOrHistoryColumn(Combined._ID));
276        map.put(Combined.TITLE, bookmarkOrHistoryColumn(Combined.TITLE));
277        map.put(Combined.URL, qualifyColumn(TABLE_HISTORY, Combined.URL));
278        map.put(Combined.DATE_CREATED, qualifyColumn(TABLE_HISTORY, Combined.DATE_CREATED));
279        map.put(Combined.DATE_LAST_VISITED, Combined.DATE_LAST_VISITED);
280        map.put(Combined.IS_BOOKMARK, "CASE WHEN " +
281                TABLE_BOOKMARKS + "." + Bookmarks._ID +
282                " IS NOT NULL THEN 1 ELSE 0 END AS " + Combined.IS_BOOKMARK);
283        map.put(Combined.VISITS, Combined.VISITS);
284        map.put(Combined.FAVICON, Combined.FAVICON);
285        map.put(Combined.THUMBNAIL, Combined.THUMBNAIL);
286        map.put(Combined.TOUCH_ICON, Combined.TOUCH_ICON);
287        map.put(Combined.USER_ENTERED, "NULL AS " + Combined.USER_ENTERED);
288
289        // Combined bookmark half
290        map = COMBINED_BOOKMARK_PROJECTION_MAP;
291        map.put(Combined._ID, Combined._ID);
292        map.put(Combined.TITLE, Combined.TITLE);
293        map.put(Combined.URL, Combined.URL);
294        map.put(Combined.DATE_CREATED, Combined.DATE_CREATED);
295        map.put(Combined.DATE_LAST_VISITED, "NULL AS " + Combined.DATE_LAST_VISITED);
296        map.put(Combined.IS_BOOKMARK, "1 AS " + Combined.IS_BOOKMARK);
297        map.put(Combined.VISITS, "0 AS " + Combined.VISITS);
298        map.put(Combined.FAVICON, Combined.FAVICON);
299        map.put(Combined.THUMBNAIL, Combined.THUMBNAIL);
300        map.put(Combined.TOUCH_ICON, Combined.TOUCH_ICON);
301        map.put(Combined.USER_ENTERED, "NULL AS " + Combined.USER_ENTERED);
302
303        // Searches
304        map = SEARCHES_PROJECTION_MAP;
305        map.put(Searches._ID, Searches._ID);
306        map.put(Searches.SEARCH, Searches.SEARCH);
307        map.put(Searches.DATE, Searches.DATE);
308
309        // Settings
310        map = SETTINGS_PROJECTION_MAP;
311        map.put(Settings.KEY, Settings.KEY);
312        map.put(Settings.VALUE, Settings.VALUE);
313    }
314
315    static final String bookmarkOrHistoryColumn(String column) {
316        return "CASE WHEN bookmarks." + column + " IS NOT NULL THEN " +
317                "bookmarks." + column + " ELSE history." + column + " END AS " + column;
318    }
319
320    static final String qualifyColumn(String table, String column) {
321        return table + "." + column + " AS " + column;
322    }
323
324    DatabaseHelper mOpenHelper;
325    SyncStateContentProviderHelper mSyncHelper = new SyncStateContentProviderHelper();
326    // This is so provider tests can intercept widget updating
327    ContentObserver mWidgetObserver = null;
328
329    final class DatabaseHelper extends SQLiteOpenHelper {
330        static final String DATABASE_NAME = "browser2.db";
331        static final int DATABASE_VERSION = 26;
332        public DatabaseHelper(Context context) {
333            super(context, DATABASE_NAME, null, DATABASE_VERSION);
334        }
335
336        @Override
337        public void onCreate(SQLiteDatabase db) {
338            db.execSQL("CREATE TABLE " + TABLE_BOOKMARKS + "(" +
339                    Bookmarks._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
340                    Bookmarks.TITLE + " TEXT," +
341                    Bookmarks.URL + " TEXT," +
342                    Bookmarks.IS_FOLDER + " INTEGER NOT NULL DEFAULT 0," +
343                    Bookmarks.PARENT + " INTEGER," +
344                    Bookmarks.POSITION + " INTEGER NOT NULL," +
345                    Bookmarks.INSERT_AFTER + " INTEGER," +
346                    Bookmarks.IS_DELETED + " INTEGER NOT NULL DEFAULT 0," +
347                    Bookmarks.ACCOUNT_NAME + " TEXT," +
348                    Bookmarks.ACCOUNT_TYPE + " TEXT," +
349                    Bookmarks.SOURCE_ID + " TEXT," +
350                    Bookmarks.VERSION + " INTEGER NOT NULL DEFAULT 1," +
351                    Bookmarks.DATE_CREATED + " INTEGER," +
352                    Bookmarks.DATE_MODIFIED + " INTEGER," +
353                    Bookmarks.DIRTY + " INTEGER NOT NULL DEFAULT 0," +
354                    Bookmarks.SYNC1 + " TEXT," +
355                    Bookmarks.SYNC2 + " TEXT," +
356                    Bookmarks.SYNC3 + " TEXT," +
357                    Bookmarks.SYNC4 + " TEXT," +
358                    Bookmarks.SYNC5 + " TEXT" +
359                    ");");
360
361            // TODO indices
362
363            db.execSQL("CREATE TABLE " + TABLE_HISTORY + "(" +
364                    History._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
365                    History.TITLE + " TEXT," +
366                    History.URL + " TEXT NOT NULL," +
367                    History.DATE_CREATED + " INTEGER," +
368                    History.DATE_LAST_VISITED + " INTEGER," +
369                    History.VISITS + " INTEGER NOT NULL DEFAULT 0," +
370                    History.USER_ENTERED + " INTEGER" +
371                    ");");
372
373            db.execSQL("CREATE TABLE " + TABLE_IMAGES + " (" +
374                    Images.URL + " TEXT UNIQUE NOT NULL," +
375                    Images.FAVICON + " BLOB," +
376                    Images.THUMBNAIL + " BLOB," +
377                    Images.TOUCH_ICON + " BLOB" +
378                    ");");
379            db.execSQL("CREATE INDEX imagesUrlIndex ON " + TABLE_IMAGES +
380                    "(" + Images.URL + ")");
381
382            db.execSQL("CREATE TABLE " + TABLE_SEARCHES + " (" +
383                    Searches._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
384                    Searches.SEARCH + " TEXT," +
385                    Searches.DATE + " LONG" +
386                    ");");
387
388            db.execSQL("CREATE TABLE " + TABLE_SETTINGS + " (" +
389                    Settings.KEY + " TEXT PRIMARY KEY," +
390                    Settings.VALUE + " TEXT NOT NULL" +
391                    ");");
392
393            mSyncHelper.createDatabase(db);
394
395            createDefaultBookmarks(db);
396        }
397
398        @Override
399        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
400            // TODO write upgrade logic
401            db.execSQL("DROP VIEW IF EXISTS combined");
402            if (oldVersion < 25) {
403                db.execSQL("DROP TABLE IF EXISTS " + TABLE_BOOKMARKS);
404                db.execSQL("DROP TABLE IF EXISTS " + TABLE_HISTORY);
405                db.execSQL("DROP TABLE IF EXISTS " + TABLE_SEARCHES);
406                db.execSQL("DROP TABLE IF EXISTS " + TABLE_IMAGES);
407                db.execSQL("DROP TABLE IF EXISTS " + TABLE_SETTINGS);
408                mSyncHelper.onAccountsChanged(db, new Account[] {}); // remove all sync info
409                onCreate(db);
410            }
411        }
412
413        @Override
414        public void onOpen(SQLiteDatabase db) {
415            db.enableWriteAheadLogging();
416            mSyncHelper.onDatabaseOpened(db);
417        }
418
419        private void createDefaultBookmarks(SQLiteDatabase db) {
420            ContentValues values = new ContentValues();
421            // TODO figure out how to deal with localization for the defaults
422
423            // Bookmarks folder
424            values.put(Bookmarks._ID, FIXED_ID_ROOT);
425            values.put(ChromeSyncColumns.SERVER_UNIQUE, ChromeSyncColumns.FOLDER_NAME_BOOKMARKS);
426            values.put(Bookmarks.TITLE, "Bookmarks");
427            values.putNull(Bookmarks.PARENT);
428            values.put(Bookmarks.POSITION, 0);
429            values.put(Bookmarks.IS_FOLDER, true);
430            values.put(Bookmarks.DIRTY, true);
431            db.insertOrThrow(TABLE_BOOKMARKS, null, values);
432
433            addDefaultBookmarks(db, FIXED_ID_ROOT);
434        }
435
436        private void addDefaultBookmarks(SQLiteDatabase db, long parentId) {
437            Resources res = getContext().getResources();
438            final CharSequence[] bookmarks = res.getTextArray(
439                    R.array.bookmarks);
440            int size = bookmarks.length;
441            TypedArray preloads = res.obtainTypedArray(R.array.bookmark_preloads);
442            try {
443                String parent = Long.toString(parentId);
444                String now = Long.toString(System.currentTimeMillis());
445                for (int i = 0; i < size; i = i + 2) {
446                    CharSequence bookmarkDestination = replaceSystemPropertyInString(getContext(),
447                            bookmarks[i + 1]);
448                    db.execSQL("INSERT INTO bookmarks (" +
449                            Bookmarks.TITLE + ", " +
450                            Bookmarks.URL + ", " +
451                            Bookmarks.IS_FOLDER + "," +
452                            Bookmarks.PARENT + "," +
453                            Bookmarks.POSITION + "," +
454                            Bookmarks.DATE_CREATED +
455                        ") VALUES (" +
456                            "'" + bookmarks[i] + "', " +
457                            "'" + bookmarkDestination + "', " +
458                            "0," +
459                            parent + "," +
460                            Integer.toString(i) + "," +
461                            now +
462                            ");");
463
464                    int faviconId = preloads.getResourceId(i, 0);
465                    int thumbId = preloads.getResourceId(i + 1, 0);
466                    byte[] thumb = null, favicon = null;
467                    try {
468                        thumb = readRaw(res, thumbId);
469                    } catch (IOException e) {
470                    }
471                    try {
472                        favicon = readRaw(res, faviconId);
473                    } catch (IOException e) {
474                    }
475                    if (thumb != null || favicon != null) {
476                        ContentValues imageValues = new ContentValues();
477                        imageValues.put(Images.URL, bookmarkDestination.toString());
478                        if (favicon != null) {
479                            imageValues.put(Images.FAVICON, favicon);
480                        }
481                        if (thumb != null) {
482                            imageValues.put(Images.THUMBNAIL, thumb);
483                        }
484                        db.insert(TABLE_IMAGES, Images.FAVICON, imageValues);
485                    }
486                }
487            } catch (ArrayIndexOutOfBoundsException e) {
488            }
489        }
490
491        private byte[] readRaw(Resources res, int id) throws IOException {
492            if (id == 0) {
493                return null;
494            }
495            InputStream is = res.openRawResource(id);
496            try {
497                ByteArrayOutputStream bos = new ByteArrayOutputStream();
498                byte[] buf = new byte[4096];
499                int read;
500                while ((read = is.read(buf)) > 0) {
501                    bos.write(buf, 0, read);
502                }
503                bos.flush();
504                return bos.toByteArray();
505            } finally {
506                is.close();
507            }
508        }
509
510        // XXX: This is a major hack to remove our dependency on gsf constants and
511        // its content provider. http://b/issue?id=2425179
512        private String getClientId(ContentResolver cr) {
513            String ret = "android-google";
514            Cursor c = null;
515            try {
516                c = cr.query(Uri.parse("content://com.google.settings/partner"),
517                        new String[] { "value" }, "name='client_id'", null, null);
518                if (c != null && c.moveToNext()) {
519                    ret = c.getString(0);
520                }
521            } catch (RuntimeException ex) {
522                // fall through to return the default
523            } finally {
524                if (c != null) {
525                    c.close();
526                }
527            }
528            return ret;
529        }
530
531        private CharSequence replaceSystemPropertyInString(Context context, CharSequence srcString) {
532            StringBuffer sb = new StringBuffer();
533            int lastCharLoc = 0;
534
535            final String client_id = getClientId(context.getContentResolver());
536
537            for (int i = 0; i < srcString.length(); ++i) {
538                char c = srcString.charAt(i);
539                if (c == '{') {
540                    sb.append(srcString.subSequence(lastCharLoc, i));
541                    lastCharLoc = i;
542              inner:
543                    for (int j = i; j < srcString.length(); ++j) {
544                        char k = srcString.charAt(j);
545                        if (k == '}') {
546                            String propertyKeyValue = srcString.subSequence(i + 1, j).toString();
547                            if (propertyKeyValue.equals("CLIENT_ID")) {
548                                sb.append(client_id);
549                            } else {
550                                sb.append("unknown");
551                            }
552                            lastCharLoc = j + 1;
553                            i = j;
554                            break inner;
555                        }
556                    }
557                }
558            }
559            if (srcString.length() - lastCharLoc > 0) {
560                // Put on the tail, if there is one
561                sb.append(srcString.subSequence(lastCharLoc, srcString.length()));
562            }
563            return sb;
564        }
565    }
566
567    @Override
568    public SQLiteOpenHelper getDatabaseHelper(Context context) {
569        synchronized (this) {
570            if (mOpenHelper == null) {
571                mOpenHelper = new DatabaseHelper(context);
572            }
573            return mOpenHelper;
574        }
575    }
576
577    @Override
578    public boolean isCallerSyncAdapter(Uri uri) {
579        return uri.getBooleanQueryParameter(BrowserContract.CALLER_IS_SYNCADAPTER, false);
580    }
581
582    @VisibleForTesting
583    public void setWidgetObserver(ContentObserver obs) {
584        mWidgetObserver = obs;
585    }
586
587    void refreshWidgets() {
588        if (mWidgetObserver == null) {
589            BookmarkThumbnailWidgetProvider.refreshWidgets(getContext());
590        } else {
591            mWidgetObserver.dispatchChange(false);
592        }
593    }
594
595    @Override
596    public String getType(Uri uri) {
597        final int match = URI_MATCHER.match(uri);
598        switch (match) {
599            case LEGACY:
600            case BOOKMARKS:
601                return Bookmarks.CONTENT_TYPE;
602            case LEGACY_ID:
603            case BOOKMARKS_ID:
604                return Bookmarks.CONTENT_ITEM_TYPE;
605            case HISTORY:
606                return History.CONTENT_TYPE;
607            case HISTORY_ID:
608                return History.CONTENT_ITEM_TYPE;
609            case SEARCHES:
610                return Searches.CONTENT_TYPE;
611            case SEARCHES_ID:
612                return Searches.CONTENT_ITEM_TYPE;
613        }
614        return null;
615    }
616
617    boolean isNullAccount(String account) {
618        if (account == null) return true;
619        account = account.trim();
620        return account.length() == 0 || account.equals("null");
621    }
622
623    Object[] getSelectionWithAccounts(Uri uri, String selection, String[] selectionArgs) {
624        // Look for account info
625        String accountType = uri.getQueryParameter(Bookmarks.PARAM_ACCOUNT_TYPE);
626        String accountName = uri.getQueryParameter(Bookmarks.PARAM_ACCOUNT_NAME);
627        boolean hasAccounts = false;
628        if (accountType != null && accountName != null) {
629            if (!isNullAccount(accountType) && !isNullAccount(accountName)) {
630                selection = DatabaseUtils.concatenateWhere(selection,
631                        Bookmarks.ACCOUNT_TYPE + "=? AND " + Bookmarks.ACCOUNT_NAME + "=? ");
632                selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
633                        new String[] { accountType, accountName });
634                hasAccounts = true;
635            } else {
636                selection = DatabaseUtils.concatenateWhere(selection,
637                        Bookmarks.ACCOUNT_NAME + " IS NULL AND " +
638                        Bookmarks.ACCOUNT_TYPE + " IS NULL");
639            }
640        }
641        return new Object[] { selection, selectionArgs, hasAccounts };
642    }
643
644    @Override
645    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
646            String sortOrder) {
647        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
648        final int match = URI_MATCHER.match(uri);
649        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
650        String limit = uri.getQueryParameter(BrowserContract.PARAM_LIMIT);
651        switch (match) {
652            case ACCOUNTS: {
653                qb.setTables(TABLE_BOOKMARKS);
654                qb.setProjectionMap(ACCOUNTS_PROJECTION_MAP);
655                qb.setDistinct(true);
656                qb.appendWhere(Bookmarks.ACCOUNT_NAME + " IS NOT NULL");
657                break;
658            }
659
660            case BOOKMARKS_FOLDER_ID:
661            case BOOKMARKS_ID:
662            case BOOKMARKS: {
663                // Only show deleted bookmarks if requested to do so
664                if (!uri.getBooleanQueryParameter(Bookmarks.QUERY_PARAMETER_SHOW_DELETED, false)) {
665                    selection = DatabaseUtils.concatenateWhere(
666                            Bookmarks.IS_DELETED + "=0", selection);
667                }
668
669                if (match == BOOKMARKS_ID) {
670                    // Tack on the ID of the specific bookmark requested
671                    selection = DatabaseUtils.concatenateWhere(selection,
672                            TABLE_BOOKMARKS + "." + Bookmarks._ID + "=?");
673                    selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
674                            new String[] { Long.toString(ContentUris.parseId(uri)) });
675                } else if (match == BOOKMARKS_FOLDER_ID) {
676                    // Tack on the ID of the specific folder requested
677                    selection = DatabaseUtils.concatenateWhere(selection,
678                            TABLE_BOOKMARKS + "." + Bookmarks.PARENT + "=?");
679                    selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
680                            new String[] { Long.toString(ContentUris.parseId(uri)) });
681                }
682
683                Object[] withAccount = getSelectionWithAccounts(uri, selection, selectionArgs);
684                selection = (String) withAccount[0];
685                selectionArgs = (String[]) withAccount[1];
686                boolean hasAccounts = (Boolean) withAccount[2];
687
688                // Set a default sort order if one isn't specified
689                if (TextUtils.isEmpty(sortOrder)) {
690                    if (hasAccounts) {
691                        sortOrder = DEFAULT_BOOKMARKS_SORT_ORDER_SYNC;
692                    } else {
693                        sortOrder = DEFAULT_BOOKMARKS_SORT_ORDER;
694                    }
695                }
696
697                qb.setProjectionMap(BOOKMARKS_PROJECTION_MAP);
698                qb.setTables(TABLE_BOOKMARKS_JOIN_IMAGES);
699                break;
700            }
701
702            case BOOKMARKS_FOLDER: {
703                // Look for an account
704                boolean useAccount = false;
705                String accountType = uri.getQueryParameter(Bookmarks.PARAM_ACCOUNT_TYPE);
706                String accountName = uri.getQueryParameter(Bookmarks.PARAM_ACCOUNT_NAME);
707                if (!isNullAccount(accountType) && !isNullAccount(accountName)) {
708                    useAccount = true;
709                }
710
711                qb.setTables(TABLE_BOOKMARKS_JOIN_IMAGES);
712                String[] args;
713                String query;
714                // Set a default sort order if one isn't specified
715                if (TextUtils.isEmpty(sortOrder)) {
716                    if (useAccount) {
717                        sortOrder = DEFAULT_BOOKMARKS_SORT_ORDER_SYNC;
718                    } else {
719                        sortOrder = DEFAULT_BOOKMARKS_SORT_ORDER;
720                    }
721                }
722                if (!useAccount) {
723                    qb.setProjectionMap(BOOKMARKS_PROJECTION_MAP);
724                    String where = Bookmarks.PARENT + "=? AND " + Bookmarks.IS_DELETED + "=0";
725                    where = DatabaseUtils.concatenateWhere(where, selection);
726                    args = new String[] { Long.toString(FIXED_ID_ROOT) };
727                    if (selectionArgs != null) {
728                        args = DatabaseUtils.appendSelectionArgs(args, selectionArgs);
729                    }
730                    query = qb.buildQuery(projection, where, null, null, sortOrder, null);
731                } else {
732                    qb.setProjectionMap(BOOKMARKS_PROJECTION_MAP);
733                    String where = Bookmarks.ACCOUNT_TYPE + "=? AND " +
734                            Bookmarks.ACCOUNT_NAME + "=? " +
735                            "AND parent = " +
736                            "(SELECT _id FROM " + TABLE_BOOKMARKS + " WHERE " +
737                            ChromeSyncColumns.SERVER_UNIQUE + "=" +
738                            "'" + ChromeSyncColumns.FOLDER_NAME_BOOKMARKS_BAR + "' " +
739                            "AND account_type = ? AND account_name = ?) " +
740                            "AND " + Bookmarks.IS_DELETED + "=0";
741                    where = DatabaseUtils.concatenateWhere(where, selection);
742                    String bookmarksBarQuery = qb.buildQuery(projection,
743                            where, null, null, null, null);
744                    args = new String[] {accountType, accountName,
745                            accountType, accountName};
746                    if (selectionArgs != null) {
747                        args = DatabaseUtils.appendSelectionArgs(args, selectionArgs);
748                    }
749
750                    where = Bookmarks.ACCOUNT_TYPE + "=? AND " + Bookmarks.ACCOUNT_NAME + "=?" +
751                            " AND " + ChromeSyncColumns.SERVER_UNIQUE + "=?";
752                    where = DatabaseUtils.concatenateWhere(where, selection);
753                    qb.setProjectionMap(OTHER_BOOKMARKS_PROJECTION_MAP);
754                    String otherBookmarksQuery = qb.buildQuery(projection,
755                            where, null, null, null, null);
756
757                    query = qb.buildUnionQuery(
758                            new String[] { bookmarksBarQuery, otherBookmarksQuery },
759                            sortOrder, limit);
760
761                    args = DatabaseUtils.appendSelectionArgs(args, new String[] {
762                            accountType, accountName, ChromeSyncColumns.FOLDER_NAME_OTHER_BOOKMARKS,
763                            });
764                    if (selectionArgs != null) {
765                        args = DatabaseUtils.appendSelectionArgs(args, selectionArgs);
766                    }
767                }
768
769                Cursor cursor = db.rawQuery(query, args);
770                if (cursor != null) {
771                    cursor.setNotificationUri(getContext().getContentResolver(),
772                            BrowserContract.AUTHORITY_URI);
773                }
774                return cursor;
775            }
776
777            case BOOKMARKS_DEFAULT_FOLDER_ID: {
778                String accountName = uri.getQueryParameter(Bookmarks.PARAM_ACCOUNT_NAME);
779                String accountType = uri.getQueryParameter(Bookmarks.PARAM_ACCOUNT_TYPE);
780                long id = queryDefaultFolderId(accountName, accountType);
781                MatrixCursor c = new MatrixCursor(new String[] {Bookmarks._ID});
782                c.newRow().add(id);
783                return c;
784            }
785
786            case BOOKMARKS_SUGGESTIONS: {
787                return doSuggestQuery(selection, selectionArgs, limit);
788            }
789
790            case HISTORY_ID: {
791                selection = DatabaseUtils.concatenateWhere(selection, TABLE_HISTORY + "._id=?");
792                selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
793                        new String[] { Long.toString(ContentUris.parseId(uri)) });
794                // fall through
795            }
796            case HISTORY: {
797                filterSearchClient(selectionArgs);
798                if (sortOrder == null) {
799                    sortOrder = DEFAULT_SORT_HISTORY;
800                }
801                qb.setProjectionMap(HISTORY_PROJECTION_MAP);
802                qb.setTables(TABLE_HISTORY_JOIN_IMAGES);
803                break;
804            }
805
806            case SEARCHES_ID: {
807                selection = DatabaseUtils.concatenateWhere(selection, TABLE_SEARCHES + "._id=?");
808                selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
809                        new String[] { Long.toString(ContentUris.parseId(uri)) });
810                // fall through
811            }
812            case SEARCHES: {
813                qb.setTables(TABLE_SEARCHES);
814                qb.setProjectionMap(SEARCHES_PROJECTION_MAP);
815                break;
816            }
817
818            case SYNCSTATE: {
819                return mSyncHelper.query(db, projection, selection, selectionArgs, sortOrder);
820            }
821
822            case SYNCSTATE_ID: {
823                selection = appendAccountToSelection(uri, selection);
824                String selectionWithId =
825                        (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ")
826                        + (selection == null ? "" : " AND (" + selection + ")");
827                return mSyncHelper.query(db, projection, selectionWithId, selectionArgs, sortOrder);
828            }
829
830            case IMAGES: {
831                qb.setTables(TABLE_IMAGES);
832                qb.setProjectionMap(IMAGES_PROJECTION_MAP);
833                break;
834            }
835
836            case LEGACY_ID:
837            case COMBINED_ID: {
838                selection = DatabaseUtils.concatenateWhere(
839                        selection, Combined._ID + " = CAST(? AS INTEGER)");
840                selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
841                        new String[] { Long.toString(ContentUris.parseId(uri)) });
842                // fall through
843            }
844            case LEGACY:
845            case COMBINED: {
846                if ((match == LEGACY || match == LEGACY_ID)
847                        && projection == null) {
848                    projection = Browser.HISTORY_PROJECTION;
849                }
850                if (match == LEGACY) {
851                    uri = BookmarkUtils.addAccountInfo(getContext(),
852                            uri.buildUpon()).build();
853                }
854                String[] args = createCombinedQuery(uri, projection, qb);
855                if (selectionArgs == null) {
856                    selectionArgs = args;
857                } else {
858                    selectionArgs = DatabaseUtils.appendSelectionArgs(args, selectionArgs);
859                }
860                break;
861            }
862
863            case SETTINGS: {
864                qb.setTables(TABLE_SETTINGS);
865                qb.setProjectionMap(SETTINGS_PROJECTION_MAP);
866                break;
867            }
868
869            default: {
870                throw new UnsupportedOperationException("Unknown URL " + uri.toString());
871            }
872        }
873
874        Cursor cursor = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder,
875                limit);
876        cursor.setNotificationUri(getContext().getContentResolver(), BrowserContract.AUTHORITY_URI);
877        return cursor;
878    }
879
880    private Cursor doSuggestQuery(String selection, String[] selectionArgs, String limit) {
881        if (selectionArgs[0] == null) {
882            return null;
883        } else {
884            String like = selectionArgs[0] + "%";
885            if (selectionArgs[0].startsWith("http")
886                    || selectionArgs[0].startsWith("file")) {
887                selectionArgs[0] = like;
888            } else {
889                selectionArgs = new String[5];
890                selectionArgs[0] = "http://" + like;
891                selectionArgs[1] = "http://www." + like;
892                selectionArgs[2] = "https://" + like;
893                selectionArgs[3] = "https://www." + like;
894                // To match against titles.
895                selectionArgs[4] = like;
896                selection = SUGGEST_SELECTION;
897            }
898        }
899        selection = DatabaseUtils.concatenateWhere(selection,
900                Bookmarks.IS_DELETED + "=0 AND " + Bookmarks.IS_FOLDER + "=0");
901
902        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
903        String accountType = prefs.getString(BrowserBookmarksPage.PREF_ACCOUNT_TYPE, null);
904        String accountName = prefs.getString(BrowserBookmarksPage.PREF_ACCOUNT_NAME, null);
905        if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
906            selection = DatabaseUtils.concatenateWhere(selection,
907                    Bookmarks.ACCOUNT_TYPE + "=? AND " + Bookmarks.ACCOUNT_NAME + "=? ");
908            selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
909                    new String[] { accountType, accountName });
910        } else {
911            selection = DatabaseUtils.concatenateWhere(selection,
912                    Bookmarks.ACCOUNT_TYPE + " IS NULL AND " +
913                    Bookmarks.ACCOUNT_NAME + " IS NULL ");
914        }
915        Cursor c = mOpenHelper.getReadableDatabase().query(TABLE_BOOKMARKS,
916                SUGGEST_PROJECTION, selection, selectionArgs, null, null,
917                DEFAULT_BOOKMARKS_SORT_ORDER, null);
918
919        return new SuggestionsCursor(c);
920    }
921
922    private String[] createCombinedQuery(
923            Uri uri, String[] projection, SQLiteQueryBuilder qb) {
924        String[] args = null;
925        StringBuilder whereBuilder = new StringBuilder(128);
926        whereBuilder.append(Bookmarks.IS_DELETED);
927        whereBuilder.append(" = 0");
928        // Look for account info
929        Object[] withAccount = getSelectionWithAccounts(uri, null, null);
930        String selection = (String) withAccount[0];
931        String[] selectionArgs = (String[]) withAccount[1];
932        if (selection != null) {
933            whereBuilder.append(" AND " + selection);
934            if (selectionArgs != null) {
935                // We use the selection twice, hence we need to duplicate the args
936                args = new String[selectionArgs.length * 2];
937                System.arraycopy(selectionArgs, 0, args, 0, selectionArgs.length);
938                System.arraycopy(selectionArgs, 0, args, selectionArgs.length,
939                        selectionArgs.length);
940            }
941        }
942        String where = whereBuilder.toString();
943        // Build the bookmark subquery for history union subquery
944        qb.setTables(TABLE_BOOKMARKS);
945        String subQuery = qb.buildQuery(null, where, null, null, null, null);
946        // Build the history union subquery
947        qb.setTables(String.format(FORMAT_COMBINED_JOIN_SUBQUERY_JOIN_IMAGES, subQuery));
948        qb.setProjectionMap(COMBINED_HISTORY_PROJECTION_MAP);
949        String historySubQuery = qb.buildQuery(null,
950                null, null, null, null, null);
951        // Build the bookmark union subquery
952        qb.setTables(TABLE_BOOKMARKS_JOIN_IMAGES);
953        qb.setProjectionMap(COMBINED_BOOKMARK_PROJECTION_MAP);
954        where += String.format(" AND %s NOT IN (SELECT %s FROM %s)",
955                Combined.URL, History.URL, TABLE_HISTORY);
956        String bookmarksSubQuery = qb.buildQuery(null, where,
957                null, null, null, null);
958        // Put it all together
959        String query = qb.buildUnionQuery(
960                new String[] {historySubQuery, bookmarksSubQuery},
961                null, null);
962        qb.setTables("(" + query + ")");
963        qb.setProjectionMap(null);
964        return args;
965    }
966
967    int deleteBookmarks(String selection, String[] selectionArgs,
968            boolean callerIsSyncAdapter) {
969        //TODO cascade deletes down from folders
970        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
971        if (callerIsSyncAdapter) {
972            return db.delete(TABLE_BOOKMARKS, selection, selectionArgs);
973        }
974        ContentValues values = new ContentValues();
975        values.put(Bookmarks.DATE_MODIFIED, System.currentTimeMillis());
976        values.put(Bookmarks.IS_DELETED, 1);
977        return updateBookmarksInTransaction(values, selection, selectionArgs,
978                callerIsSyncAdapter);
979    }
980
981    @Override
982    public int deleteInTransaction(Uri uri, String selection, String[] selectionArgs,
983            boolean callerIsSyncAdapter) {
984        final int match = URI_MATCHER.match(uri);
985        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
986        int deleted = 0;
987        switch (match) {
988            case BOOKMARKS_ID: {
989                selection = DatabaseUtils.concatenateWhere(selection,
990                        TABLE_BOOKMARKS + "._id=?");
991                selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
992                        new String[] { Long.toString(ContentUris.parseId(uri)) });
993                // fall through
994            }
995            case BOOKMARKS: {
996                // Look for account info
997                Object[] withAccount = getSelectionWithAccounts(uri, selection, selectionArgs);
998                selection = (String) withAccount[0];
999                selectionArgs = (String[]) withAccount[1];
1000                deleted = deleteBookmarks(selection, selectionArgs, callerIsSyncAdapter);
1001                pruneImages();
1002                if (deleted > 0) {
1003                    refreshWidgets();
1004                }
1005                break;
1006            }
1007
1008            case HISTORY_ID: {
1009                selection = DatabaseUtils.concatenateWhere(selection, TABLE_HISTORY + "._id=?");
1010                selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
1011                        new String[] { Long.toString(ContentUris.parseId(uri)) });
1012                // fall through
1013            }
1014            case HISTORY: {
1015                filterSearchClient(selectionArgs);
1016                deleted = db.delete(TABLE_HISTORY, selection, selectionArgs);
1017                pruneImages();
1018                break;
1019            }
1020
1021            case SEARCHES_ID: {
1022                selection = DatabaseUtils.concatenateWhere(selection, TABLE_SEARCHES + "._id=?");
1023                selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
1024                        new String[] { Long.toString(ContentUris.parseId(uri)) });
1025                // fall through
1026            }
1027            case SEARCHES: {
1028                deleted = db.delete(TABLE_SEARCHES, selection, selectionArgs);
1029                break;
1030            }
1031
1032            case SYNCSTATE: {
1033                deleted = mSyncHelper.delete(db, selection, selectionArgs);
1034                break;
1035            }
1036            case SYNCSTATE_ID: {
1037                String selectionWithId =
1038                        (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ")
1039                        + (selection == null ? "" : " AND (" + selection + ")");
1040                deleted = mSyncHelper.delete(db, selectionWithId, selectionArgs);
1041                break;
1042            }
1043            case LEGACY_ID: {
1044                selection = DatabaseUtils.concatenateWhere(
1045                        selection, Combined._ID + " = CAST(? AS INTEGER)");
1046                selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
1047                        new String[] { Long.toString(ContentUris.parseId(uri)) });
1048                // fall through
1049            }
1050            case LEGACY: {
1051                String[] projection = new String[] { Combined._ID,
1052                        Combined.IS_BOOKMARK, Combined.URL };
1053                SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
1054                uri = BookmarkUtils.addAccountInfo(getContext(), uri.buildUpon())
1055                        .build();
1056                String[] args = createCombinedQuery(uri, projection, qb);
1057                if (selectionArgs == null) {
1058                    selectionArgs = args;
1059                } else {
1060                    selectionArgs = DatabaseUtils.appendSelectionArgs(
1061                            args, selectionArgs);
1062                }
1063                Cursor c = qb.query(db, projection, selection, selectionArgs,
1064                        null, null, null);
1065                while (c.moveToNext()) {
1066                    long id = c.getLong(0);
1067                    boolean isBookmark = c.getInt(1) != 0;
1068                    String url = c.getString(2);
1069                    if (isBookmark) {
1070                        deleted += deleteBookmarks(Bookmarks._ID + "=?",
1071                                new String[] { Long.toString(id) },
1072                                callerIsSyncAdapter);
1073                        db.delete(TABLE_HISTORY, History.URL + "=?",
1074                                new String[] { url });
1075                    } else {
1076                        deleted += db.delete(TABLE_HISTORY,
1077                                Bookmarks._ID + "=?",
1078                                new String[] { Long.toString(id) });
1079                    }
1080                }
1081                break;
1082            }
1083            default: {
1084                throw new UnsupportedOperationException("Unknown delete URI " + uri);
1085            }
1086        }
1087        if (deleted > 0) {
1088            postNotifyUri(uri);
1089            postNotifyUri(LEGACY_AUTHORITY_URI);
1090        }
1091        return deleted;
1092    }
1093
1094    long queryDefaultFolderId(String accountName, String accountType) {
1095        if (!isNullAccount(accountName) && !isNullAccount(accountType)) {
1096            final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
1097            Cursor c = db.query(TABLE_BOOKMARKS, new String[] { Bookmarks._ID },
1098                    ChromeSyncColumns.SERVER_UNIQUE + " = ?" +
1099                    " AND account_type = ? AND account_name = ?",
1100                    new String[] { ChromeSyncColumns.FOLDER_NAME_BOOKMARKS_BAR,
1101                    accountType, accountName }, null, null, null);
1102            if (c.moveToFirst()) {
1103                return c.getLong(0);
1104            }
1105        }
1106        return FIXED_ID_ROOT;
1107    }
1108
1109    @Override
1110    public Uri insertInTransaction(Uri uri, ContentValues values, boolean callerIsSyncAdapter) {
1111        int match = URI_MATCHER.match(uri);
1112        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1113        long id = -1;
1114        if (match == LEGACY) {
1115            // Intercept and route to the correct table
1116            Integer bookmark = values.getAsInteger(BookmarkColumns.BOOKMARK);
1117            values.remove(BookmarkColumns.BOOKMARK);
1118            if (bookmark == null || bookmark == 0) {
1119                match = HISTORY;
1120            } else {
1121                match = BOOKMARKS;
1122                values.remove(BookmarkColumns.DATE);
1123                values.remove(BookmarkColumns.VISITS);
1124                values.remove(BookmarkColumns.USER_ENTERED);
1125                SharedPreferences prefs = PreferenceManager
1126                        .getDefaultSharedPreferences(getContext());
1127                String accountType = prefs.getString(
1128                        BrowserBookmarksPage.PREF_ACCOUNT_TYPE, null);
1129                String accountName = prefs.getString(
1130                        BrowserBookmarksPage.PREF_ACCOUNT_NAME, null);
1131                values.put(Bookmarks.ACCOUNT_TYPE, accountType);
1132                values.put(Bookmarks.ACCOUNT_NAME, accountName);
1133                values.put(Bookmarks.IS_FOLDER, 0);
1134            }
1135        }
1136        switch (match) {
1137            case BOOKMARKS: {
1138                // Mark rows dirty if they're not coming from a sync adapter
1139                if (!callerIsSyncAdapter) {
1140                    long now = System.currentTimeMillis();
1141                    values.put(Bookmarks.DATE_CREATED, now);
1142                    values.put(Bookmarks.DATE_MODIFIED, now);
1143                    values.put(Bookmarks.DIRTY, 1);
1144
1145                    // If no parent is set default to the "Bookmarks Bar" folder
1146                    if (!values.containsKey(Bookmarks.PARENT)) {
1147                        String accountType = values
1148                                .getAsString(Bookmarks.ACCOUNT_TYPE);
1149                        String accountName = values
1150                                .getAsString(Bookmarks.ACCOUNT_NAME);
1151                        values.put(Bookmarks.PARENT,
1152                                queryDefaultFolderId(accountName, accountType));
1153                    }
1154                }
1155
1156                // If no position is requested put the bookmark at the beginning of the list
1157                if (!values.containsKey(Bookmarks.POSITION)) {
1158                    values.put(Bookmarks.POSITION, Long.toString(Long.MIN_VALUE));
1159                }
1160
1161                // Extract out the image values so they can be inserted into the images table
1162                String url = values.getAsString(Bookmarks.URL);
1163                ContentValues imageValues = extractImageValues(values, url);
1164                Boolean isFolder = values.getAsBoolean(Bookmarks.IS_FOLDER);
1165                if ((isFolder == null || !isFolder)
1166                        && imageValues != null && !TextUtils.isEmpty(url)) {
1167                    int count = db.update(TABLE_IMAGES, imageValues, Images.URL + "=?",
1168                            new String[] { url });
1169                    if (count == 0) {
1170                        db.insertOrThrow(TABLE_IMAGES, Images.FAVICON, imageValues);
1171                    }
1172                }
1173
1174                id = db.insertOrThrow(TABLE_BOOKMARKS, Bookmarks.DIRTY, values);
1175                refreshWidgets();
1176                break;
1177            }
1178
1179            case HISTORY: {
1180                // If no created time is specified set it to now
1181                if (!values.containsKey(History.DATE_CREATED)) {
1182                    values.put(History.DATE_CREATED, System.currentTimeMillis());
1183                }
1184                String url = values.getAsString(History.URL);
1185                url = filterSearchClient(url);
1186                values.put(History.URL, url);
1187
1188                // Extract out the image values so they can be inserted into the images table
1189                ContentValues imageValues = extractImageValues(values,
1190                        values.getAsString(History.URL));
1191                if (imageValues != null) {
1192                    db.insertOrThrow(TABLE_IMAGES, Images.FAVICON, imageValues);
1193                }
1194
1195                id = db.insertOrThrow(TABLE_HISTORY, History.VISITS, values);
1196                break;
1197            }
1198
1199            case SEARCHES: {
1200                id = insertSearchesInTransaction(db, values);
1201                break;
1202            }
1203
1204            case SYNCSTATE: {
1205                id = mSyncHelper.insert(db, values);
1206                break;
1207            }
1208
1209            case SETTINGS: {
1210                id = 0;
1211                insertSettingsInTransaction(db, values);
1212                break;
1213            }
1214
1215            default: {
1216                throw new UnsupportedOperationException("Unknown insert URI " + uri);
1217            }
1218        }
1219
1220        if (id >= 0) {
1221            postNotifyUri(uri);
1222            postNotifyUri(LEGACY_AUTHORITY_URI);
1223            return ContentUris.withAppendedId(uri, id);
1224        } else {
1225            return null;
1226        }
1227    }
1228
1229    private void filterSearchClient(String[] selectionArgs) {
1230        if (selectionArgs != null) {
1231            for (int i = 0; i < selectionArgs.length; i++) {
1232                selectionArgs[i] = filterSearchClient(selectionArgs[i]);
1233            }
1234        }
1235    }
1236
1237    // Filters out the client=ms- param for search urls
1238    private String filterSearchClient(String url) {
1239        // remove "client" before updating it to the history so that it wont
1240        // show up in the auto-complete list.
1241        int index = url.indexOf("client=ms-");
1242        if (index > 0 && url.contains(".google.")) {
1243            int end = url.indexOf('&', index);
1244            if (end > 0) {
1245                url = url.substring(0, index)
1246                        .concat(url.substring(end + 1));
1247            } else {
1248                // the url.charAt(index-1) should be either '?' or '&'
1249                url = url.substring(0, index-1);
1250            }
1251        }
1252        return url;
1253    }
1254
1255    /**
1256     * Searches are unique, so perform an UPSERT manually since SQLite doesn't support them.
1257     */
1258    private long insertSearchesInTransaction(SQLiteDatabase db, ContentValues values) {
1259        String search = values.getAsString(Searches.SEARCH);
1260        if (TextUtils.isEmpty(search)) {
1261            throw new IllegalArgumentException("Must include the SEARCH field");
1262        }
1263        Cursor cursor = null;
1264        try {
1265            cursor = db.query(TABLE_SEARCHES, new String[] { Searches._ID },
1266                    Searches.SEARCH + "=?", new String[] { search }, null, null, null);
1267            if (cursor.moveToNext()) {
1268                long id = cursor.getLong(0);
1269                db.update(TABLE_SEARCHES, values, Searches._ID + "=?",
1270                        new String[] { Long.toString(id) });
1271                return id;
1272            } else {
1273                return db.insertOrThrow(TABLE_SEARCHES, Searches.SEARCH, values);
1274            }
1275        } finally {
1276            if (cursor != null) cursor.close();
1277        }
1278    }
1279
1280    /**
1281     * Settings are unique, so perform an UPSERT manually since SQLite doesn't support them.
1282     */
1283    private long insertSettingsInTransaction(SQLiteDatabase db, ContentValues values) {
1284        String key = values.getAsString(Settings.KEY);
1285        if (TextUtils.isEmpty(key)) {
1286            throw new IllegalArgumentException("Must include the KEY field");
1287        }
1288        String[] keyArray = new String[] { key };
1289        Cursor cursor = null;
1290        try {
1291            cursor = db.query(TABLE_SETTINGS, new String[] { Settings.KEY },
1292                    Settings.KEY + "=?", keyArray, null, null, null);
1293            if (cursor.moveToNext()) {
1294                long id = cursor.getLong(0);
1295                db.update(TABLE_SETTINGS, values, Settings.KEY + "=?", keyArray);
1296                return id;
1297            } else {
1298                return db.insertOrThrow(TABLE_SETTINGS, Settings.VALUE, values);
1299            }
1300        } finally {
1301            if (cursor != null) cursor.close();
1302        }
1303    }
1304
1305    @Override
1306    public int updateInTransaction(Uri uri, ContentValues values, String selection,
1307            String[] selectionArgs, boolean callerIsSyncAdapter) {
1308        int match = URI_MATCHER.match(uri);
1309        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1310        if (match == LEGACY || match == LEGACY_ID) {
1311            // Intercept and route to the correct table
1312            Integer bookmark = values.getAsInteger(BookmarkColumns.BOOKMARK);
1313            values.remove(BookmarkColumns.BOOKMARK);
1314            if (bookmark == null || bookmark == 0) {
1315                if (match == LEGACY) {
1316                    match = HISTORY;
1317                } else {
1318                    match = HISTORY_ID;
1319                }
1320            } else {
1321                if (match == LEGACY) {
1322                    match = BOOKMARKS;
1323                } else {
1324                    match = BOOKMARKS_ID;
1325                }
1326                values.remove(BookmarkColumns.DATE);
1327                values.remove(BookmarkColumns.VISITS);
1328                values.remove(BookmarkColumns.USER_ENTERED);
1329            }
1330        }
1331        int modified = 0;
1332        switch (match) {
1333            case BOOKMARKS_ID: {
1334                selection = DatabaseUtils.concatenateWhere(selection,
1335                        TABLE_BOOKMARKS + "._id=?");
1336                selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
1337                        new String[] { Long.toString(ContentUris.parseId(uri)) });
1338                // fall through
1339            }
1340            case BOOKMARKS: {
1341                Object[] withAccount = getSelectionWithAccounts(uri, selection, selectionArgs);
1342                selection = (String) withAccount[0];
1343                selectionArgs = (String[]) withAccount[1];
1344                modified = updateBookmarksInTransaction(values, selection, selectionArgs,
1345                        callerIsSyncAdapter);
1346                if (modified > 0) {
1347                    refreshWidgets();
1348                }
1349                break;
1350            }
1351
1352            case HISTORY_ID: {
1353                selection = DatabaseUtils.concatenateWhere(selection, TABLE_HISTORY + "._id=?");
1354                selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
1355                        new String[] { Long.toString(ContentUris.parseId(uri)) });
1356                // fall through
1357            }
1358            case HISTORY: {
1359                modified = updateHistoryInTransaction(values, selection, selectionArgs);
1360                break;
1361            }
1362
1363            case SYNCSTATE: {
1364                modified = mSyncHelper.update(mDb, values,
1365                        appendAccountToSelection(uri, selection), selectionArgs);
1366                break;
1367            }
1368
1369            case SYNCSTATE_ID: {
1370                selection = appendAccountToSelection(uri, selection);
1371                String selectionWithId =
1372                        (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ")
1373                        + (selection == null ? "" : " AND (" + selection + ")");
1374                modified = mSyncHelper.update(mDb, values,
1375                        selectionWithId, selectionArgs);
1376                break;
1377            }
1378
1379            case IMAGES: {
1380                String url = values.getAsString(Images.URL);
1381                if (TextUtils.isEmpty(url)) {
1382                    throw new IllegalArgumentException("Images.URL is required");
1383                }
1384                if (!shouldUpdateImages(db, url, values)) {
1385                    return 0;
1386                }
1387                int count = db.update(TABLE_IMAGES, values, Images.URL + "=?",
1388                        new String[] { url });
1389                if (count == 0) {
1390                    db.insertOrThrow(TABLE_IMAGES, Images.FAVICON, values);
1391                    count = 1;
1392                }
1393                if (getUrlCount(db, TABLE_BOOKMARKS, url) > 0) {
1394                    postNotifyUri(Bookmarks.CONTENT_URI);
1395                    refreshWidgets();
1396                }
1397                if (getUrlCount(db, TABLE_HISTORY, url) > 0) {
1398                    postNotifyUri(History.CONTENT_URI);
1399                }
1400                postNotifyUri(LEGACY_AUTHORITY_URI);
1401                pruneImages();
1402                return count;
1403            }
1404
1405            case SEARCHES: {
1406                modified = db.update(TABLE_SEARCHES, values, selection, selectionArgs);
1407                break;
1408            }
1409
1410            default: {
1411                throw new UnsupportedOperationException("Unknown update URI " + uri);
1412            }
1413        }
1414        pruneImages();
1415        if (modified > 0) {
1416            postNotifyUri(uri);
1417            postNotifyUri(LEGACY_AUTHORITY_URI);
1418        }
1419        return modified;
1420    }
1421
1422    // We want to avoid sending out more URI notifications than we have to
1423    // Thus, we check to see if the images we are about to store are already there
1424    // This is used because things like a site's favion or touch icon is rarely
1425    // changed, but the browser tries to update it every time the page loads.
1426    // Without this, we will always send out 3 URI notifications per page load.
1427    // With this, that drops to 0 or 1, depending on if the thumbnail changed.
1428    private boolean shouldUpdateImages(
1429            SQLiteDatabase db, String url, ContentValues values) {
1430        final String[] projection = new String[] {
1431                Images.FAVICON,
1432                Images.THUMBNAIL,
1433                Images.TOUCH_ICON,
1434        };
1435        Cursor cursor = db.query(TABLE_IMAGES, projection, Images.URL + "=?",
1436                new String[] { url }, null, null, null);
1437        byte[] nfavicon = values.getAsByteArray(Images.FAVICON);
1438        byte[] nthumb = values.getAsByteArray(Images.THUMBNAIL);
1439        byte[] ntouch = values.getAsByteArray(Images.TOUCH_ICON);
1440        byte[] cfavicon = null;
1441        byte[] cthumb = null;
1442        byte[] ctouch = null;
1443        try {
1444            if (cursor.getCount() <= 0) {
1445                return nfavicon != null || nthumb != null || ntouch != null;
1446            }
1447            while (cursor.moveToNext()) {
1448                if (nfavicon != null) {
1449                    cfavicon = cursor.getBlob(0);
1450                    if (!Arrays.equals(nfavicon, cfavicon)) {
1451                        return true;
1452                    }
1453                }
1454                if (nthumb != null) {
1455                    cthumb = cursor.getBlob(1);
1456                    if (!Arrays.equals(nthumb, cthumb)) {
1457                        return true;
1458                    }
1459                }
1460                if (ntouch != null) {
1461                    ctouch = cursor.getBlob(2);
1462                    if (!Arrays.equals(ntouch, ctouch)) {
1463                        return true;
1464                    }
1465                }
1466            }
1467        } finally {
1468            cursor.close();
1469        }
1470        return false;
1471    }
1472
1473    int getUrlCount(SQLiteDatabase db, String table, String url) {
1474        Cursor c = db.query(table, new String[] { "COUNT(*)" },
1475                "url = ?", new String[] { url }, null, null, null);
1476        try {
1477            int count = 0;
1478            if (c.moveToFirst()) {
1479                count = c.getInt(0);
1480            }
1481            return count;
1482        } finally {
1483            c.close();
1484        }
1485    }
1486
1487    /**
1488     * Does a query to find the matching bookmarks and updates each one with the provided values.
1489     */
1490    int updateBookmarksInTransaction(ContentValues values, String selection,
1491            String[] selectionArgs, boolean callerIsSyncAdapter) {
1492        int count = 0;
1493        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1494        Cursor cursor = db.query(TABLE_BOOKMARKS,
1495                new String[] { Bookmarks._ID, Bookmarks.VERSION, Bookmarks.URL },
1496                selection, selectionArgs, null, null, null);
1497        try {
1498            String[] args = new String[1];
1499            // Mark the bookmark dirty if the caller isn't a sync adapter
1500            if (!callerIsSyncAdapter) {
1501                values.put(Bookmarks.DATE_MODIFIED, System.currentTimeMillis());
1502                values.put(Bookmarks.DIRTY, 1);
1503            }
1504
1505            boolean updatingUrl = values.containsKey(Bookmarks.URL);
1506            String url = null;
1507            if (updatingUrl) {
1508                url = values.getAsString(Bookmarks.URL);
1509            }
1510            ContentValues imageValues = extractImageValues(values, url);
1511
1512            while (cursor.moveToNext()) {
1513                args[0] = cursor.getString(0);
1514                if (!callerIsSyncAdapter) {
1515                    // increase the local version for non-sync changes
1516                    values.put(Bookmarks.VERSION, cursor.getLong(1) + 1);
1517                }
1518                count += db.update(TABLE_BOOKMARKS, values, "_id=?", args);
1519
1520                // Update the images over in their table
1521                if (imageValues != null) {
1522                    if (!updatingUrl) {
1523                        url = cursor.getString(2);
1524                        imageValues.put(Images.URL, url);
1525                    }
1526
1527                    if (!TextUtils.isEmpty(url)) {
1528                        args[0] = url;
1529                        if (db.update(TABLE_IMAGES, imageValues, Images.URL + "=?", args) == 0) {
1530                            db.insert(TABLE_IMAGES, Images.FAVICON, imageValues);
1531                        }
1532                    }
1533                }
1534            }
1535        } finally {
1536            if (cursor != null) cursor.close();
1537        }
1538        return count;
1539    }
1540
1541    /**
1542     * Does a query to find the matching bookmarks and updates each one with the provided values.
1543     */
1544    int updateHistoryInTransaction(ContentValues values, String selection, String[] selectionArgs) {
1545        int count = 0;
1546        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1547        filterSearchClient(selectionArgs);
1548        Cursor cursor = query(History.CONTENT_URI,
1549                new String[] { History._ID, History.URL },
1550                selection, selectionArgs, null);
1551        try {
1552            String[] args = new String[1];
1553
1554            boolean updatingUrl = values.containsKey(History.URL);
1555            String url = null;
1556            if (updatingUrl) {
1557                url = filterSearchClient(values.getAsString(History.URL));
1558                values.put(History.URL, url);
1559            }
1560            ContentValues imageValues = extractImageValues(values, url);
1561
1562            while (cursor.moveToNext()) {
1563                args[0] = cursor.getString(0);
1564                count += db.update(TABLE_HISTORY, values, "_id=?", args);
1565
1566                // Update the images over in their table
1567                if (imageValues != null) {
1568                    if (!updatingUrl) {
1569                        url = cursor.getString(1);
1570                        imageValues.put(Images.URL, url);
1571                    }
1572                    args[0] = url;
1573                    if (db.update(TABLE_IMAGES, imageValues, Images.URL + "=?", args) == 0) {
1574                        db.insert(TABLE_IMAGES, Images.FAVICON, imageValues);
1575                    }
1576                }
1577            }
1578        } finally {
1579            if (cursor != null) cursor.close();
1580        }
1581        return count;
1582    }
1583
1584    String appendAccountToSelection(Uri uri, String selection) {
1585        final String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
1586        final String accountType = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE);
1587
1588        final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType);
1589        if (partialUri) {
1590            // Throw when either account is incomplete
1591            throw new IllegalArgumentException(
1592                    "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE for " + uri);
1593        }
1594
1595        // Accounts are valid by only checking one parameter, since we've
1596        // already ruled out partial accounts.
1597        final boolean validAccount = !TextUtils.isEmpty(accountName);
1598        if (validAccount) {
1599            StringBuilder selectionSb = new StringBuilder(RawContacts.ACCOUNT_NAME + "="
1600                    + DatabaseUtils.sqlEscapeString(accountName) + " AND "
1601                    + RawContacts.ACCOUNT_TYPE + "="
1602                    + DatabaseUtils.sqlEscapeString(accountType));
1603            if (!TextUtils.isEmpty(selection)) {
1604                selectionSb.append(" AND (");
1605                selectionSb.append(selection);
1606                selectionSb.append(')');
1607            }
1608            return selectionSb.toString();
1609        } else {
1610            return selection;
1611        }
1612    }
1613
1614    ContentValues extractImageValues(ContentValues values, String url) {
1615        ContentValues imageValues = null;
1616        // favicon
1617        if (values.containsKey(Bookmarks.FAVICON)) {
1618            imageValues = new ContentValues();
1619            imageValues.put(Images.FAVICON, values.getAsByteArray(Bookmarks.FAVICON));
1620            values.remove(Bookmarks.FAVICON);
1621        }
1622
1623        // thumbnail
1624        if (values.containsKey(Bookmarks.THUMBNAIL)) {
1625            if (imageValues == null) {
1626                imageValues = new ContentValues();
1627            }
1628            imageValues.put(Images.THUMBNAIL, values.getAsByteArray(Bookmarks.THUMBNAIL));
1629            values.remove(Bookmarks.THUMBNAIL);
1630        }
1631
1632        // touch icon
1633        if (values.containsKey(Bookmarks.TOUCH_ICON)) {
1634            if (imageValues == null) {
1635                imageValues = new ContentValues();
1636            }
1637            imageValues.put(Images.TOUCH_ICON, values.getAsByteArray(Bookmarks.TOUCH_ICON));
1638            values.remove(Bookmarks.TOUCH_ICON);
1639        }
1640
1641        if (imageValues != null) {
1642            imageValues.put(Images.URL,  url);
1643        }
1644        return imageValues;
1645    }
1646
1647    void pruneImages() {
1648        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1649        db.delete(TABLE_IMAGES, IMAGE_PRUNE, null);
1650    }
1651
1652    static class SuggestionsCursor extends AbstractCursor {
1653        private static final int ID_INDEX = 0;
1654        private static final int URL_INDEX = 1;
1655        private static final int TITLE_INDEX = 2;
1656        // shared suggestion array index, make sure to match COLUMNS
1657        private static final int SUGGEST_COLUMN_INTENT_ACTION_ID = 1;
1658        private static final int SUGGEST_COLUMN_INTENT_DATA_ID = 2;
1659        private static final int SUGGEST_COLUMN_TEXT_1_ID = 3;
1660        private static final int SUGGEST_COLUMN_TEXT_2_TEXT_ID = 4;
1661        private static final int SUGGEST_COLUMN_TEXT_2_URL_ID = 5;
1662        private static final int SUGGEST_COLUMN_ICON_1_ID = 6;
1663
1664        // shared suggestion columns
1665        private static final String[] COLUMNS = new String[] {
1666                BaseColumns._ID,
1667                SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
1668                SearchManager.SUGGEST_COLUMN_INTENT_DATA,
1669                SearchManager.SUGGEST_COLUMN_TEXT_1,
1670                SearchManager.SUGGEST_COLUMN_TEXT_2,
1671                SearchManager.SUGGEST_COLUMN_TEXT_2_URL,
1672                SearchManager.SUGGEST_COLUMN_ICON_1};
1673
1674        private Cursor mSource;
1675
1676        public SuggestionsCursor(Cursor cursor) {
1677            mSource = cursor;
1678        }
1679
1680        @Override
1681        public String[] getColumnNames() {
1682            return COLUMNS;
1683        }
1684
1685        @Override
1686        public String getString(int columnIndex) {
1687            switch (columnIndex) {
1688            case ID_INDEX:
1689                return mSource.getString(columnIndex);
1690            case SUGGEST_COLUMN_INTENT_ACTION_ID:
1691                return Intent.ACTION_VIEW;
1692            case SUGGEST_COLUMN_INTENT_DATA_ID:
1693            case SUGGEST_COLUMN_TEXT_2_TEXT_ID:
1694            case SUGGEST_COLUMN_TEXT_2_URL_ID:
1695                return mSource.getString(URL_INDEX);
1696            case SUGGEST_COLUMN_TEXT_1_ID:
1697                return mSource.getString(TITLE_INDEX);
1698            case SUGGEST_COLUMN_ICON_1_ID:
1699                return Integer.toString(R.drawable.ic_bookmark_off_holo_dark);
1700            }
1701            return null;
1702        }
1703
1704        @Override
1705        public int getCount() {
1706            return mSource.getCount();
1707        }
1708
1709        @Override
1710        public double getDouble(int column) {
1711            throw new UnsupportedOperationException();
1712        }
1713
1714        @Override
1715        public float getFloat(int column) {
1716            throw new UnsupportedOperationException();
1717        }
1718
1719        @Override
1720        public int getInt(int column) {
1721            throw new UnsupportedOperationException();
1722        }
1723
1724        @Override
1725        public long getLong(int column) {
1726            switch (column) {
1727            case ID_INDEX:
1728                return mSource.getLong(ID_INDEX);
1729            }
1730            throw new UnsupportedOperationException();
1731        }
1732
1733        @Override
1734        public short getShort(int column) {
1735            throw new UnsupportedOperationException();
1736        }
1737
1738        @Override
1739        public boolean isNull(int column) {
1740            return mSource.isNull(column);
1741        }
1742
1743        @Override
1744        public boolean onMove(int oldPosition, int newPosition) {
1745            return mSource.moveToPosition(newPosition);
1746        }
1747    }
1748}
1749