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