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