|
1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- |
|
2 * This Source Code Form is subject to the terms of the Mozilla Public |
|
3 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
5 |
|
6 package org.mozilla.gecko.db; |
|
7 |
|
8 import java.io.ByteArrayOutputStream; |
|
9 import java.util.Collection; |
|
10 import java.util.HashMap; |
|
11 import java.util.List; |
|
12 |
|
13 import org.mozilla.gecko.AboutPages; |
|
14 import org.mozilla.gecko.db.BrowserContract.Bookmarks; |
|
15 import org.mozilla.gecko.db.BrowserContract.Combined; |
|
16 import org.mozilla.gecko.db.BrowserContract.ExpirePriority; |
|
17 import org.mozilla.gecko.db.BrowserContract.FaviconColumns; |
|
18 import org.mozilla.gecko.db.BrowserContract.Favicons; |
|
19 import org.mozilla.gecko.db.BrowserContract.History; |
|
20 import org.mozilla.gecko.db.BrowserContract.ReadingListItems; |
|
21 import org.mozilla.gecko.db.BrowserContract.SyncColumns; |
|
22 import org.mozilla.gecko.db.BrowserContract.Thumbnails; |
|
23 import org.mozilla.gecko.db.BrowserContract.URLColumns; |
|
24 import org.mozilla.gecko.favicons.decoders.FaviconDecoder; |
|
25 import org.mozilla.gecko.favicons.decoders.LoadFaviconResult; |
|
26 |
|
27 import android.content.ContentProviderOperation; |
|
28 import android.content.ContentResolver; |
|
29 import android.content.ContentValues; |
|
30 import android.database.ContentObserver; |
|
31 import android.database.Cursor; |
|
32 import android.database.CursorWrapper; |
|
33 import android.graphics.Bitmap; |
|
34 import android.graphics.drawable.BitmapDrawable; |
|
35 import android.net.Uri; |
|
36 import android.provider.Browser; |
|
37 import android.text.TextUtils; |
|
38 import android.util.Log; |
|
39 |
|
40 public class LocalBrowserDB implements BrowserDB.BrowserDBIface { |
|
41 // Calculate these once, at initialization. isLoggable is too expensive to |
|
42 // have in-line in each log call. |
|
43 private static final String LOGTAG = "GeckoLocalBrowserDB"; |
|
44 private static boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG); |
|
45 protected static void debug(String message) { |
|
46 if (logDebug) { |
|
47 Log.d(LOGTAG, message); |
|
48 } |
|
49 } |
|
50 |
|
51 private final String mProfile; |
|
52 |
|
53 // Map of folder GUIDs to IDs. Used for caching. |
|
54 private HashMap<String, Long> mFolderIdMap; |
|
55 |
|
56 // Use wrapped Boolean so that we can have a null state |
|
57 private Boolean mDesktopBookmarksExist; |
|
58 |
|
59 private final Uri mBookmarksUriWithProfile; |
|
60 private final Uri mParentsUriWithProfile; |
|
61 private final Uri mFlagsUriWithProfile; |
|
62 private final Uri mHistoryUriWithProfile; |
|
63 private final Uri mHistoryExpireUriWithProfile; |
|
64 private final Uri mCombinedUriWithProfile; |
|
65 private final Uri mDeletedHistoryUriWithProfile; |
|
66 private final Uri mUpdateHistoryUriWithProfile; |
|
67 private final Uri mFaviconsUriWithProfile; |
|
68 private final Uri mThumbnailsUriWithProfile; |
|
69 private final Uri mReadingListUriWithProfile; |
|
70 |
|
71 private static final String[] DEFAULT_BOOKMARK_COLUMNS = |
|
72 new String[] { Bookmarks._ID, |
|
73 Bookmarks.GUID, |
|
74 Bookmarks.URL, |
|
75 Bookmarks.TITLE, |
|
76 Bookmarks.TYPE, |
|
77 Bookmarks.PARENT }; |
|
78 |
|
79 public LocalBrowserDB(String profile) { |
|
80 mProfile = profile; |
|
81 mFolderIdMap = new HashMap<String, Long>(); |
|
82 mDesktopBookmarksExist = null; |
|
83 |
|
84 mBookmarksUriWithProfile = appendProfile(Bookmarks.CONTENT_URI); |
|
85 mParentsUriWithProfile = appendProfile(Bookmarks.PARENTS_CONTENT_URI); |
|
86 mFlagsUriWithProfile = appendProfile(Bookmarks.FLAGS_URI); |
|
87 mHistoryUriWithProfile = appendProfile(History.CONTENT_URI); |
|
88 mHistoryExpireUriWithProfile = appendProfile(History.CONTENT_OLD_URI); |
|
89 mCombinedUriWithProfile = appendProfile(Combined.CONTENT_URI); |
|
90 mFaviconsUriWithProfile = appendProfile(Favicons.CONTENT_URI); |
|
91 mThumbnailsUriWithProfile = appendProfile(Thumbnails.CONTENT_URI); |
|
92 mReadingListUriWithProfile = appendProfile(ReadingListItems.CONTENT_URI); |
|
93 |
|
94 mDeletedHistoryUriWithProfile = mHistoryUriWithProfile.buildUpon(). |
|
95 appendQueryParameter(BrowserContract.PARAM_SHOW_DELETED, "1").build(); |
|
96 |
|
97 mUpdateHistoryUriWithProfile = mHistoryUriWithProfile.buildUpon(). |
|
98 appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true"). |
|
99 appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build(); |
|
100 } |
|
101 |
|
102 // Invalidate cached data |
|
103 @Override |
|
104 public void invalidateCachedState() { |
|
105 mDesktopBookmarksExist = null; |
|
106 } |
|
107 |
|
108 private Uri historyUriWithLimit(int limit) { |
|
109 return mHistoryUriWithProfile.buildUpon().appendQueryParameter(BrowserContract.PARAM_LIMIT, |
|
110 String.valueOf(limit)).build(); |
|
111 } |
|
112 |
|
113 private Uri bookmarksUriWithLimit(int limit) { |
|
114 return mBookmarksUriWithProfile.buildUpon().appendQueryParameter(BrowserContract.PARAM_LIMIT, |
|
115 String.valueOf(limit)).build(); |
|
116 } |
|
117 |
|
118 private Uri combinedUriWithLimit(int limit) { |
|
119 return mCombinedUriWithProfile.buildUpon().appendQueryParameter(BrowserContract.PARAM_LIMIT, |
|
120 String.valueOf(limit)).build(); |
|
121 } |
|
122 |
|
123 private Uri appendProfile(Uri uri) { |
|
124 return uri.buildUpon().appendQueryParameter(BrowserContract.PARAM_PROFILE, mProfile).build(); |
|
125 } |
|
126 |
|
127 private Uri getAllBookmarksUri() { |
|
128 Uri.Builder uriBuilder = mBookmarksUriWithProfile.buildUpon() |
|
129 .appendQueryParameter(BrowserContract.PARAM_SHOW_DELETED, "1"); |
|
130 return uriBuilder.build(); |
|
131 } |
|
132 |
|
133 private Uri getAllHistoryUri() { |
|
134 Uri.Builder uriBuilder = mHistoryUriWithProfile.buildUpon() |
|
135 .appendQueryParameter(BrowserContract.PARAM_SHOW_DELETED, "1"); |
|
136 return uriBuilder.build(); |
|
137 } |
|
138 |
|
139 private Uri getAllFaviconsUri() { |
|
140 Uri.Builder uriBuilder = mFaviconsUriWithProfile.buildUpon() |
|
141 .appendQueryParameter(BrowserContract.PARAM_SHOW_DELETED, "1"); |
|
142 return uriBuilder.build(); |
|
143 } |
|
144 |
|
145 private Cursor filterAllSites(ContentResolver cr, String[] projection, CharSequence constraint, |
|
146 int limit, CharSequence urlFilter) { |
|
147 return filterAllSites(cr, projection, constraint, limit, urlFilter, "", null); |
|
148 } |
|
149 |
|
150 private Cursor filterAllSites(ContentResolver cr, String[] projection, CharSequence constraint, |
|
151 int limit, CharSequence urlFilter, String selection, String[] selectionArgs) { |
|
152 // The combined history/bookmarks selection queries for sites with a url or title containing |
|
153 // the constraint string(s), treating space-separated words as separate constraints |
|
154 if (!TextUtils.isEmpty(constraint)) { |
|
155 String[] constraintWords = constraint.toString().split(" "); |
|
156 // Only create a filter query with a maximum of 10 constraint words |
|
157 int constraintCount = Math.min(constraintWords.length, 10); |
|
158 for (int i = 0; i < constraintCount; i++) { |
|
159 selection = DBUtils.concatenateWhere(selection, "(" + Combined.URL + " LIKE ? OR " + |
|
160 Combined.TITLE + " LIKE ?)"); |
|
161 String constraintWord = "%" + constraintWords[i] + "%"; |
|
162 selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, |
|
163 new String[] { constraintWord, constraintWord }); |
|
164 } |
|
165 } |
|
166 |
|
167 if (urlFilter != null) { |
|
168 selection = DBUtils.concatenateWhere(selection, "(" + Combined.URL + " NOT LIKE ?)"); |
|
169 selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, new String[] { urlFilter.toString() }); |
|
170 } |
|
171 |
|
172 // Our version of frecency is computed by scaling the number of visits by a multiplier |
|
173 // that approximates Gaussian decay, based on how long ago the entry was last visited. |
|
174 // Since we're limited by the math we can do with sqlite, we're calculating this |
|
175 // approximation using the Cauchy distribution: multiplier = 15^2 / (age^2 + 15^2). |
|
176 // Using 15 as our scale parameter, we get a constant 15^2 = 225. Following this math, |
|
177 // frecencyScore = numVisits * max(1, 100 * 225 / (age*age + 225)). (See bug 704977) |
|
178 // We also give bookmarks an extra bonus boost by adding 100 points to their frecency score. |
|
179 final String sortOrder = BrowserContract.getFrecencySortOrder(true, false); |
|
180 |
|
181 Cursor c = cr.query(combinedUriWithLimit(limit), |
|
182 projection, |
|
183 selection, |
|
184 selectionArgs, |
|
185 sortOrder); |
|
186 |
|
187 return new LocalDBCursor(c); |
|
188 } |
|
189 |
|
190 @Override |
|
191 public int getCount(ContentResolver cr, String database) { |
|
192 int count = 0; |
|
193 String[] columns = null; |
|
194 String constraint = null; |
|
195 Uri uri = null; |
|
196 if ("history".equals(database)) { |
|
197 uri = mHistoryUriWithProfile; |
|
198 columns = new String[] { History._ID }; |
|
199 constraint = Combined.VISITS + " > 0"; |
|
200 } else if ("bookmarks".equals(database)) { |
|
201 uri = mBookmarksUriWithProfile; |
|
202 columns = new String[] { Bookmarks._ID }; |
|
203 // ignore folders, tags, keywords, separators, etc. |
|
204 constraint = Bookmarks.TYPE + " = " + Bookmarks.TYPE_BOOKMARK; |
|
205 } else if ("thumbnails".equals(database)) { |
|
206 uri = mThumbnailsUriWithProfile; |
|
207 columns = new String[] { Thumbnails._ID }; |
|
208 } else if ("favicons".equals(database)) { |
|
209 uri = mFaviconsUriWithProfile; |
|
210 columns = new String[] { Favicons._ID }; |
|
211 } |
|
212 if (uri != null) { |
|
213 Cursor cursor = null; |
|
214 |
|
215 try { |
|
216 cursor = cr.query(uri, columns, constraint, null, null); |
|
217 count = cursor.getCount(); |
|
218 } finally { |
|
219 if (cursor != null) |
|
220 cursor.close(); |
|
221 } |
|
222 } |
|
223 debug("Got count " + count + " for " + database); |
|
224 return count; |
|
225 } |
|
226 |
|
227 @Override |
|
228 public Cursor filter(ContentResolver cr, CharSequence constraint, int limit) { |
|
229 return filterAllSites(cr, |
|
230 new String[] { Combined._ID, |
|
231 Combined.URL, |
|
232 Combined.TITLE, |
|
233 Combined.DISPLAY, |
|
234 Combined.BOOKMARK_ID, |
|
235 Combined.HISTORY_ID }, |
|
236 constraint, |
|
237 limit, |
|
238 null); |
|
239 } |
|
240 |
|
241 @Override |
|
242 public Cursor getTopSites(ContentResolver cr, int limit) { |
|
243 // Filter out bookmarks that don't have real parents (e.g. pinned sites or reading list items) |
|
244 String selection = DBUtils.concatenateWhere("", Combined.URL + " NOT IN (SELECT " + |
|
245 Bookmarks.URL + " FROM bookmarks WHERE " + |
|
246 DBUtils.qualifyColumn("bookmarks", Bookmarks.PARENT) + " < ? AND " + |
|
247 DBUtils.qualifyColumn("bookmarks", Bookmarks.IS_DELETED) + " == 0)"); |
|
248 String[] selectionArgs = new String[] { String.valueOf(Bookmarks.FIXED_ROOT_ID) }; |
|
249 |
|
250 return filterAllSites(cr, |
|
251 new String[] { Combined._ID, |
|
252 Combined.URL, |
|
253 Combined.TITLE, |
|
254 Combined.DISPLAY, |
|
255 Combined.BOOKMARK_ID, |
|
256 Combined.HISTORY_ID }, |
|
257 "", |
|
258 limit, |
|
259 AboutPages.URL_FILTER, |
|
260 selection, |
|
261 selectionArgs); |
|
262 } |
|
263 |
|
264 @Override |
|
265 public void updateVisitedHistory(ContentResolver cr, String uri) { |
|
266 ContentValues values = new ContentValues(); |
|
267 |
|
268 values.put(History.URL, uri); |
|
269 values.put(History.DATE_LAST_VISITED, System.currentTimeMillis()); |
|
270 values.put(History.IS_DELETED, 0); |
|
271 |
|
272 // This will insert a new history entry if one for this URL |
|
273 // doesn't already exist |
|
274 cr.update(mUpdateHistoryUriWithProfile, |
|
275 values, |
|
276 History.URL + " = ?", |
|
277 new String[] { uri }); |
|
278 } |
|
279 |
|
280 @Override |
|
281 public void updateHistoryTitle(ContentResolver cr, String uri, String title) { |
|
282 ContentValues values = new ContentValues(); |
|
283 values.put(History.TITLE, title); |
|
284 |
|
285 cr.update(mHistoryUriWithProfile, |
|
286 values, |
|
287 History.URL + " = ?", |
|
288 new String[] { uri }); |
|
289 } |
|
290 |
|
291 @Override |
|
292 public void updateHistoryEntry(ContentResolver cr, String uri, String title, |
|
293 long date, int visits) { |
|
294 int oldVisits = 0; |
|
295 Cursor cursor = null; |
|
296 try { |
|
297 cursor = cr.query(mHistoryUriWithProfile, |
|
298 new String[] { History.VISITS }, |
|
299 History.URL + " = ?", |
|
300 new String[] { uri }, |
|
301 null); |
|
302 |
|
303 if (cursor.moveToFirst()) { |
|
304 oldVisits = cursor.getInt(0); |
|
305 } |
|
306 } finally { |
|
307 if (cursor != null) |
|
308 cursor.close(); |
|
309 } |
|
310 |
|
311 ContentValues values = new ContentValues(); |
|
312 values.put(History.DATE_LAST_VISITED, date); |
|
313 values.put(History.VISITS, oldVisits + visits); |
|
314 if (title != null) { |
|
315 values.put(History.TITLE, title); |
|
316 } |
|
317 |
|
318 cr.update(mHistoryUriWithProfile, |
|
319 values, |
|
320 History.URL + " = ?", |
|
321 new String[] { uri }); |
|
322 } |
|
323 |
|
324 @Override |
|
325 public Cursor getAllVisitedHistory(ContentResolver cr) { |
|
326 Cursor c = cr.query(mHistoryUriWithProfile, |
|
327 new String[] { History.URL }, |
|
328 History.VISITS + " > 0", |
|
329 null, |
|
330 null); |
|
331 |
|
332 return new LocalDBCursor(c); |
|
333 } |
|
334 |
|
335 @Override |
|
336 public Cursor getRecentHistory(ContentResolver cr, int limit) { |
|
337 Cursor c = cr.query(combinedUriWithLimit(limit), |
|
338 new String[] { Combined._ID, |
|
339 Combined.BOOKMARK_ID, |
|
340 Combined.HISTORY_ID, |
|
341 Combined.URL, |
|
342 Combined.TITLE, |
|
343 Combined.DISPLAY, |
|
344 Combined.DATE_LAST_VISITED, |
|
345 Combined.VISITS }, |
|
346 History.DATE_LAST_VISITED + " > 0", |
|
347 null, |
|
348 History.DATE_LAST_VISITED + " DESC"); |
|
349 |
|
350 return new LocalDBCursor(c); |
|
351 } |
|
352 |
|
353 @Override |
|
354 public void expireHistory(ContentResolver cr, ExpirePriority priority) { |
|
355 Uri url = mHistoryExpireUriWithProfile; |
|
356 url = url.buildUpon().appendQueryParameter(BrowserContract.PARAM_EXPIRE_PRIORITY, priority.toString()).build(); |
|
357 cr.delete(url, null, null); |
|
358 } |
|
359 |
|
360 @Override |
|
361 public void removeHistoryEntry(ContentResolver cr, int id) { |
|
362 cr.delete(mHistoryUriWithProfile, |
|
363 History._ID + " = ?", |
|
364 new String[] { String.valueOf(id) }); |
|
365 } |
|
366 |
|
367 @Override |
|
368 public void removeHistoryEntry(ContentResolver cr, String url) { |
|
369 int deleted = cr.delete(mHistoryUriWithProfile, |
|
370 History.URL + " = ?", |
|
371 new String[] { url }); |
|
372 } |
|
373 |
|
374 @Override |
|
375 public void clearHistory(ContentResolver cr) { |
|
376 cr.delete(mHistoryUriWithProfile, null, null); |
|
377 } |
|
378 |
|
379 @Override |
|
380 public Cursor getBookmarksInFolder(ContentResolver cr, long folderId) { |
|
381 Cursor c = null; |
|
382 boolean addDesktopFolder = false; |
|
383 |
|
384 // We always want to show mobile bookmarks in the root view. |
|
385 if (folderId == Bookmarks.FIXED_ROOT_ID) { |
|
386 folderId = getFolderIdFromGuid(cr, Bookmarks.MOBILE_FOLDER_GUID); |
|
387 |
|
388 // We'll add a fake "Desktop Bookmarks" folder to the root view if desktop |
|
389 // bookmarks exist, so that the user can still access non-mobile bookmarks. |
|
390 addDesktopFolder = desktopBookmarksExist(cr); |
|
391 } |
|
392 |
|
393 if (folderId == Bookmarks.FAKE_DESKTOP_FOLDER_ID) { |
|
394 // Since the "Desktop Bookmarks" folder doesn't actually exist, we |
|
395 // just fake it by querying specifically certain known desktop folders. |
|
396 c = cr.query(mBookmarksUriWithProfile, |
|
397 DEFAULT_BOOKMARK_COLUMNS, |
|
398 Bookmarks.GUID + " = ? OR " + |
|
399 Bookmarks.GUID + " = ? OR " + |
|
400 Bookmarks.GUID + " = ?", |
|
401 new String[] { Bookmarks.TOOLBAR_FOLDER_GUID, |
|
402 Bookmarks.MENU_FOLDER_GUID, |
|
403 Bookmarks.UNFILED_FOLDER_GUID }, |
|
404 null); |
|
405 } else { |
|
406 // Right now, we only support showing folder and bookmark type of |
|
407 // entries. We should add support for other types though (bug 737024) |
|
408 c = cr.query(mBookmarksUriWithProfile, |
|
409 DEFAULT_BOOKMARK_COLUMNS, |
|
410 Bookmarks.PARENT + " = ? AND " + |
|
411 "(" + Bookmarks.TYPE + " = ? OR " + |
|
412 "(" + Bookmarks.TYPE + " = ? AND " + Bookmarks.URL + " IS NOT NULL))", |
|
413 new String[] { String.valueOf(folderId), |
|
414 String.valueOf(Bookmarks.TYPE_FOLDER), |
|
415 String.valueOf(Bookmarks.TYPE_BOOKMARK) }, |
|
416 null); |
|
417 } |
|
418 |
|
419 if (addDesktopFolder) { |
|
420 // Wrap cursor to add fake desktop bookmarks and reading list folders |
|
421 c = new SpecialFoldersCursorWrapper(c, addDesktopFolder); |
|
422 } |
|
423 |
|
424 return new LocalDBCursor(c); |
|
425 } |
|
426 |
|
427 @Override |
|
428 public Cursor getReadingList(ContentResolver cr) { |
|
429 return cr.query(mReadingListUriWithProfile, |
|
430 ReadingListItems.DEFAULT_PROJECTION, |
|
431 null, |
|
432 null, |
|
433 null); |
|
434 } |
|
435 |
|
436 |
|
437 // Returns true if any desktop bookmarks exist, which will be true if the user |
|
438 // has set up sync at one point, or done a profile migration from XUL fennec. |
|
439 private boolean desktopBookmarksExist(ContentResolver cr) { |
|
440 if (mDesktopBookmarksExist != null) |
|
441 return mDesktopBookmarksExist; |
|
442 |
|
443 Cursor c = null; |
|
444 int count = 0; |
|
445 try { |
|
446 // Check to see if there are any bookmarks in one of our three |
|
447 // fixed "Desktop Boomarks" folders. |
|
448 c = cr.query(bookmarksUriWithLimit(1), |
|
449 new String[] { Bookmarks._ID }, |
|
450 Bookmarks.PARENT + " = ? OR " + |
|
451 Bookmarks.PARENT + " = ? OR " + |
|
452 Bookmarks.PARENT + " = ?", |
|
453 new String[] { String.valueOf(getFolderIdFromGuid(cr, Bookmarks.TOOLBAR_FOLDER_GUID)), |
|
454 String.valueOf(getFolderIdFromGuid(cr, Bookmarks.MENU_FOLDER_GUID)), |
|
455 String.valueOf(getFolderIdFromGuid(cr, Bookmarks.UNFILED_FOLDER_GUID)) }, |
|
456 null); |
|
457 count = c.getCount(); |
|
458 } finally { |
|
459 if (c != null) |
|
460 c.close(); |
|
461 } |
|
462 |
|
463 // Cache result for future queries |
|
464 mDesktopBookmarksExist = (count > 0); |
|
465 return mDesktopBookmarksExist; |
|
466 } |
|
467 |
|
468 @Override |
|
469 public int getReadingListCount(ContentResolver cr) { |
|
470 Cursor c = null; |
|
471 try { |
|
472 c = cr.query(mReadingListUriWithProfile, |
|
473 new String[] { ReadingListItems._ID }, |
|
474 null, |
|
475 null, |
|
476 null); |
|
477 return c.getCount(); |
|
478 } finally { |
|
479 if (c != null) { |
|
480 c.close(); |
|
481 } |
|
482 } |
|
483 } |
|
484 |
|
485 @Override |
|
486 public boolean isBookmark(ContentResolver cr, String uri) { |
|
487 // This method is about normal bookmarks, not the Reading List. |
|
488 Cursor c = null; |
|
489 try { |
|
490 c = cr.query(bookmarksUriWithLimit(1), |
|
491 new String[] { Bookmarks._ID }, |
|
492 Bookmarks.URL + " = ? AND " + |
|
493 Bookmarks.PARENT + " != ? AND " + |
|
494 Bookmarks.PARENT + " != ?", |
|
495 new String[] { uri, |
|
496 String.valueOf(Bookmarks.FIXED_READING_LIST_ID), |
|
497 String.valueOf(Bookmarks.FIXED_PINNED_LIST_ID) }, |
|
498 Bookmarks.URL); |
|
499 return c.getCount() > 0; |
|
500 } catch (NullPointerException e) { |
|
501 Log.e(LOGTAG, "NullPointerException in isBookmark"); |
|
502 } finally { |
|
503 if (c != null) |
|
504 c.close(); |
|
505 } |
|
506 |
|
507 return false; |
|
508 } |
|
509 |
|
510 @Override |
|
511 public boolean isReadingListItem(ContentResolver cr, String uri) { |
|
512 Cursor c = null; |
|
513 try { |
|
514 c = cr.query(mReadingListUriWithProfile, |
|
515 new String[] { ReadingListItems._ID }, |
|
516 ReadingListItems.URL + " = ? ", |
|
517 new String[] { uri }, |
|
518 null); |
|
519 return c.getCount() > 0; |
|
520 } catch (NullPointerException e) { |
|
521 Log.e(LOGTAG, "NullPointerException in isReadingListItem"); |
|
522 } finally { |
|
523 if (c != null) |
|
524 c.close(); |
|
525 } |
|
526 |
|
527 return false; |
|
528 } |
|
529 |
|
530 /** |
|
531 * For a given URI, we want to return a number of things: |
|
532 * |
|
533 * * Is this URI the URI of a bookmark? |
|
534 * * ... a reading list item? |
|
535 * |
|
536 * This will expand as necessary to eliminate multiple consecutive queries. |
|
537 */ |
|
538 @Override |
|
539 public int getItemFlags(ContentResolver cr, String uri) { |
|
540 final Cursor c = cr.query(mFlagsUriWithProfile, |
|
541 null, |
|
542 null, |
|
543 new String[] { uri }, |
|
544 null); |
|
545 if (c == null) { |
|
546 return 0; |
|
547 } |
|
548 |
|
549 try { |
|
550 // This should never fail: it returns a single `flags` row. |
|
551 c.moveToFirst(); |
|
552 return Bookmarks.FLAG_SUCCESS | c.getInt(0); |
|
553 } finally { |
|
554 c.close(); |
|
555 } |
|
556 } |
|
557 |
|
558 @Override |
|
559 public String getUrlForKeyword(ContentResolver cr, String keyword) { |
|
560 Cursor c = null; |
|
561 try { |
|
562 c = cr.query(mBookmarksUriWithProfile, |
|
563 new String[] { Bookmarks.URL }, |
|
564 Bookmarks.KEYWORD + " = ?", |
|
565 new String[] { keyword }, |
|
566 null); |
|
567 |
|
568 if (c.moveToFirst()) |
|
569 return c.getString(c.getColumnIndexOrThrow(Bookmarks.URL)); |
|
570 } finally { |
|
571 if (c != null) |
|
572 c.close(); |
|
573 } |
|
574 |
|
575 return null; |
|
576 } |
|
577 |
|
578 private synchronized long getFolderIdFromGuid(ContentResolver cr, String guid) { |
|
579 if (mFolderIdMap.containsKey(guid)) |
|
580 return mFolderIdMap.get(guid); |
|
581 |
|
582 long folderId = -1; |
|
583 Cursor c = null; |
|
584 |
|
585 try { |
|
586 c = cr.query(mBookmarksUriWithProfile, |
|
587 new String[] { Bookmarks._ID }, |
|
588 Bookmarks.GUID + " = ?", |
|
589 new String[] { guid }, |
|
590 null); |
|
591 |
|
592 if (c.moveToFirst()) |
|
593 folderId = c.getLong(c.getColumnIndexOrThrow(Bookmarks._ID)); |
|
594 } finally { |
|
595 if (c != null) |
|
596 c.close(); |
|
597 } |
|
598 |
|
599 mFolderIdMap.put(guid, folderId); |
|
600 return folderId; |
|
601 } |
|
602 |
|
603 /** |
|
604 * Find parents of records that match the provided criteria, and bump their |
|
605 * modified timestamp. |
|
606 */ |
|
607 protected void bumpParents(ContentResolver cr, String param, String value) { |
|
608 ContentValues values = new ContentValues(); |
|
609 values.put(Bookmarks.DATE_MODIFIED, System.currentTimeMillis()); |
|
610 |
|
611 String where = param + " = ?"; |
|
612 String[] args = new String[] { value }; |
|
613 int updated = cr.update(mParentsUriWithProfile, values, where, args); |
|
614 debug("Updated " + updated + " rows to new modified time."); |
|
615 } |
|
616 |
|
617 private void addBookmarkItem(ContentResolver cr, String title, String uri, long folderId) { |
|
618 final long now = System.currentTimeMillis(); |
|
619 ContentValues values = new ContentValues(); |
|
620 values.put(Browser.BookmarkColumns.TITLE, title); |
|
621 values.put(Bookmarks.URL, uri); |
|
622 values.put(Bookmarks.PARENT, folderId); |
|
623 values.put(Bookmarks.DATE_MODIFIED, now); |
|
624 |
|
625 // Get the page's favicon ID from the history table |
|
626 Cursor c = null; |
|
627 try { |
|
628 c = cr.query(mHistoryUriWithProfile, |
|
629 new String[] { History.FAVICON_ID }, |
|
630 History.URL + " = ?", |
|
631 new String[] { uri }, |
|
632 null); |
|
633 |
|
634 if (c.moveToFirst()) { |
|
635 int columnIndex = c.getColumnIndexOrThrow(History.FAVICON_ID); |
|
636 if (!c.isNull(columnIndex)) |
|
637 values.put(Bookmarks.FAVICON_ID, c.getLong(columnIndex)); |
|
638 } |
|
639 } finally { |
|
640 if (c != null) |
|
641 c.close(); |
|
642 } |
|
643 |
|
644 // Restore deleted record if possible |
|
645 values.put(Bookmarks.IS_DELETED, 0); |
|
646 |
|
647 final Uri bookmarksWithInsert = mBookmarksUriWithProfile.buildUpon() |
|
648 .appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true") |
|
649 .build(); |
|
650 cr.update(bookmarksWithInsert, |
|
651 values, |
|
652 Bookmarks.URL + " = ? AND " + |
|
653 Bookmarks.PARENT + " = " + folderId, |
|
654 new String[] { uri }); |
|
655 |
|
656 // Bump parent modified time using its ID. |
|
657 debug("Bumping parent modified time for addition to: " + folderId); |
|
658 final String where = Bookmarks._ID + " = ?"; |
|
659 final String[] args = new String[] { String.valueOf(folderId) }; |
|
660 |
|
661 ContentValues bumped = new ContentValues(); |
|
662 bumped.put(Bookmarks.DATE_MODIFIED, now); |
|
663 |
|
664 final int updated = cr.update(mBookmarksUriWithProfile, bumped, where, args); |
|
665 debug("Updated " + updated + " rows to new modified time."); |
|
666 } |
|
667 |
|
668 @Override |
|
669 public void addBookmark(ContentResolver cr, String title, String uri) { |
|
670 long folderId = getFolderIdFromGuid(cr, Bookmarks.MOBILE_FOLDER_GUID); |
|
671 addBookmarkItem(cr, title, uri, folderId); |
|
672 } |
|
673 |
|
674 @Override |
|
675 public void removeBookmark(ContentResolver cr, int id) { |
|
676 Uri contentUri = mBookmarksUriWithProfile; |
|
677 |
|
678 // Do this now so that the item still exists! |
|
679 final String idString = String.valueOf(id); |
|
680 bumpParents(cr, Bookmarks._ID, idString); |
|
681 |
|
682 final String[] idArgs = new String[] { idString }; |
|
683 final String idEquals = Bookmarks._ID + " = ?"; |
|
684 cr.delete(contentUri, idEquals, idArgs); |
|
685 } |
|
686 |
|
687 @Override |
|
688 public void removeBookmarksWithURL(ContentResolver cr, String uri) { |
|
689 Uri contentUri = mBookmarksUriWithProfile; |
|
690 |
|
691 // Do this now so that the items still exist! |
|
692 bumpParents(cr, Bookmarks.URL, uri); |
|
693 |
|
694 // Toggling bookmark on an URL should not affect the items in the reading list or pinned sites. |
|
695 final String[] urlArgs = new String[] { uri, String.valueOf(Bookmarks.FIXED_READING_LIST_ID), String.valueOf(Bookmarks.FIXED_PINNED_LIST_ID) }; |
|
696 final String urlEquals = Bookmarks.URL + " = ? AND " + Bookmarks.PARENT + " != ? AND " + Bookmarks.PARENT + " != ? "; |
|
697 |
|
698 cr.delete(contentUri, urlEquals, urlArgs); |
|
699 } |
|
700 |
|
701 @Override |
|
702 public void addReadingListItem(ContentResolver cr, ContentValues values) { |
|
703 // Check that required fields are present. |
|
704 for (String field: ReadingListItems.REQUIRED_FIELDS) { |
|
705 if (!values.containsKey(field)) { |
|
706 throw new IllegalArgumentException("Missing required field for reading list item: " + field); |
|
707 } |
|
708 } |
|
709 |
|
710 // Clear delete flag if necessary |
|
711 values.put(ReadingListItems.IS_DELETED, 0); |
|
712 |
|
713 // Restore deleted record if possible |
|
714 final Uri insertUri = mReadingListUriWithProfile |
|
715 .buildUpon() |
|
716 .appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true") |
|
717 .build(); |
|
718 |
|
719 final int updated = cr.update(insertUri, |
|
720 values, |
|
721 ReadingListItems.URL + " = ? ", |
|
722 new String[] { values.getAsString(ReadingListItems.URL) }); |
|
723 |
|
724 debug("Updated " + updated + " rows to new modified time."); |
|
725 } |
|
726 |
|
727 @Override |
|
728 public void removeReadingListItemWithURL(ContentResolver cr, String uri) { |
|
729 cr.delete(mReadingListUriWithProfile, ReadingListItems.URL + " = ? ", new String[] { uri }); |
|
730 } |
|
731 |
|
732 @Override |
|
733 public void removeReadingListItem(ContentResolver cr, int id) { |
|
734 cr.delete(mReadingListUriWithProfile, ReadingListItems._ID + " = ? ", new String[] { String.valueOf(id) }); |
|
735 } |
|
736 |
|
737 @Override |
|
738 public void registerBookmarkObserver(ContentResolver cr, ContentObserver observer) { |
|
739 cr.registerContentObserver(mBookmarksUriWithProfile, false, observer); |
|
740 } |
|
741 |
|
742 @Override |
|
743 public void registerHistoryObserver(ContentResolver cr, ContentObserver observer) { |
|
744 cr.registerContentObserver(mHistoryUriWithProfile, false, observer); |
|
745 } |
|
746 |
|
747 @Override |
|
748 public void updateBookmark(ContentResolver cr, int id, String uri, String title, String keyword) { |
|
749 ContentValues values = new ContentValues(); |
|
750 values.put(Browser.BookmarkColumns.TITLE, title); |
|
751 values.put(Bookmarks.URL, uri); |
|
752 values.put(Bookmarks.KEYWORD, keyword); |
|
753 values.put(Bookmarks.DATE_MODIFIED, System.currentTimeMillis()); |
|
754 |
|
755 cr.update(mBookmarksUriWithProfile, |
|
756 values, |
|
757 Bookmarks._ID + " = ?", |
|
758 new String[] { String.valueOf(id) }); |
|
759 } |
|
760 |
|
761 /** |
|
762 * Get the favicon from the database, if any, associated with the given favicon URL. (That is, |
|
763 * the URL of the actual favicon image, not the URL of the page with which the favicon is associated.) |
|
764 * @param cr The ContentResolver to use. |
|
765 * @param faviconURL The URL of the favicon to fetch from the database. |
|
766 * @return The decoded Bitmap from the database, if any. null if none is stored. |
|
767 */ |
|
768 @Override |
|
769 public LoadFaviconResult getFaviconForUrl(ContentResolver cr, String faviconURL) { |
|
770 Cursor c = null; |
|
771 byte[] b = null; |
|
772 |
|
773 try { |
|
774 c = cr.query(mFaviconsUriWithProfile, |
|
775 new String[] { Favicons.DATA }, |
|
776 Favicons.URL + " = ? AND " + Favicons.DATA + " IS NOT NULL", |
|
777 new String[] { faviconURL }, |
|
778 null); |
|
779 |
|
780 if (!c.moveToFirst()) { |
|
781 return null; |
|
782 } |
|
783 |
|
784 final int faviconIndex = c.getColumnIndexOrThrow(Favicons.DATA); |
|
785 b = c.getBlob(faviconIndex); |
|
786 } finally { |
|
787 if (c != null) { |
|
788 c.close(); |
|
789 } |
|
790 } |
|
791 |
|
792 if (b == null) { |
|
793 return null; |
|
794 } |
|
795 |
|
796 return FaviconDecoder.decodeFavicon(b); |
|
797 } |
|
798 |
|
799 @Override |
|
800 public String getFaviconUrlForHistoryUrl(ContentResolver cr, String uri) { |
|
801 Cursor c = null; |
|
802 |
|
803 try { |
|
804 c = cr.query(mHistoryUriWithProfile, |
|
805 new String[] { History.FAVICON_URL }, |
|
806 Combined.URL + " = ?", |
|
807 new String[] { uri }, |
|
808 null); |
|
809 |
|
810 if (c.moveToFirst()) |
|
811 return c.getString(c.getColumnIndexOrThrow(History.FAVICON_URL)); |
|
812 } finally { |
|
813 if (c != null) |
|
814 c.close(); |
|
815 } |
|
816 |
|
817 return null; |
|
818 } |
|
819 |
|
820 @Override |
|
821 public void updateFaviconForUrl(ContentResolver cr, String pageUri, |
|
822 byte[] encodedFavicon, String faviconUri) { |
|
823 ContentValues values = new ContentValues(); |
|
824 values.put(Favicons.URL, faviconUri); |
|
825 values.put(Favicons.PAGE_URL, pageUri); |
|
826 values.put(Favicons.DATA, encodedFavicon); |
|
827 |
|
828 // Update or insert |
|
829 Uri faviconsUri = getAllFaviconsUri().buildUpon(). |
|
830 appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build(); |
|
831 |
|
832 cr.update(faviconsUri, |
|
833 values, |
|
834 Favicons.URL + " = ?", |
|
835 new String[] { faviconUri }); |
|
836 } |
|
837 |
|
838 @Override |
|
839 public void updateThumbnailForUrl(ContentResolver cr, String uri, |
|
840 BitmapDrawable thumbnail) { |
|
841 Bitmap bitmap = thumbnail.getBitmap(); |
|
842 |
|
843 byte[] data = null; |
|
844 ByteArrayOutputStream stream = new ByteArrayOutputStream(); |
|
845 if (bitmap.compress(Bitmap.CompressFormat.PNG, 0, stream)) { |
|
846 data = stream.toByteArray(); |
|
847 } else { |
|
848 Log.w(LOGTAG, "Favicon compression failed."); |
|
849 } |
|
850 |
|
851 ContentValues values = new ContentValues(); |
|
852 values.put(Thumbnails.DATA, data); |
|
853 values.put(Thumbnails.URL, uri); |
|
854 |
|
855 Uri thumbnailsUri = mThumbnailsUriWithProfile.buildUpon(). |
|
856 appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build(); |
|
857 cr.update(thumbnailsUri, |
|
858 values, |
|
859 Thumbnails.URL + " = ?", |
|
860 new String[] { uri }); |
|
861 } |
|
862 |
|
863 @Override |
|
864 public byte[] getThumbnailForUrl(ContentResolver cr, String uri) { |
|
865 Cursor c = null; |
|
866 byte[] b = null; |
|
867 try { |
|
868 c = cr.query(mThumbnailsUriWithProfile, |
|
869 new String[]{ Thumbnails.DATA }, |
|
870 Thumbnails.URL + " = ? AND " + Thumbnails.DATA + " IS NOT NULL", |
|
871 new String[]{ uri }, |
|
872 null); |
|
873 |
|
874 if (!c.moveToFirst()) { |
|
875 return null; |
|
876 } |
|
877 |
|
878 int thumbnailIndex = c.getColumnIndexOrThrow(Thumbnails.DATA); |
|
879 b = c.getBlob(thumbnailIndex); |
|
880 } finally { |
|
881 if (c != null) { |
|
882 c.close(); |
|
883 } |
|
884 } |
|
885 |
|
886 return b; |
|
887 } |
|
888 |
|
889 /** |
|
890 * Query for non-null thumbnails matching the provided <code>urls</code>. |
|
891 * The returned cursor will have no more than, but possibly fewer than, |
|
892 * the requested number of thumbnails. |
|
893 * |
|
894 * Returns null if the provided list of URLs is empty or null. |
|
895 */ |
|
896 @Override |
|
897 public Cursor getThumbnailsForUrls(ContentResolver cr, List<String> urls) { |
|
898 if (urls == null) { |
|
899 return null; |
|
900 } |
|
901 |
|
902 int urlCount = urls.size(); |
|
903 if (urlCount == 0) { |
|
904 return null; |
|
905 } |
|
906 |
|
907 // Don't match against null thumbnails. |
|
908 StringBuilder selection = new StringBuilder( |
|
909 Thumbnails.DATA + " IS NOT NULL AND " + |
|
910 Thumbnails.URL + " IN (" |
|
911 ); |
|
912 |
|
913 // Compute a (?, ?, ?) sequence to match the provided URLs. |
|
914 int i = 1; |
|
915 while (i++ < urlCount) { |
|
916 selection.append("?, "); |
|
917 } |
|
918 selection.append("?)"); |
|
919 |
|
920 String[] selectionArgs = urls.toArray(new String[urlCount]); |
|
921 |
|
922 return cr.query(mThumbnailsUriWithProfile, |
|
923 new String[] { Thumbnails.URL, Thumbnails.DATA }, |
|
924 selection.toString(), |
|
925 selectionArgs, |
|
926 null); |
|
927 } |
|
928 |
|
929 @Override |
|
930 public void removeThumbnails(ContentResolver cr) { |
|
931 cr.delete(mThumbnailsUriWithProfile, null, null); |
|
932 } |
|
933 |
|
934 // Utility function for updating existing history using batch operations |
|
935 public void updateHistoryInBatch(ContentResolver cr, |
|
936 Collection<ContentProviderOperation> operations, |
|
937 String url, String title, |
|
938 long date, int visits) { |
|
939 Cursor cursor = null; |
|
940 |
|
941 try { |
|
942 final String[] projection = new String[] { |
|
943 History._ID, |
|
944 History.VISITS, |
|
945 History.DATE_LAST_VISITED |
|
946 }; |
|
947 |
|
948 // We need to get the old visit count. |
|
949 cursor = cr.query(getAllHistoryUri(), |
|
950 projection, |
|
951 History.URL + " = ?", |
|
952 new String[] { url }, |
|
953 null); |
|
954 |
|
955 ContentValues values = new ContentValues(); |
|
956 |
|
957 // Restore deleted record if possible |
|
958 values.put(History.IS_DELETED, 0); |
|
959 |
|
960 if (cursor.moveToFirst()) { |
|
961 int visitsCol = cursor.getColumnIndexOrThrow(History.VISITS); |
|
962 int dateCol = cursor.getColumnIndexOrThrow(History.DATE_LAST_VISITED); |
|
963 int oldVisits = cursor.getInt(visitsCol); |
|
964 long oldDate = cursor.getLong(dateCol); |
|
965 values.put(History.VISITS, oldVisits + visits); |
|
966 // Only update last visited if newer. |
|
967 if (date > oldDate) { |
|
968 values.put(History.DATE_LAST_VISITED, date); |
|
969 } |
|
970 } else { |
|
971 values.put(History.VISITS, visits); |
|
972 values.put(History.DATE_LAST_VISITED, date); |
|
973 } |
|
974 if (title != null) { |
|
975 values.put(History.TITLE, title); |
|
976 } |
|
977 values.put(History.URL, url); |
|
978 |
|
979 Uri historyUri = getAllHistoryUri().buildUpon(). |
|
980 appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build(); |
|
981 |
|
982 // Update or insert |
|
983 ContentProviderOperation.Builder builder = |
|
984 ContentProviderOperation.newUpdate(historyUri); |
|
985 builder.withSelection(History.URL + " = ?", new String[] { url }); |
|
986 builder.withValues(values); |
|
987 |
|
988 // Queue the operation |
|
989 operations.add(builder.build()); |
|
990 } finally { |
|
991 if (cursor != null) |
|
992 cursor.close(); |
|
993 } |
|
994 } |
|
995 |
|
996 public void updateBookmarkInBatch(ContentResolver cr, |
|
997 Collection<ContentProviderOperation> operations, |
|
998 String url, String title, String guid, |
|
999 long parent, long added, |
|
1000 long modified, long position, |
|
1001 String keyword, int type) { |
|
1002 ContentValues values = new ContentValues(); |
|
1003 if (title == null && url != null) { |
|
1004 title = url; |
|
1005 } |
|
1006 if (title != null) { |
|
1007 values.put(Bookmarks.TITLE, title); |
|
1008 } |
|
1009 if (url != null) { |
|
1010 values.put(Bookmarks.URL, url); |
|
1011 } |
|
1012 if (guid != null) { |
|
1013 values.put(SyncColumns.GUID, guid); |
|
1014 } |
|
1015 if (keyword != null) { |
|
1016 values.put(Bookmarks.KEYWORD, keyword); |
|
1017 } |
|
1018 if (added > 0) { |
|
1019 values.put(SyncColumns.DATE_CREATED, added); |
|
1020 } |
|
1021 if (modified > 0) { |
|
1022 values.put(SyncColumns.DATE_MODIFIED, modified); |
|
1023 } |
|
1024 values.put(Bookmarks.POSITION, position); |
|
1025 // Restore deleted record if possible |
|
1026 values.put(Bookmarks.IS_DELETED, 0); |
|
1027 |
|
1028 // This assumes no "real" folder has a negative ID. Only |
|
1029 // things like the reading list folder do. |
|
1030 if (parent < 0) { |
|
1031 parent = getFolderIdFromGuid(cr, Bookmarks.MOBILE_FOLDER_GUID); |
|
1032 } |
|
1033 values.put(Bookmarks.PARENT, parent); |
|
1034 values.put(Bookmarks.TYPE, type); |
|
1035 |
|
1036 Uri bookmarkUri = getAllBookmarksUri().buildUpon(). |
|
1037 appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build(); |
|
1038 // Update or insert |
|
1039 ContentProviderOperation.Builder builder = |
|
1040 ContentProviderOperation.newUpdate(bookmarkUri); |
|
1041 if (url != null) { |
|
1042 // Bookmarks are defined by their URL and Folder. |
|
1043 builder.withSelection(Bookmarks.URL + " = ? AND " |
|
1044 + Bookmarks.PARENT + " = ? AND " |
|
1045 + Bookmarks.PARENT + " != ?", |
|
1046 new String[] { url, |
|
1047 Long.toString(parent), |
|
1048 String.valueOf(Bookmarks.FIXED_READING_LIST_ID) |
|
1049 }); |
|
1050 } else if (title != null) { |
|
1051 // Or their title and parent folder. (Folders!) |
|
1052 builder.withSelection(Bookmarks.TITLE + " = ? AND " |
|
1053 + Bookmarks.PARENT + " = ? AND " |
|
1054 + Bookmarks.PARENT + " != ?", |
|
1055 new String[] { title, |
|
1056 Long.toString(parent), |
|
1057 String.valueOf(Bookmarks.FIXED_READING_LIST_ID) |
|
1058 }); |
|
1059 } else if (type == Bookmarks.TYPE_SEPARATOR) { |
|
1060 // Or their their position (seperators) |
|
1061 builder.withSelection(Bookmarks.POSITION + " = ? AND " |
|
1062 + Bookmarks.PARENT + " = ? AND " |
|
1063 + Bookmarks.PARENT + " != ?", |
|
1064 new String[] { Long.toString(position), |
|
1065 Long.toString(parent), |
|
1066 String.valueOf(Bookmarks.FIXED_READING_LIST_ID) |
|
1067 }); |
|
1068 } else { |
|
1069 Log.e(LOGTAG, "Bookmark entry without url or title and not a seperator, not added."); |
|
1070 } |
|
1071 builder.withValues(values); |
|
1072 |
|
1073 // Queue the operation |
|
1074 operations.add(builder.build()); |
|
1075 } |
|
1076 |
|
1077 public void updateFaviconInBatch(ContentResolver cr, |
|
1078 Collection<ContentProviderOperation> operations, |
|
1079 String url, String faviconUrl, |
|
1080 String faviconGuid, byte[] data) { |
|
1081 ContentValues values = new ContentValues(); |
|
1082 values.put(Favicons.DATA, data); |
|
1083 values.put(Favicons.PAGE_URL, url); |
|
1084 if (faviconUrl != null) { |
|
1085 values.put(Favicons.URL, faviconUrl); |
|
1086 } |
|
1087 |
|
1088 // Update or insert |
|
1089 Uri faviconsUri = getAllFaviconsUri().buildUpon(). |
|
1090 appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build(); |
|
1091 // Update or insert |
|
1092 ContentProviderOperation.Builder builder = |
|
1093 ContentProviderOperation.newUpdate(faviconsUri); |
|
1094 builder.withValues(values); |
|
1095 builder.withSelection(Favicons.PAGE_URL + " = ?", new String[] { url }); |
|
1096 // Queue the operation |
|
1097 operations.add(builder.build()); |
|
1098 } |
|
1099 |
|
1100 // This wrapper adds a fake "Desktop Bookmarks" folder entry to the |
|
1101 // beginning of the cursor's data set. |
|
1102 private class SpecialFoldersCursorWrapper extends CursorWrapper { |
|
1103 private int mIndexOffset; |
|
1104 |
|
1105 private int mDesktopBookmarksIndex = -1; |
|
1106 |
|
1107 private boolean mAtDesktopBookmarksPosition = false; |
|
1108 |
|
1109 public SpecialFoldersCursorWrapper(Cursor c, boolean showDesktopBookmarks) { |
|
1110 super(c); |
|
1111 |
|
1112 mIndexOffset = 0; |
|
1113 |
|
1114 if (showDesktopBookmarks) { |
|
1115 mDesktopBookmarksIndex = mIndexOffset; |
|
1116 mIndexOffset++; |
|
1117 } |
|
1118 } |
|
1119 |
|
1120 @Override |
|
1121 public int getCount() { |
|
1122 return super.getCount() + mIndexOffset; |
|
1123 } |
|
1124 |
|
1125 @Override |
|
1126 public boolean moveToPosition(int position) { |
|
1127 mAtDesktopBookmarksPosition = (mDesktopBookmarksIndex == position); |
|
1128 |
|
1129 if (mAtDesktopBookmarksPosition) |
|
1130 return true; |
|
1131 |
|
1132 return super.moveToPosition(position - mIndexOffset); |
|
1133 } |
|
1134 |
|
1135 @Override |
|
1136 public long getLong(int columnIndex) { |
|
1137 if (!mAtDesktopBookmarksPosition) |
|
1138 return super.getLong(columnIndex); |
|
1139 |
|
1140 if (columnIndex == getColumnIndex(Bookmarks.PARENT)) { |
|
1141 return Bookmarks.FIXED_ROOT_ID; |
|
1142 } |
|
1143 |
|
1144 return -1; |
|
1145 } |
|
1146 |
|
1147 @Override |
|
1148 public int getInt(int columnIndex) { |
|
1149 if (!mAtDesktopBookmarksPosition) |
|
1150 return super.getInt(columnIndex); |
|
1151 |
|
1152 if (columnIndex == getColumnIndex(Bookmarks._ID) && mAtDesktopBookmarksPosition) |
|
1153 return Bookmarks.FAKE_DESKTOP_FOLDER_ID; |
|
1154 |
|
1155 if (columnIndex == getColumnIndex(Bookmarks.TYPE)) |
|
1156 return Bookmarks.TYPE_FOLDER; |
|
1157 |
|
1158 return -1; |
|
1159 } |
|
1160 |
|
1161 @Override |
|
1162 public String getString(int columnIndex) { |
|
1163 if (!mAtDesktopBookmarksPosition) |
|
1164 return super.getString(columnIndex); |
|
1165 |
|
1166 if (columnIndex == getColumnIndex(Bookmarks.GUID) && mAtDesktopBookmarksPosition) |
|
1167 return Bookmarks.FAKE_DESKTOP_FOLDER_GUID; |
|
1168 |
|
1169 return ""; |
|
1170 } |
|
1171 } |
|
1172 |
|
1173 private static class LocalDBCursor extends CursorWrapper { |
|
1174 public LocalDBCursor(Cursor c) { |
|
1175 super(c); |
|
1176 } |
|
1177 |
|
1178 private String translateColumnName(String columnName) { |
|
1179 if (columnName.equals(BrowserDB.URLColumns.URL)) { |
|
1180 columnName = URLColumns.URL; |
|
1181 } else if (columnName.equals(BrowserDB.URLColumns.TITLE)) { |
|
1182 columnName = URLColumns.TITLE; |
|
1183 } else if (columnName.equals(BrowserDB.URLColumns.FAVICON)) { |
|
1184 columnName = FaviconColumns.FAVICON; |
|
1185 } else if (columnName.equals(BrowserDB.URLColumns.DATE_LAST_VISITED)) { |
|
1186 columnName = History.DATE_LAST_VISITED; |
|
1187 } else if (columnName.equals(BrowserDB.URLColumns.VISITS)) { |
|
1188 columnName = History.VISITS; |
|
1189 } |
|
1190 |
|
1191 return columnName; |
|
1192 } |
|
1193 |
|
1194 @Override |
|
1195 public int getColumnIndex(String columnName) { |
|
1196 return super.getColumnIndex(translateColumnName(columnName)); |
|
1197 } |
|
1198 |
|
1199 @Override |
|
1200 public int getColumnIndexOrThrow(String columnName) { |
|
1201 return super.getColumnIndexOrThrow(translateColumnName(columnName)); |
|
1202 } |
|
1203 } |
|
1204 |
|
1205 |
|
1206 @Override |
|
1207 public void pinSite(ContentResolver cr, String url, String title, int position) { |
|
1208 ContentValues values = new ContentValues(); |
|
1209 final long now = System.currentTimeMillis(); |
|
1210 values.put(Bookmarks.TITLE, title); |
|
1211 values.put(Bookmarks.URL, url); |
|
1212 values.put(Bookmarks.PARENT, Bookmarks.FIXED_PINNED_LIST_ID); |
|
1213 values.put(Bookmarks.DATE_MODIFIED, now); |
|
1214 values.put(Bookmarks.POSITION, position); |
|
1215 values.put(Bookmarks.IS_DELETED, 0); |
|
1216 |
|
1217 // We do an update-and-replace here without deleting any existing pins for the given URL. |
|
1218 // That means if the user pins a URL, then edits another thumbnail to use the same URL, |
|
1219 // we'll end up with two pins for that site. This is the intended behavior, which |
|
1220 // incidentally saves us a delete query. |
|
1221 Uri uri = mBookmarksUriWithProfile.buildUpon() |
|
1222 .appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build(); |
|
1223 cr.update(uri, |
|
1224 values, |
|
1225 Bookmarks.POSITION + " = ? AND " + |
|
1226 Bookmarks.PARENT + " = ?", |
|
1227 new String[] { Integer.toString(position), |
|
1228 String.valueOf(Bookmarks.FIXED_PINNED_LIST_ID) }); |
|
1229 } |
|
1230 |
|
1231 @Override |
|
1232 public Cursor getPinnedSites(ContentResolver cr, int limit) { |
|
1233 return cr.query(bookmarksUriWithLimit(limit), |
|
1234 new String[] { Bookmarks._ID, |
|
1235 Bookmarks.URL, |
|
1236 Bookmarks.TITLE, |
|
1237 Bookmarks.POSITION }, |
|
1238 Bookmarks.PARENT + " == ?", |
|
1239 new String[] { String.valueOf(Bookmarks.FIXED_PINNED_LIST_ID) }, |
|
1240 Bookmarks.POSITION + " ASC"); |
|
1241 } |
|
1242 |
|
1243 @Override |
|
1244 public void unpinSite(ContentResolver cr, int position) { |
|
1245 cr.delete(mBookmarksUriWithProfile, |
|
1246 Bookmarks.PARENT + " == ? AND " + Bookmarks.POSITION + " = ?", |
|
1247 new String[] { |
|
1248 String.valueOf(Bookmarks.FIXED_PINNED_LIST_ID), |
|
1249 Integer.toString(position) |
|
1250 }); |
|
1251 } |
|
1252 |
|
1253 @Override |
|
1254 public void unpinAllSites(ContentResolver cr) { |
|
1255 cr.delete(mBookmarksUriWithProfile, |
|
1256 Bookmarks.PARENT + " == ?", |
|
1257 new String[] { |
|
1258 String.valueOf(Bookmarks.FIXED_PINNED_LIST_ID) |
|
1259 }); |
|
1260 } |
|
1261 |
|
1262 @Override |
|
1263 public boolean isVisited(ContentResolver cr, String uri) { |
|
1264 int count = 0; |
|
1265 Cursor c = null; |
|
1266 |
|
1267 try { |
|
1268 c = cr.query(historyUriWithLimit(1), |
|
1269 new String[] { History._ID }, |
|
1270 History.URL + " = ?", |
|
1271 new String[] { uri }, |
|
1272 History.URL); |
|
1273 count = c.getCount(); |
|
1274 } catch (NullPointerException e) { |
|
1275 Log.e(LOGTAG, "NullPointerException in isVisited"); |
|
1276 } finally { |
|
1277 if (c != null) |
|
1278 c.close(); |
|
1279 } |
|
1280 |
|
1281 return (count > 0); |
|
1282 } |
|
1283 |
|
1284 public Cursor getBookmarkForUrl(ContentResolver cr, String url) { |
|
1285 Cursor c = cr.query(bookmarksUriWithLimit(1), |
|
1286 new String[] { Bookmarks._ID, |
|
1287 Bookmarks.URL, |
|
1288 Bookmarks.TITLE, |
|
1289 Bookmarks.KEYWORD }, |
|
1290 Bookmarks.URL + " = ?", |
|
1291 new String[] { url }, |
|
1292 null); |
|
1293 |
|
1294 if (c != null && c.getCount() == 0) { |
|
1295 c.close(); |
|
1296 c = null; |
|
1297 } |
|
1298 |
|
1299 return c; |
|
1300 } |
|
1301 } |