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