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