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