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