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 file, 4 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 6 package org.mozilla.gecko.db; 7 8 import java.lang.ref.WeakReference; 9 import java.util.ArrayList; 10 import java.util.Collections; 11 import java.util.HashMap; 12 import java.util.List; 13 import java.util.Map; 14 15 import org.mozilla.gecko.AboutPages; 16 import org.mozilla.gecko.GeckoProfile; 17 import org.mozilla.gecko.R; 18 import org.mozilla.gecko.db.BrowserContract.ActivityStreamBlocklist; 19 import org.mozilla.gecko.db.BrowserContract.Bookmarks; 20 import org.mozilla.gecko.db.BrowserContract.Combined; 21 import org.mozilla.gecko.db.BrowserContract.FaviconColumns; 22 import org.mozilla.gecko.db.BrowserContract.Favicons; 23 import org.mozilla.gecko.db.BrowserContract.Highlights; 24 import org.mozilla.gecko.db.BrowserContract.History; 25 import org.mozilla.gecko.db.BrowserContract.Visits; 26 import org.mozilla.gecko.db.BrowserContract.Schema; 27 import org.mozilla.gecko.db.BrowserContract.Tabs; 28 import org.mozilla.gecko.db.BrowserContract.Thumbnails; 29 import org.mozilla.gecko.db.BrowserContract.TopSites; 30 import org.mozilla.gecko.db.BrowserContract.UrlAnnotations; 31 import org.mozilla.gecko.db.BrowserContract.PageMetadata; 32 import org.mozilla.gecko.db.DBUtils.UpdateOperation; 33 import org.mozilla.gecko.icons.IconsHelper; 34 import org.mozilla.gecko.sync.Utils; 35 import org.mozilla.gecko.util.ThreadUtils; 36 37 import android.content.BroadcastReceiver; 38 import android.content.ContentProviderOperation; 39 import android.content.ContentProviderResult; 40 import android.content.ContentUris; 41 import android.content.ContentValues; 42 import android.content.Context; 43 import android.content.Intent; 44 import android.content.IntentFilter; 45 import android.content.OperationApplicationException; 46 import android.content.UriMatcher; 47 import android.database.Cursor; 48 import android.database.DatabaseUtils; 49 import android.database.MatrixCursor; 50 import android.database.MergeCursor; 51 import android.database.SQLException; 52 import android.database.sqlite.SQLiteCursor; 53 import android.database.sqlite.SQLiteDatabase; 54 import android.database.sqlite.SQLiteQueryBuilder; 55 import android.net.Uri; 56 import android.support.v4.content.LocalBroadcastManager; 57 import android.text.TextUtils; 58 import android.util.Log; 59 60 public class BrowserProvider extends SharedBrowserDatabaseProvider { 61 public static final String ACTION_SHRINK_MEMORY = "org.mozilla.gecko.db.intent.action.SHRINK_MEMORY"; 62 63 private static final String LOGTAG = "GeckoBrowserProvider"; 64 65 // How many records to reposition in a single query. 66 // This should be less than the SQLite maximum number of query variables 67 // (currently 999) divided by the number of variables used per positioning 68 // query (currently 3). 69 static final int MAX_POSITION_UPDATES_PER_QUERY = 100; 70 71 // Minimum number of records to keep when expiring history. 72 static final int DEFAULT_EXPIRY_RETAIN_COUNT = 2000; 73 static final int AGGRESSIVE_EXPIRY_RETAIN_COUNT = 500; 74 75 // Factor used to determine the minimum number of records to keep when expiring the activity stream blocklist 76 static final int ACTIVITYSTREAM_BLOCKLIST_EXPIRY_FACTOR = 5; 77 78 // Minimum duration to keep when expiring. 79 static final long DEFAULT_EXPIRY_PRESERVE_WINDOW = 1000L * 60L * 60L * 24L * 28L; // Four weeks. 80 // Minimum number of thumbnails to keep around. 81 static final int DEFAULT_EXPIRY_THUMBNAIL_COUNT = 15; 82 83 static final String TABLE_BOOKMARKS = Bookmarks.TABLE_NAME; 84 static final String TABLE_HISTORY = History.TABLE_NAME; 85 static final String TABLE_VISITS = Visits.TABLE_NAME; 86 static final String TABLE_FAVICONS = Favicons.TABLE_NAME; 87 static final String TABLE_THUMBNAILS = Thumbnails.TABLE_NAME; 88 static final String TABLE_TABS = Tabs.TABLE_NAME; 89 static final String TABLE_URL_ANNOTATIONS = UrlAnnotations.TABLE_NAME; 90 static final String TABLE_ACTIVITY_STREAM_BLOCKLIST = ActivityStreamBlocklist.TABLE_NAME; 91 static final String TABLE_PAGE_METADATA = PageMetadata.TABLE_NAME; 92 93 static final String VIEW_COMBINED = Combined.VIEW_NAME; 94 static final String VIEW_BOOKMARKS_WITH_FAVICONS = Bookmarks.VIEW_WITH_FAVICONS; 95 static final String VIEW_BOOKMARKS_WITH_ANNOTATIONS = Bookmarks.VIEW_WITH_ANNOTATIONS; 96 static final String VIEW_HISTORY_WITH_FAVICONS = History.VIEW_WITH_FAVICONS; 97 static final String VIEW_COMBINED_WITH_FAVICONS = Combined.VIEW_WITH_FAVICONS; 98 99 // Bookmark matches 100 static final int BOOKMARKS = 100; 101 static final int BOOKMARKS_ID = 101; 102 static final int BOOKMARKS_FOLDER_ID = 102; 103 static final int BOOKMARKS_PARENT = 103; 104 static final int BOOKMARKS_POSITIONS = 104; 105 106 // History matches 107 static final int HISTORY = 200; 108 static final int HISTORY_ID = 201; 109 static final int HISTORY_OLD = 202; 110 111 // Favicon matches 112 static final int FAVICONS = 300; 113 static final int FAVICON_ID = 301; 114 115 // Schema matches 116 static final int SCHEMA = 400; 117 118 // Combined bookmarks and history matches 119 static final int COMBINED = 500; 120 121 // Control matches 122 static final int CONTROL = 600; 123 124 // Search Suggest matches. Obsolete. 125 static final int SEARCH_SUGGEST = 700; 126 127 // Thumbnail matches 128 static final int THUMBNAILS = 800; 129 static final int THUMBNAIL_ID = 801; 130 131 static final int URL_ANNOTATIONS = 900; 132 133 static final int TOPSITES = 1000; 134 135 static final int VISITS = 1100; 136 137 static final int METADATA = 1200; 138 139 static final int HIGHLIGHTS = 1300; 140 141 static final int ACTIVITY_STREAM_BLOCKLIST = 1400; 142 143 static final int PAGE_METADATA = 1500; 144 145 static final String DEFAULT_BOOKMARKS_SORT_ORDER = Bookmarks.TYPE 146 + " ASC, " + Bookmarks.POSITION + " ASC, " + Bookmarks._ID 147 + " ASC"; 148 149 static final String DEFAULT_HISTORY_SORT_ORDER = History.DATE_LAST_VISITED + " DESC"; 150 static final String DEFAULT_VISITS_SORT_ORDER = Visits.DATE_VISITED + " DESC"; 151 152 static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH); 153 154 static final Map<String, String> BOOKMARKS_PROJECTION_MAP; 155 static final Map<String, String> HISTORY_PROJECTION_MAP; 156 static final Map<String, String> COMBINED_PROJECTION_MAP; 157 static final Map<String, String> SCHEMA_PROJECTION_MAP; 158 static final Map<String, String> FAVICONS_PROJECTION_MAP; 159 static final Map<String, String> THUMBNAILS_PROJECTION_MAP; 160 static final Map<String, String> URL_ANNOTATIONS_PROJECTION_MAP; 161 static final Map<String, String> VISIT_PROJECTION_MAP; 162 static final Map<String, String> PAGE_METADATA_PROJECTION_MAP; 163 static final Table[] sTables; 164 165 static { 166 sTables = new Table[] { 167 // See awful shortcut assumption hack in getURLMetadataTable. 168 new URLMetadataTable() 169 }; 170 // We will reuse this. 171 HashMap<String, String> map; 172 173 // Bookmarks URI_MATCHER.addURI(BrowserContract.AUTHORITY, R, BOOKMARKS)174 URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks", BOOKMARKS); URI_MATCHER.addURI(BrowserContract.AUTHORITY, R, BOOKMARKS_ID)175 URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks/#", BOOKMARKS_ID); URI_MATCHER.addURI(BrowserContract.AUTHORITY, R, BOOKMARKS_PARENT)176 URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks/parents", BOOKMARKS_PARENT); URI_MATCHER.addURI(BrowserContract.AUTHORITY, R, BOOKMARKS_POSITIONS)177 URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks/positions", BOOKMARKS_POSITIONS); URI_MATCHER.addURI(BrowserContract.AUTHORITY, R, BOOKMARKS_FOLDER_ID)178 URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks/folder/#", BOOKMARKS_FOLDER_ID); 179 180 map = new HashMap<String, String>(); map.put(Bookmarks._ID, Bookmarks._ID)181 map.put(Bookmarks._ID, Bookmarks._ID); map.put(Bookmarks.TITLE, Bookmarks.TITLE)182 map.put(Bookmarks.TITLE, Bookmarks.TITLE); map.put(Bookmarks.URL, Bookmarks.URL)183 map.put(Bookmarks.URL, Bookmarks.URL); map.put(Bookmarks.FAVICON, Bookmarks.FAVICON)184 map.put(Bookmarks.FAVICON, Bookmarks.FAVICON); map.put(Bookmarks.FAVICON_ID, Bookmarks.FAVICON_ID)185 map.put(Bookmarks.FAVICON_ID, Bookmarks.FAVICON_ID); map.put(Bookmarks.FAVICON_URL, Bookmarks.FAVICON_URL)186 map.put(Bookmarks.FAVICON_URL, Bookmarks.FAVICON_URL); map.put(Bookmarks.TYPE, Bookmarks.TYPE)187 map.put(Bookmarks.TYPE, Bookmarks.TYPE); map.put(Bookmarks.PARENT, Bookmarks.PARENT)188 map.put(Bookmarks.PARENT, Bookmarks.PARENT); map.put(Bookmarks.POSITION, Bookmarks.POSITION)189 map.put(Bookmarks.POSITION, Bookmarks.POSITION); map.put(Bookmarks.TAGS, Bookmarks.TAGS)190 map.put(Bookmarks.TAGS, Bookmarks.TAGS); map.put(Bookmarks.DESCRIPTION, Bookmarks.DESCRIPTION)191 map.put(Bookmarks.DESCRIPTION, Bookmarks.DESCRIPTION); map.put(Bookmarks.KEYWORD, Bookmarks.KEYWORD)192 map.put(Bookmarks.KEYWORD, Bookmarks.KEYWORD); map.put(Bookmarks.DATE_CREATED, Bookmarks.DATE_CREATED)193 map.put(Bookmarks.DATE_CREATED, Bookmarks.DATE_CREATED); map.put(Bookmarks.DATE_MODIFIED, Bookmarks.DATE_MODIFIED)194 map.put(Bookmarks.DATE_MODIFIED, Bookmarks.DATE_MODIFIED); map.put(Bookmarks.GUID, Bookmarks.GUID)195 map.put(Bookmarks.GUID, Bookmarks.GUID); map.put(Bookmarks.IS_DELETED, Bookmarks.IS_DELETED)196 map.put(Bookmarks.IS_DELETED, Bookmarks.IS_DELETED); 197 BOOKMARKS_PROJECTION_MAP = Collections.unmodifiableMap(map); 198 199 // History URI_MATCHER.addURI(BrowserContract.AUTHORITY, R, HISTORY)200 URI_MATCHER.addURI(BrowserContract.AUTHORITY, "history", HISTORY); URI_MATCHER.addURI(BrowserContract.AUTHORITY, R, HISTORY_ID)201 URI_MATCHER.addURI(BrowserContract.AUTHORITY, "history/#", HISTORY_ID); URI_MATCHER.addURI(BrowserContract.AUTHORITY, R, HISTORY_OLD)202 URI_MATCHER.addURI(BrowserContract.AUTHORITY, "history/old", HISTORY_OLD); 203 204 map = new HashMap<String, String>(); map.put(History._ID, History._ID)205 map.put(History._ID, History._ID); map.put(History.TITLE, History.TITLE)206 map.put(History.TITLE, History.TITLE); map.put(History.URL, History.URL)207 map.put(History.URL, History.URL); map.put(History.FAVICON, History.FAVICON)208 map.put(History.FAVICON, History.FAVICON); map.put(History.FAVICON_ID, History.FAVICON_ID)209 map.put(History.FAVICON_ID, History.FAVICON_ID); map.put(History.FAVICON_URL, History.FAVICON_URL)210 map.put(History.FAVICON_URL, History.FAVICON_URL); map.put(History.VISITS, History.VISITS)211 map.put(History.VISITS, History.VISITS); map.put(History.LOCAL_VISITS, History.LOCAL_VISITS)212 map.put(History.LOCAL_VISITS, History.LOCAL_VISITS); map.put(History.REMOTE_VISITS, History.REMOTE_VISITS)213 map.put(History.REMOTE_VISITS, History.REMOTE_VISITS); map.put(History.DATE_LAST_VISITED, History.DATE_LAST_VISITED)214 map.put(History.DATE_LAST_VISITED, History.DATE_LAST_VISITED); map.put(History.LOCAL_DATE_LAST_VISITED, History.LOCAL_DATE_LAST_VISITED)215 map.put(History.LOCAL_DATE_LAST_VISITED, History.LOCAL_DATE_LAST_VISITED); map.put(History.REMOTE_DATE_LAST_VISITED, History.REMOTE_DATE_LAST_VISITED)216 map.put(History.REMOTE_DATE_LAST_VISITED, History.REMOTE_DATE_LAST_VISITED); map.put(History.DATE_CREATED, History.DATE_CREATED)217 map.put(History.DATE_CREATED, History.DATE_CREATED); map.put(History.DATE_MODIFIED, History.DATE_MODIFIED)218 map.put(History.DATE_MODIFIED, History.DATE_MODIFIED); map.put(History.GUID, History.GUID)219 map.put(History.GUID, History.GUID); map.put(History.IS_DELETED, History.IS_DELETED)220 map.put(History.IS_DELETED, History.IS_DELETED); 221 HISTORY_PROJECTION_MAP = Collections.unmodifiableMap(map); 222 223 // Visits URI_MATCHER.addURI(BrowserContract.AUTHORITY, R, VISITS)224 URI_MATCHER.addURI(BrowserContract.AUTHORITY, "visits", VISITS); 225 226 map = new HashMap<String, String>(); map.put(Visits._ID, Visits._ID)227 map.put(Visits._ID, Visits._ID); map.put(Visits.HISTORY_GUID, Visits.HISTORY_GUID)228 map.put(Visits.HISTORY_GUID, Visits.HISTORY_GUID); map.put(Visits.VISIT_TYPE, Visits.VISIT_TYPE)229 map.put(Visits.VISIT_TYPE, Visits.VISIT_TYPE); map.put(Visits.DATE_VISITED, Visits.DATE_VISITED)230 map.put(Visits.DATE_VISITED, Visits.DATE_VISITED); map.put(Visits.IS_LOCAL, Visits.IS_LOCAL)231 map.put(Visits.IS_LOCAL, Visits.IS_LOCAL); 232 VISIT_PROJECTION_MAP = Collections.unmodifiableMap(map); 233 234 // Favicons URI_MATCHER.addURI(BrowserContract.AUTHORITY, R, FAVICONS)235 URI_MATCHER.addURI(BrowserContract.AUTHORITY, "favicons", FAVICONS); URI_MATCHER.addURI(BrowserContract.AUTHORITY, R, FAVICON_ID)236 URI_MATCHER.addURI(BrowserContract.AUTHORITY, "favicons/#", FAVICON_ID); 237 238 map = new HashMap<String, String>(); map.put(Favicons._ID, Favicons._ID)239 map.put(Favicons._ID, Favicons._ID); map.put(Favicons.URL, Favicons.URL)240 map.put(Favicons.URL, Favicons.URL); map.put(Favicons.DATA, Favicons.DATA)241 map.put(Favicons.DATA, Favicons.DATA); map.put(Favicons.DATE_CREATED, Favicons.DATE_CREATED)242 map.put(Favicons.DATE_CREATED, Favicons.DATE_CREATED); map.put(Favicons.DATE_MODIFIED, Favicons.DATE_MODIFIED)243 map.put(Favicons.DATE_MODIFIED, Favicons.DATE_MODIFIED); 244 FAVICONS_PROJECTION_MAP = Collections.unmodifiableMap(map); 245 246 // Thumbnails URI_MATCHER.addURI(BrowserContract.AUTHORITY, R, THUMBNAILS)247 URI_MATCHER.addURI(BrowserContract.AUTHORITY, "thumbnails", THUMBNAILS); URI_MATCHER.addURI(BrowserContract.AUTHORITY, R, THUMBNAIL_ID)248 URI_MATCHER.addURI(BrowserContract.AUTHORITY, "thumbnails/#", THUMBNAIL_ID); 249 250 map = new HashMap<String, String>(); map.put(Thumbnails._ID, Thumbnails._ID)251 map.put(Thumbnails._ID, Thumbnails._ID); map.put(Thumbnails.URL, Thumbnails.URL)252 map.put(Thumbnails.URL, Thumbnails.URL); map.put(Thumbnails.DATA, Thumbnails.DATA)253 map.put(Thumbnails.DATA, Thumbnails.DATA); 254 THUMBNAILS_PROJECTION_MAP = Collections.unmodifiableMap(map); 255 256 // Url annotations URI_MATCHER.addURI(BrowserContract.AUTHORITY, TABLE_URL_ANNOTATIONS, URL_ANNOTATIONS)257 URI_MATCHER.addURI(BrowserContract.AUTHORITY, TABLE_URL_ANNOTATIONS, URL_ANNOTATIONS); 258 259 map = new HashMap<>(); map.put(UrlAnnotations._ID, UrlAnnotations._ID)260 map.put(UrlAnnotations._ID, UrlAnnotations._ID); map.put(UrlAnnotations.URL, UrlAnnotations.URL)261 map.put(UrlAnnotations.URL, UrlAnnotations.URL); map.put(UrlAnnotations.KEY, UrlAnnotations.KEY)262 map.put(UrlAnnotations.KEY, UrlAnnotations.KEY); map.put(UrlAnnotations.VALUE, UrlAnnotations.VALUE)263 map.put(UrlAnnotations.VALUE, UrlAnnotations.VALUE); map.put(UrlAnnotations.DATE_CREATED, UrlAnnotations.DATE_CREATED)264 map.put(UrlAnnotations.DATE_CREATED, UrlAnnotations.DATE_CREATED); map.put(UrlAnnotations.DATE_MODIFIED, UrlAnnotations.DATE_MODIFIED)265 map.put(UrlAnnotations.DATE_MODIFIED, UrlAnnotations.DATE_MODIFIED); map.put(UrlAnnotations.SYNC_STATUS, UrlAnnotations.SYNC_STATUS)266 map.put(UrlAnnotations.SYNC_STATUS, UrlAnnotations.SYNC_STATUS); 267 URL_ANNOTATIONS_PROJECTION_MAP = Collections.unmodifiableMap(map); 268 269 // Combined bookmarks and history URI_MATCHER.addURI(BrowserContract.AUTHORITY, R, COMBINED)270 URI_MATCHER.addURI(BrowserContract.AUTHORITY, "combined", COMBINED); 271 272 map = new HashMap<String, String>(); map.put(Combined._ID, Combined._ID)273 map.put(Combined._ID, Combined._ID); map.put(Combined.BOOKMARK_ID, Combined.BOOKMARK_ID)274 map.put(Combined.BOOKMARK_ID, Combined.BOOKMARK_ID); map.put(Combined.HISTORY_ID, Combined.HISTORY_ID)275 map.put(Combined.HISTORY_ID, Combined.HISTORY_ID); map.put(Combined.URL, Combined.URL)276 map.put(Combined.URL, Combined.URL); map.put(Combined.TITLE, Combined.TITLE)277 map.put(Combined.TITLE, Combined.TITLE); map.put(Combined.VISITS, Combined.VISITS)278 map.put(Combined.VISITS, Combined.VISITS); map.put(Combined.DATE_LAST_VISITED, Combined.DATE_LAST_VISITED)279 map.put(Combined.DATE_LAST_VISITED, Combined.DATE_LAST_VISITED); map.put(Combined.FAVICON, Combined.FAVICON)280 map.put(Combined.FAVICON, Combined.FAVICON); map.put(Combined.FAVICON_ID, Combined.FAVICON_ID)281 map.put(Combined.FAVICON_ID, Combined.FAVICON_ID); map.put(Combined.FAVICON_URL, Combined.FAVICON_URL)282 map.put(Combined.FAVICON_URL, Combined.FAVICON_URL); map.put(Combined.LOCAL_DATE_LAST_VISITED, Combined.LOCAL_DATE_LAST_VISITED)283 map.put(Combined.LOCAL_DATE_LAST_VISITED, Combined.LOCAL_DATE_LAST_VISITED); map.put(Combined.REMOTE_DATE_LAST_VISITED, Combined.REMOTE_DATE_LAST_VISITED)284 map.put(Combined.REMOTE_DATE_LAST_VISITED, Combined.REMOTE_DATE_LAST_VISITED); map.put(Combined.LOCAL_VISITS_COUNT, Combined.LOCAL_VISITS_COUNT)285 map.put(Combined.LOCAL_VISITS_COUNT, Combined.LOCAL_VISITS_COUNT); map.put(Combined.REMOTE_VISITS_COUNT, Combined.REMOTE_VISITS_COUNT)286 map.put(Combined.REMOTE_VISITS_COUNT, Combined.REMOTE_VISITS_COUNT); 287 COMBINED_PROJECTION_MAP = Collections.unmodifiableMap(map); 288 289 map = new HashMap<>(); map.put(PageMetadata._ID, PageMetadata._ID)290 map.put(PageMetadata._ID, PageMetadata._ID); map.put(PageMetadata.HISTORY_GUID, PageMetadata.HISTORY_GUID)291 map.put(PageMetadata.HISTORY_GUID, PageMetadata.HISTORY_GUID); map.put(PageMetadata.DATE_CREATED, PageMetadata.DATE_CREATED)292 map.put(PageMetadata.DATE_CREATED, PageMetadata.DATE_CREATED); map.put(PageMetadata.HAS_IMAGE, PageMetadata.HAS_IMAGE)293 map.put(PageMetadata.HAS_IMAGE, PageMetadata.HAS_IMAGE); map.put(PageMetadata.JSON, PageMetadata.JSON)294 map.put(PageMetadata.JSON, PageMetadata.JSON); 295 PAGE_METADATA_PROJECTION_MAP = Collections.unmodifiableMap(map); 296 URI_MATCHER.addURI(BrowserContract.AUTHORITY, R, PAGE_METADATA)297 URI_MATCHER.addURI(BrowserContract.AUTHORITY, "page_metadata", PAGE_METADATA); 298 299 // Schema URI_MATCHER.addURI(BrowserContract.AUTHORITY, R, SCHEMA)300 URI_MATCHER.addURI(BrowserContract.AUTHORITY, "schema", SCHEMA); 301 302 map = new HashMap<String, String>(); map.put(Schema.VERSION, Schema.VERSION)303 map.put(Schema.VERSION, Schema.VERSION); 304 SCHEMA_PROJECTION_MAP = Collections.unmodifiableMap(map); 305 306 307 // Control URI_MATCHER.addURI(BrowserContract.AUTHORITY, R, CONTROL)308 URI_MATCHER.addURI(BrowserContract.AUTHORITY, "control", CONTROL); 309 310 for (Table table : sTables) { 311 for (Table.ContentProviderInfo type : table.getContentProviderInfo()) { URI_MATCHER.addURI(BrowserContract.AUTHORITY, type.name, type.id)312 URI_MATCHER.addURI(BrowserContract.AUTHORITY, type.name, type.id); 313 } 314 } 315 316 // Combined pinned sites, top visited sites, and suggested sites URI_MATCHER.addURI(BrowserContract.AUTHORITY, R, TOPSITES)317 URI_MATCHER.addURI(BrowserContract.AUTHORITY, "topsites", TOPSITES); 318 URI_MATCHER.addURI(BrowserContract.AUTHORITY, R, HIGHLIGHTS)319 URI_MATCHER.addURI(BrowserContract.AUTHORITY, "highlights", HIGHLIGHTS); 320 URI_MATCHER.addURI(BrowserContract.AUTHORITY, ActivityStreamBlocklist.TABLE_NAME, ACTIVITY_STREAM_BLOCKLIST)321 URI_MATCHER.addURI(BrowserContract.AUTHORITY, ActivityStreamBlocklist.TABLE_NAME, ACTIVITY_STREAM_BLOCKLIST); 322 } 323 324 private static class ShrinkMemoryReceiver extends BroadcastReceiver { 325 private final WeakReference<BrowserProvider> mBrowserProviderWeakReference; 326 ShrinkMemoryReceiver(final BrowserProvider browserProvider)327 public ShrinkMemoryReceiver(final BrowserProvider browserProvider) { 328 mBrowserProviderWeakReference = new WeakReference<>(browserProvider); 329 } 330 331 @Override onReceive(Context context, Intent intent)332 public void onReceive(Context context, Intent intent) { 333 final BrowserProvider browserProvider = mBrowserProviderWeakReference.get(); 334 if (browserProvider == null) { 335 return; 336 } 337 final PerProfileDatabases<BrowserDatabaseHelper> databases = browserProvider.getDatabases(); 338 if (databases == null) { 339 return; 340 } 341 ThreadUtils.postToBackgroundThread(new Runnable() { 342 @Override 343 public void run() { 344 databases.shrinkMemory(); 345 } 346 }); 347 } 348 } 349 350 private final ShrinkMemoryReceiver mShrinkMemoryReceiver = new ShrinkMemoryReceiver(this); 351 352 @Override onCreate()353 public boolean onCreate() { 354 if (!super.onCreate()) { 355 return false; 356 } 357 358 LocalBroadcastManager.getInstance(getContext()).registerReceiver(mShrinkMemoryReceiver, 359 new IntentFilter(ACTION_SHRINK_MEMORY)); 360 361 return true; 362 } 363 364 @Override shutdown()365 public void shutdown() { 366 LocalBroadcastManager.getInstance(getContext()).unregisterReceiver(mShrinkMemoryReceiver); 367 368 super.shutdown(); 369 } 370 371 // Convenience accessor. 372 // Assumes structure of sTables! getURLMetadataTable()373 private URLMetadataTable getURLMetadataTable() { 374 return (URLMetadataTable) sTables[0]; 375 } 376 hasFaviconsInProjection(String[] projection)377 private static boolean hasFaviconsInProjection(String[] projection) { 378 if (projection == null) return true; 379 for (int i = 0; i < projection.length; ++i) { 380 if (projection[i].equals(FaviconColumns.FAVICON) || 381 projection[i].equals(FaviconColumns.FAVICON_URL)) 382 return true; 383 } 384 385 return false; 386 } 387 388 // Calculate these once, at initialization. isLoggable is too expensive to 389 // have in-line in each log call. 390 private static final boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG); 391 private static final boolean logVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE); trace(String message)392 protected static void trace(String message) { 393 if (logVerbose) { 394 Log.v(LOGTAG, message); 395 } 396 } 397 debug(String message)398 protected static void debug(String message) { 399 if (logDebug) { 400 Log.d(LOGTAG, message); 401 } 402 } 403 404 /** 405 * Remove enough activity stream blocklist items to bring the database count below <code>retain</code>. 406 * 407 * Items will be removed according to their creation date, oldest being removed first. 408 */ expireActivityStreamBlocklist(final SQLiteDatabase db, final int retain)409 private void expireActivityStreamBlocklist(final SQLiteDatabase db, final int retain) { 410 Log.d(LOGTAG, "Expiring highlights blocklist."); 411 final long rows = DatabaseUtils.queryNumEntries(db, TABLE_ACTIVITY_STREAM_BLOCKLIST); 412 413 if (retain >= rows) { 414 debug("Not expiring highlights blocklist: only have " + rows + " rows."); 415 return; 416 } 417 418 final long toRemove = rows - retain; 419 420 final String statement = "DELETE FROM " + TABLE_ACTIVITY_STREAM_BLOCKLIST + " WHERE " + ActivityStreamBlocklist._ID + " IN " + 421 " ( SELECT " + ActivityStreamBlocklist._ID + " FROM " + TABLE_ACTIVITY_STREAM_BLOCKLIST + " " + 422 "ORDER BY " + ActivityStreamBlocklist.CREATED + " ASC LIMIT " + toRemove + ")"; 423 424 beginWrite(db); 425 db.execSQL(statement); 426 } 427 428 /** 429 * Remove enough history items to bring the database count below <code>retain</code>, 430 * removing no items with a modified time after <code>keepAfter</code>. 431 * 432 * Provide <code>keepAfter</code> less than or equal to zero to skip that check. 433 * 434 * Items will be removed according to last visited date. 435 */ expireHistory(final SQLiteDatabase db, final int retain, final long keepAfter)436 private void expireHistory(final SQLiteDatabase db, final int retain, final long keepAfter) { 437 Log.d(LOGTAG, "Expiring history."); 438 final long rows = DatabaseUtils.queryNumEntries(db, TABLE_HISTORY); 439 440 if (retain >= rows) { 441 debug("Not expiring history: only have " + rows + " rows."); 442 return; 443 } 444 445 final long toRemove = rows - retain; 446 debug("Expiring at most " + toRemove + " rows earlier than " + keepAfter + "."); 447 448 final String sql; 449 if (keepAfter > 0) { 450 sql = "DELETE FROM " + TABLE_HISTORY + " " + 451 "WHERE MAX(" + History.DATE_LAST_VISITED + ", " + History.DATE_MODIFIED + ") < " + keepAfter + " " + 452 " AND " + History._ID + " IN ( SELECT " + 453 History._ID + " FROM " + TABLE_HISTORY + " " + 454 "ORDER BY " + History.DATE_LAST_VISITED + " ASC LIMIT " + toRemove + 455 ")"; 456 } else { 457 sql = "DELETE FROM " + TABLE_HISTORY + " WHERE " + History._ID + " " + 458 "IN ( SELECT " + History._ID + " FROM " + TABLE_HISTORY + " " + 459 "ORDER BY " + History.DATE_LAST_VISITED + " ASC LIMIT " + toRemove + ")"; 460 } 461 trace("Deleting using query: " + sql); 462 463 beginWrite(db); 464 db.execSQL(sql); 465 } 466 467 /** 468 * Remove any thumbnails that for sites that aren't likely to be ever shown. 469 * Items will be removed according to a frecency calculation and only if they are not pinned 470 * 471 * Call this method within a transaction. 472 */ expireThumbnails(final SQLiteDatabase db)473 private void expireThumbnails(final SQLiteDatabase db) { 474 Log.d(LOGTAG, "Expiring thumbnails."); 475 final String sortOrder = BrowserContract.getCombinedFrecencySortOrder(true, false); 476 final String sql = "DELETE FROM " + TABLE_THUMBNAILS + 477 " WHERE " + Thumbnails.URL + " NOT IN ( " + 478 " SELECT " + Combined.URL + 479 " FROM " + Combined.VIEW_NAME + 480 " ORDER BY " + sortOrder + 481 " LIMIT " + DEFAULT_EXPIRY_THUMBNAIL_COUNT + 482 ") AND " + Thumbnails.URL + " NOT IN ( " + 483 " SELECT " + Bookmarks.URL + 484 " FROM " + TABLE_BOOKMARKS + 485 " WHERE " + Bookmarks.PARENT + " = " + Bookmarks.FIXED_PINNED_LIST_ID + 486 ") AND " + Thumbnails.URL + " NOT IN ( " + 487 " SELECT " + Tabs.URL + 488 " FROM " + TABLE_TABS + 489 ")"; 490 trace("Clear thumbs using query: " + sql); 491 db.execSQL(sql); 492 } 493 shouldIncrementVisits(Uri uri)494 private boolean shouldIncrementVisits(Uri uri) { 495 String incrementVisits = uri.getQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS); 496 return Boolean.parseBoolean(incrementVisits); 497 } 498 shouldIncrementRemoteAggregates(Uri uri)499 private boolean shouldIncrementRemoteAggregates(Uri uri) { 500 final String incrementRemoteAggregates = uri.getQueryParameter(BrowserContract.PARAM_INCREMENT_REMOTE_AGGREGATES); 501 return Boolean.parseBoolean(incrementRemoteAggregates); 502 } 503 504 @Override getType(Uri uri)505 public String getType(Uri uri) { 506 final int match = URI_MATCHER.match(uri); 507 508 trace("Getting URI type: " + uri); 509 510 switch (match) { 511 case BOOKMARKS: 512 trace("URI is BOOKMARKS: " + uri); 513 return Bookmarks.CONTENT_TYPE; 514 case BOOKMARKS_ID: 515 trace("URI is BOOKMARKS_ID: " + uri); 516 return Bookmarks.CONTENT_ITEM_TYPE; 517 case HISTORY: 518 trace("URI is HISTORY: " + uri); 519 return History.CONTENT_TYPE; 520 case HISTORY_ID: 521 trace("URI is HISTORY_ID: " + uri); 522 return History.CONTENT_ITEM_TYPE; 523 default: 524 String type = getContentItemType(match); 525 if (type != null) { 526 trace("URI is " + type); 527 return type; 528 } 529 530 debug("URI has unrecognized type: " + uri); 531 return null; 532 } 533 } 534 535 @SuppressWarnings("fallthrough") 536 @Override deleteInTransaction(Uri uri, String selection, String[] selectionArgs)537 public int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) { 538 trace("Calling delete in transaction on URI: " + uri); 539 final SQLiteDatabase db = getWritableDatabase(uri); 540 541 final int match = URI_MATCHER.match(uri); 542 int deleted = 0; 543 544 switch (match) { 545 case BOOKMARKS_ID: 546 trace("Delete on BOOKMARKS_ID: " + uri); 547 548 selection = DBUtils.concatenateWhere(selection, TABLE_BOOKMARKS + "._id = ?"); 549 selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, 550 new String[] { Long.toString(ContentUris.parseId(uri)) }); 551 // fall through 552 case BOOKMARKS: { 553 trace("Deleting bookmarks: " + uri); 554 deleted = deleteBookmarks(uri, selection, selectionArgs); 555 deleteUnusedImages(uri); 556 break; 557 } 558 559 case HISTORY_ID: 560 trace("Delete on HISTORY_ID: " + uri); 561 562 selection = DBUtils.concatenateWhere(selection, TABLE_HISTORY + "._id = ?"); 563 selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, 564 new String[] { Long.toString(ContentUris.parseId(uri)) }); 565 // fall through 566 case HISTORY: { 567 trace("Deleting history: " + uri); 568 beginWrite(db); 569 /** 570 * Deletes from Sync are actual DELETE statements, which will cascade delete relevant visits. 571 * Fennec's deletes mark records as deleted and wipe out all information (except for GUID). 572 * Eventually, Fennec will purge history records that were marked as deleted for longer than some 573 * period of time (e.g. 20 days). 574 * See {@link SharedBrowserDatabaseProvider#cleanUpSomeDeletedRecords(Uri, String)}. 575 */ 576 final ArrayList<String> historyGUIDs = getHistoryGUIDsFromSelection(db, uri, selection, selectionArgs); 577 578 if (!isCallerSync(uri)) { 579 deleteVisitsForHistory(db, historyGUIDs); 580 } 581 deletePageMetadataForHistory(db, historyGUIDs); 582 deleted = deleteHistory(db, uri, selection, selectionArgs); 583 deleteUnusedImages(uri); 584 break; 585 } 586 587 case VISITS: 588 trace("Deleting visits: " + uri); 589 beginWrite(db); 590 deleted = deleteVisits(uri, selection, selectionArgs); 591 break; 592 593 case HISTORY_OLD: { 594 String priority = uri.getQueryParameter(BrowserContract.PARAM_EXPIRE_PRIORITY); 595 long keepAfter = System.currentTimeMillis() - DEFAULT_EXPIRY_PRESERVE_WINDOW; 596 int retainCount = DEFAULT_EXPIRY_RETAIN_COUNT; 597 598 if (BrowserContract.ExpirePriority.AGGRESSIVE.toString().equals(priority)) { 599 keepAfter = 0; 600 retainCount = AGGRESSIVE_EXPIRY_RETAIN_COUNT; 601 } 602 expireHistory(db, retainCount, keepAfter); 603 expireActivityStreamBlocklist(db, retainCount / ACTIVITYSTREAM_BLOCKLIST_EXPIRY_FACTOR); 604 expireThumbnails(db); 605 deleteUnusedImages(uri); 606 break; 607 } 608 609 case FAVICON_ID: 610 debug("Delete on FAVICON_ID: " + uri); 611 612 selection = DBUtils.concatenateWhere(selection, TABLE_FAVICONS + "._id = ?"); 613 selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, 614 new String[] { Long.toString(ContentUris.parseId(uri)) }); 615 // fall through 616 case FAVICONS: { 617 trace("Deleting favicons: " + uri); 618 beginWrite(db); 619 deleted = deleteFavicons(uri, selection, selectionArgs); 620 break; 621 } 622 623 case THUMBNAIL_ID: 624 debug("Delete on THUMBNAIL_ID: " + uri); 625 626 selection = DBUtils.concatenateWhere(selection, TABLE_THUMBNAILS + "._id = ?"); 627 selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, 628 new String[] { Long.toString(ContentUris.parseId(uri)) }); 629 // fall through 630 case THUMBNAILS: { 631 trace("Deleting thumbnails: " + uri); 632 beginWrite(db); 633 deleted = deleteThumbnails(uri, selection, selectionArgs); 634 break; 635 } 636 637 case URL_ANNOTATIONS: 638 trace("Delete on URL_ANNOTATIONS: " + uri); 639 deleteUrlAnnotation(uri, selection, selectionArgs); 640 break; 641 642 case PAGE_METADATA: 643 trace("Delete on PAGE_METADATA: " + uri); 644 deleted = deletePageMetadata(uri, selection, selectionArgs); 645 break; 646 647 default: { 648 Table table = findTableFor(match); 649 if (table == null) { 650 throw new UnsupportedOperationException("Unknown delete URI " + uri); 651 } 652 trace("Deleting TABLE: " + uri); 653 beginWrite(db); 654 deleted = table.delete(db, uri, match, selection, selectionArgs); 655 } 656 } 657 658 debug("Deleted " + deleted + " rows for URI: " + uri); 659 660 return deleted; 661 } 662 663 @Override insertInTransaction(Uri uri, ContentValues values)664 public Uri insertInTransaction(Uri uri, ContentValues values) { 665 trace("Calling insert in transaction on URI: " + uri); 666 667 int match = URI_MATCHER.match(uri); 668 long id = -1; 669 670 switch (match) { 671 case BOOKMARKS: { 672 trace("Insert on BOOKMARKS: " + uri); 673 id = insertBookmark(uri, values); 674 break; 675 } 676 677 case HISTORY: { 678 trace("Insert on HISTORY: " + uri); 679 id = insertHistory(uri, values); 680 break; 681 } 682 683 case VISITS: { 684 trace("Insert on VISITS: " + uri); 685 id = insertVisit(uri, values); 686 break; 687 } 688 689 case FAVICONS: { 690 trace("Insert on FAVICONS: " + uri); 691 id = insertFavicon(uri, values); 692 break; 693 } 694 695 case THUMBNAILS: { 696 trace("Insert on THUMBNAILS: " + uri); 697 id = insertThumbnail(uri, values); 698 break; 699 } 700 701 case URL_ANNOTATIONS: { 702 trace("Insert on URL_ANNOTATIONS: " + uri); 703 id = insertUrlAnnotation(uri, values); 704 break; 705 } 706 707 case ACTIVITY_STREAM_BLOCKLIST: { 708 trace("Insert on ACTIVITY_STREAM_BLOCKLIST: " + uri); 709 id = insertActivityStreamBlocklistSite(uri, values); 710 break; 711 } 712 713 case PAGE_METADATA: { 714 trace("Insert on PAGE_METADATA: " + uri); 715 id = insertPageMetadata(uri, values); 716 break; 717 } 718 719 default: { 720 Table table = findTableFor(match); 721 if (table == null) { 722 throw new UnsupportedOperationException("Unknown insert URI " + uri); 723 } 724 725 trace("Insert on TABLE: " + uri); 726 final SQLiteDatabase db = getWritableDatabase(uri); 727 beginWrite(db); 728 id = table.insert(db, uri, match, values); 729 } 730 } 731 732 debug("Inserted ID in database: " + id); 733 734 if (id >= 0) 735 return ContentUris.withAppendedId(uri, id); 736 737 return null; 738 } 739 740 @SuppressWarnings("fallthrough") 741 @Override updateInTransaction(Uri uri, ContentValues values, String selection, String[] selectionArgs)742 public int updateInTransaction(Uri uri, ContentValues values, String selection, 743 String[] selectionArgs) { 744 trace("Calling update in transaction on URI: " + uri); 745 746 int match = URI_MATCHER.match(uri); 747 int updated = 0; 748 749 final SQLiteDatabase db = getWritableDatabase(uri); 750 switch (match) { 751 // We provide a dedicated (hacky) API for callers to bulk-update the positions of 752 // folder children by passing an array of GUID strings as `selectionArgs`. 753 // Each child will have its position column set to its index in the provided array. 754 // 755 // This avoids callers having to issue a large number of UPDATE queries through 756 // the usual channels. See Bug 728783. 757 // 758 // Note that this is decidedly not a general-purpose API; use at your own risk. 759 // `values` and `selection` are ignored. 760 case BOOKMARKS_POSITIONS: { 761 debug("Update on BOOKMARKS_POSITIONS: " + uri); 762 763 // This already starts and finishes its own transaction. 764 updated = updateBookmarkPositions(uri, selectionArgs); 765 break; 766 } 767 768 case BOOKMARKS_PARENT: { 769 debug("Update on BOOKMARKS_PARENT: " + uri); 770 beginWrite(db); 771 updated = updateBookmarkParents(db, values, selection, selectionArgs); 772 break; 773 } 774 775 case BOOKMARKS_ID: 776 debug("Update on BOOKMARKS_ID: " + uri); 777 778 selection = DBUtils.concatenateWhere(selection, TABLE_BOOKMARKS + "._id = ?"); 779 selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, 780 new String[] { Long.toString(ContentUris.parseId(uri)) }); 781 // fall through 782 case BOOKMARKS: { 783 debug("Updating bookmark: " + uri); 784 if (shouldUpdateOrInsert(uri)) { 785 updated = updateOrInsertBookmark(uri, values, selection, selectionArgs); 786 } else { 787 updated = updateBookmarks(uri, values, selection, selectionArgs); 788 } 789 break; 790 } 791 792 case HISTORY_ID: 793 debug("Update on HISTORY_ID: " + uri); 794 795 selection = DBUtils.concatenateWhere(selection, TABLE_HISTORY + "._id = ?"); 796 selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, 797 new String[] { Long.toString(ContentUris.parseId(uri)) }); 798 // fall through 799 case HISTORY: { 800 debug("Updating history: " + uri); 801 if (shouldUpdateOrInsert(uri)) { 802 updated = updateOrInsertHistory(uri, values, selection, selectionArgs); 803 } else { 804 updated = updateHistory(uri, values, selection, selectionArgs); 805 } 806 if (shouldIncrementVisits(uri)) { 807 insertVisitForHistory(uri, values, selection, selectionArgs); 808 } 809 break; 810 } 811 812 case FAVICONS: { 813 debug("Update on FAVICONS: " + uri); 814 815 String url = values.getAsString(Favicons.URL); 816 String faviconSelection = null; 817 String[] faviconSelectionArgs = null; 818 819 if (!TextUtils.isEmpty(url)) { 820 faviconSelection = Favicons.URL + " = ?"; 821 faviconSelectionArgs = new String[] { url }; 822 } 823 824 if (shouldUpdateOrInsert(uri)) { 825 updated = updateOrInsertFavicon(uri, values, faviconSelection, faviconSelectionArgs); 826 } else { 827 updated = updateExistingFavicon(uri, values, faviconSelection, faviconSelectionArgs); 828 } 829 break; 830 } 831 832 case THUMBNAILS: { 833 debug("Update on THUMBNAILS: " + uri); 834 835 String url = values.getAsString(Thumbnails.URL); 836 837 // if no URL is provided, update all of the entries 838 if (TextUtils.isEmpty(values.getAsString(Thumbnails.URL))) { 839 updated = updateExistingThumbnail(uri, values, null, null); 840 } else if (shouldUpdateOrInsert(uri)) { 841 updated = updateOrInsertThumbnail(uri, values, Thumbnails.URL + " = ?", 842 new String[] { url }); 843 } else { 844 updated = updateExistingThumbnail(uri, values, Thumbnails.URL + " = ?", 845 new String[] { url }); 846 } 847 break; 848 } 849 850 case URL_ANNOTATIONS: 851 updateUrlAnnotation(uri, values, selection, selectionArgs); 852 break; 853 854 default: { 855 Table table = findTableFor(match); 856 if (table == null) { 857 throw new UnsupportedOperationException("Unknown update URI " + uri); 858 } 859 trace("Update TABLE: " + uri); 860 861 beginWrite(db); 862 updated = table.update(db, uri, match, values, selection, selectionArgs); 863 if (shouldUpdateOrInsert(uri) && updated == 0) { 864 trace("No update, inserting for URL: " + uri); 865 table.insert(db, uri, match, values); 866 updated = 1; 867 } 868 } 869 } 870 871 debug("Updated " + updated + " rows for URI: " + uri); 872 return updated; 873 } 874 875 /** 876 * Get topsites by themselves, without the inclusion of pinned sites. Suggested sites 877 * will be appended (if necessary) to the end of the list in order to provide up to PARAM_LIMIT items. 878 */ getPlainTopSites(final Uri uri)879 private Cursor getPlainTopSites(final Uri uri) { 880 final SQLiteDatabase db = getReadableDatabase(uri); 881 882 final String limitParam = uri.getQueryParameter(BrowserContract.PARAM_LIMIT); 883 final int limit; 884 if (limitParam != null) { 885 limit = Integer.parseInt(limitParam); 886 } else { 887 limit = 12; 888 } 889 890 // Filter out: unvisited pages (history_id == -1) pinned (and other special) sites, deleted sites, 891 // and about: pages. 892 final String ignoreForTopSitesWhereClause = 893 "(" + Combined.HISTORY_ID + " IS NOT -1)" + 894 " AND " + 895 Combined.URL + " NOT IN (SELECT " + 896 Bookmarks.URL + " FROM " + TABLE_BOOKMARKS + " WHERE " + 897 DBUtils.qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " < " + Bookmarks.FIXED_ROOT_ID + " AND " + 898 DBUtils.qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " == 0)" + 899 " AND " + 900 "(" + Combined.URL + " NOT LIKE ?)"; 901 902 final String[] ignoreForTopSitesArgs = new String[] { 903 AboutPages.URL_FILTER 904 }; 905 906 final Cursor c = db.rawQuery("SELECT " + 907 Bookmarks._ID + ", " + 908 Combined.BOOKMARK_ID + ", " + 909 Combined.HISTORY_ID + ", " + 910 Bookmarks.URL + ", " + 911 Bookmarks.TITLE + ", " + 912 Combined.HISTORY_ID + ", " + 913 TopSites.TYPE_TOP + " AS " + TopSites.TYPE + 914 " FROM " + Combined.VIEW_NAME + 915 " WHERE " + ignoreForTopSitesWhereClause + 916 " ORDER BY " + BrowserContract.getCombinedFrecencySortOrder(true, false) + 917 " LIMIT " + limit, 918 ignoreForTopSitesArgs); 919 920 c.setNotificationUri(getContext().getContentResolver(), 921 BrowserContract.AUTHORITY_URI); 922 923 if (c.getCount() == limit) { 924 return c; 925 } 926 927 // If we don't have enough data: get suggested sites too 928 final SuggestedSites suggestedSites = BrowserDB.from(GeckoProfile.get( 929 getContext(), uri.getQueryParameter(BrowserContract.PARAM_PROFILE))).getSuggestedSites(); 930 931 final Cursor suggestedSitesCursor = suggestedSites.get(limit - c.getCount()); 932 933 return new MergeCursor(new Cursor[]{ 934 c, 935 suggestedSitesCursor 936 }); 937 } 938 getTopSites(final Uri uri)939 private Cursor getTopSites(final Uri uri) { 940 // In order to correctly merge the top and pinned sites we: 941 // 942 // 1. Generate a list of free ids for topsites - this is the positions that are NOT used by pinned sites. 943 // We do this using a subquery with a self-join in order to generate rowids, that allow us to join with 944 // the list of topsites. 945 // 2. Generate the list of topsites in order of frecency. 946 // 3. Join these, so that each topsite is given its resulting position 947 // 4. UNION all with the pinned sites, and order by position 948 // 949 // Suggested sites are placed after the topsites, but might still be interspersed with the suggested sites, 950 // hence we append these to the topsite list, and treat these identically to topsites from this point on. 951 // 952 // We require rowids to join the two lists, however subqueries aren't given rowids - hence we use two different 953 // tricks to generate these: 954 // 1. The list of free ids is small, hence we can do a self-join to generate rowids. 955 // 2. The topsites list is larger, hence we use a temporary table, which automatically provides rowids. 956 957 final SQLiteDatabase db = getWritableDatabase(uri); 958 959 final String TABLE_TOPSITES = "topsites"; 960 961 final String limitParam = uri.getQueryParameter(BrowserContract.PARAM_LIMIT); 962 final String gridLimitParam = uri.getQueryParameter(BrowserContract.PARAM_SUGGESTEDSITES_LIMIT); 963 964 final int totalLimit; 965 final int suggestedGridLimit; 966 967 if (limitParam == null) { 968 totalLimit = 50; 969 } else { 970 totalLimit = Integer.parseInt(limitParam, 10); 971 } 972 973 if (gridLimitParam == null) { 974 suggestedGridLimit = getContext().getResources().getInteger(R.integer.number_of_top_sites); 975 } else { 976 suggestedGridLimit = Integer.parseInt(gridLimitParam, 10); 977 } 978 979 final String pinnedSitesFromClause = "FROM " + TABLE_BOOKMARKS + " WHERE " + 980 Bookmarks.PARENT + " == " + Bookmarks.FIXED_PINNED_LIST_ID + 981 " AND " + Bookmarks.IS_DELETED + " IS NOT 1"; 982 983 // Ideally we'd use a recursive CTE to generate our sequence, e.g. something like this worked at one point: 984 // " WITH RECURSIVE" + 985 // " cnt(x) AS (VALUES(1) UNION ALL SELECT x+1 FROM cnt WHERE x < 6)" + 986 // However that requires SQLite >= 3.8.3 (available on Android >= 5.0), so in the meantime 987 // we use a temporary numbers table. 988 // Note: SQLite rowids are 1-indexed, whereas we're expecting 0-indexed values for the position. Our numbers 989 // table starts at position = 0, which ensures the correct results here. 990 final String freeIDSubquery = 991 " SELECT count(free_ids.position) + 1 AS rowid, numbers.position AS " + Bookmarks.POSITION + 992 " FROM (SELECT position FROM numbers WHERE position NOT IN (SELECT " + Bookmarks.POSITION + " " + pinnedSitesFromClause + ")) AS numbers" + 993 " LEFT OUTER JOIN " + 994 " (SELECT position FROM numbers WHERE position NOT IN (SELECT " + Bookmarks.POSITION + " " + pinnedSitesFromClause + ")) AS free_ids" + 995 " ON numbers.position > free_ids.position" + 996 " GROUP BY numbers.position" + 997 " ORDER BY numbers.position ASC" + 998 " LIMIT " + suggestedGridLimit; 999 1000 // Filter out: unvisited pages (history_id == -1) pinned (and other special) sites, deleted sites, 1001 // and about: pages. 1002 final String ignoreForTopSitesWhereClause = 1003 "(" + Combined.HISTORY_ID + " IS NOT -1)" + 1004 " AND " + 1005 Combined.URL + " NOT IN (SELECT " + 1006 Bookmarks.URL + " FROM bookmarks WHERE " + 1007 DBUtils.qualifyColumn("bookmarks", Bookmarks.PARENT) + " < " + Bookmarks.FIXED_ROOT_ID + " AND " + 1008 DBUtils.qualifyColumn("bookmarks", Bookmarks.IS_DELETED) + " == 0)" + 1009 " AND " + 1010 "(" + Combined.URL + " NOT LIKE ?)"; 1011 1012 final String[] ignoreForTopSitesArgs = new String[] { 1013 AboutPages.URL_FILTER 1014 }; 1015 1016 // Stuff the suggested sites into SQL: this allows us to filter pinned and topsites out of the suggested 1017 // sites list as part of the final query (as opposed to walking cursors in java) 1018 final SuggestedSites suggestedSites = BrowserDB.from(GeckoProfile.get( 1019 getContext(), uri.getQueryParameter(BrowserContract.PARAM_PROFILE))).getSuggestedSites(); 1020 1021 StringBuilder suggestedSitesBuilder = new StringBuilder(); 1022 // We could access the underlying data here, however SuggestedSites also performs filtering on the suggested 1023 // sites list, which means we'd need to process the lists within SuggestedSites in any case. If we're doing 1024 // that processing, there is little real between us using a MatrixCursor, or a Map (or List) instead of the 1025 // MatrixCursor. 1026 final Cursor suggestedSitesCursor = suggestedSites.get(suggestedGridLimit); 1027 1028 String[] suggestedSiteArgs = new String[0]; 1029 1030 boolean hasProcessedAnySuggestedSites = false; 1031 1032 final int idColumnIndex = suggestedSitesCursor.getColumnIndexOrThrow(Bookmarks._ID); 1033 final int urlColumnIndex = suggestedSitesCursor.getColumnIndexOrThrow(Bookmarks.URL); 1034 final int titleColumnIndex = suggestedSitesCursor.getColumnIndexOrThrow(Bookmarks.TITLE); 1035 1036 while (suggestedSitesCursor.moveToNext()) { 1037 // We'll be using this as a subquery, hence we need to avoid the preceding UNION ALL 1038 if (hasProcessedAnySuggestedSites) { 1039 suggestedSitesBuilder.append(" UNION ALL"); 1040 } else { 1041 hasProcessedAnySuggestedSites = true; 1042 } 1043 suggestedSitesBuilder.append(" SELECT" + 1044 " ? AS " + Bookmarks._ID + "," + 1045 " ? AS " + Bookmarks.URL + "," + 1046 " ? AS " + Bookmarks.TITLE); 1047 1048 suggestedSiteArgs = DBUtils.appendSelectionArgs(suggestedSiteArgs, 1049 new String[] { 1050 suggestedSitesCursor.getString(idColumnIndex), 1051 suggestedSitesCursor.getString(urlColumnIndex), 1052 suggestedSitesCursor.getString(titleColumnIndex) 1053 }); 1054 } 1055 suggestedSitesCursor.close(); 1056 1057 boolean hasPreparedBlankTiles = false; 1058 1059 // We can somewhat reduce the number of blanks we produce by eliminating suggested sites. 1060 // We do the actual limit calculation in SQL (since we need to take into account the number 1061 // of pinned sites too), but this might avoid producing 5 or so additional blank tiles 1062 // that would then need to be filtered out. 1063 final int maxBlanksNeeded = suggestedGridLimit - suggestedSitesCursor.getCount(); 1064 1065 final StringBuilder blanksBuilder = new StringBuilder(); 1066 for (int i = 0; i < maxBlanksNeeded; i++) { 1067 if (hasPreparedBlankTiles) { 1068 blanksBuilder.append(" UNION ALL"); 1069 } else { 1070 hasPreparedBlankTiles = true; 1071 } 1072 1073 blanksBuilder.append(" SELECT" + 1074 " -1 AS " + Bookmarks._ID + "," + 1075 " '' AS " + Bookmarks.URL + "," + 1076 " '' AS " + Bookmarks.TITLE); 1077 } 1078 1079 1080 1081 // To restrict suggested sites to the grid, we simply subtract the number of topsites (which have already had 1082 // the pinned sites filtered out), and the number of pinned sites. 1083 // SQLite completely ignores negative limits, hence we need to manually limit to 0 in this case. 1084 final String suggestedLimitClause = " LIMIT MAX(0, (" + suggestedGridLimit + " - (SELECT COUNT(*) FROM " + TABLE_TOPSITES + ") - (SELECT COUNT(*) " + pinnedSitesFromClause + "))) "; 1085 1086 // Pinned site positions are zero indexed, but we need to get the maximum 1-indexed position. 1087 // Hence to correctly calculate the largest pinned position (which should be 0 if there are 1088 // no sites, or 1-6 if we have at least one pinned site), we coalesce the DB position (0-5) 1089 // with -1 to represent no-sites, which allows us to directly add 1 to obtain the expected value 1090 // regardless of whether a position was actually retrieved. 1091 final String blanksLimitClause = " LIMIT MAX(0, " + 1092 "COALESCE((SELECT " + Bookmarks.POSITION + " " + pinnedSitesFromClause + "), -1) + 1" + 1093 " - (SELECT COUNT(*) " + pinnedSitesFromClause + ")" + 1094 " - (SELECT COUNT(*) FROM " + TABLE_TOPSITES + ")" + 1095 ")"; 1096 1097 db.beginTransaction(); 1098 try { 1099 db.execSQL("DROP TABLE IF EXISTS " + TABLE_TOPSITES); 1100 1101 db.execSQL("CREATE TEMP TABLE " + TABLE_TOPSITES + " AS" + 1102 " SELECT " + 1103 Bookmarks._ID + ", " + 1104 Combined.BOOKMARK_ID + ", " + 1105 Combined.HISTORY_ID + ", " + 1106 Bookmarks.URL + ", " + 1107 Bookmarks.TITLE + ", " + 1108 Combined.HISTORY_ID + ", " + 1109 TopSites.TYPE_TOP + " AS " + TopSites.TYPE + 1110 " FROM " + Combined.VIEW_NAME + 1111 " WHERE " + ignoreForTopSitesWhereClause + 1112 " ORDER BY " + BrowserContract.getCombinedFrecencySortOrder(true, false) + 1113 " LIMIT " + totalLimit, 1114 1115 ignoreForTopSitesArgs); 1116 1117 if (hasProcessedAnySuggestedSites) { 1118 db.execSQL("INSERT INTO " + TABLE_TOPSITES + 1119 // We need to LIMIT _after_ selecting the relevant suggested sites, which requires us to 1120 // use an additional internal subquery, since we cannot LIMIT a subquery that is part of UNION ALL. 1121 // Hence the weird SELECT * FROM (SELECT ...relevant suggested sites... LIMIT ?) 1122 " SELECT * FROM (SELECT " + 1123 Bookmarks._ID + ", " + 1124 Bookmarks._ID + " AS " + Combined.BOOKMARK_ID + ", " + 1125 " -1 AS " + Combined.HISTORY_ID + ", " + 1126 Bookmarks.URL + ", " + 1127 Bookmarks.TITLE + ", " + 1128 "NULL AS " + Combined.HISTORY_ID + ", " + 1129 TopSites.TYPE_SUGGESTED + " as " + TopSites.TYPE + 1130 " FROM ( " + suggestedSitesBuilder.toString() + " )" + 1131 " WHERE " + 1132 Bookmarks.URL + " NOT IN (SELECT url FROM " + TABLE_TOPSITES + ")" + 1133 " AND " + 1134 Bookmarks.URL + " NOT IN (SELECT url " + pinnedSitesFromClause + ")" + 1135 suggestedLimitClause + " )", 1136 1137 suggestedSiteArgs); 1138 } 1139 1140 if (hasPreparedBlankTiles) { 1141 db.execSQL("INSERT INTO " + TABLE_TOPSITES + 1142 // We need to LIMIT _after_ selecting the relevant suggested sites, which requires us to 1143 // use an additional internal subquery, since we cannot LIMIT a subquery that is part of UNION ALL. 1144 // Hence the weird SELECT * FROM (SELECT ...relevant suggested sites... LIMIT ?) 1145 " SELECT * FROM (SELECT " + 1146 Bookmarks._ID + ", " + 1147 Bookmarks._ID + " AS " + Combined.BOOKMARK_ID + ", " + 1148 " -1 AS " + Combined.HISTORY_ID + ", " + 1149 Bookmarks.URL + ", " + 1150 Bookmarks.TITLE + ", " + 1151 "NULL AS " + Combined.HISTORY_ID + ", " + 1152 TopSites.TYPE_BLANK + " as " + TopSites.TYPE + 1153 " FROM ( " + blanksBuilder.toString() + " )" + 1154 blanksLimitClause + " )"); 1155 } 1156 1157 // If we retrieve more topsites than we have free positions for in the freeIdSubquery, 1158 // we will have topsites that don't receive a position when joining TABLE_TOPSITES 1159 // with freeIdSubquery. Hence we need to coalesce the position with a generated position. 1160 // We know that the difference in positions will be at most suggestedGridLimit, hence we 1161 // can add that to the rowid to generate a safe position. 1162 // I.e. if we have 6 pinned sites then positions 0..5 are filled, the JOIN results in 1163 // the first N rows having positions 6..(N+6), so row N+1 should receive a position that is at 1164 // least N+1+6, which is equal to rowid + 6. 1165 final SQLiteCursor c = (SQLiteCursor) db.rawQuery( 1166 "SELECT " + 1167 Bookmarks._ID + ", " + 1168 TopSites.BOOKMARK_ID + ", " + 1169 TopSites.HISTORY_ID + ", " + 1170 Bookmarks.URL + ", " + 1171 Bookmarks.TITLE + ", " + 1172 "COALESCE(" + Bookmarks.POSITION + ", " + 1173 DBUtils.qualifyColumn(TABLE_TOPSITES, "rowid") + " + " + suggestedGridLimit + 1174 ")" + " AS " + Bookmarks.POSITION + ", " + 1175 Combined.HISTORY_ID + ", " + 1176 TopSites.TYPE + 1177 " FROM " + TABLE_TOPSITES + 1178 " LEFT OUTER JOIN " + // TABLE_IDS + 1179 "(" + freeIDSubquery + ") AS id_results" + 1180 " ON " + DBUtils.qualifyColumn(TABLE_TOPSITES, "rowid") + 1181 " = " + DBUtils.qualifyColumn("id_results", "rowid") + 1182 1183 " UNION ALL " + 1184 1185 "SELECT " + 1186 Bookmarks._ID + ", " + 1187 Bookmarks._ID + " AS " + TopSites.BOOKMARK_ID + ", " + 1188 " -1 AS " + TopSites.HISTORY_ID + ", " + 1189 Bookmarks.URL + ", " + 1190 Bookmarks.TITLE + ", " + 1191 Bookmarks.POSITION + ", " + 1192 "NULL AS " + Combined.HISTORY_ID + ", " + 1193 TopSites.TYPE_PINNED + " as " + TopSites.TYPE + 1194 " " + pinnedSitesFromClause + 1195 1196 " ORDER BY " + Bookmarks.POSITION, 1197 1198 null); 1199 1200 c.setNotificationUri(getContext().getContentResolver(), 1201 BrowserContract.AUTHORITY_URI); 1202 1203 // Force the cursor to be compiled and the cursor-window filled now: 1204 // (A) without compiling the cursor now we won't have access to the TEMP table which 1205 // is removed as soon as we close our connection. 1206 // (B) this might also mitigate the situation causing this crash where we're accessing 1207 // a cursor and crashing in fillWindow. 1208 c.moveToFirst(); 1209 1210 db.setTransactionSuccessful(); 1211 return c; 1212 } finally { 1213 db.endTransaction(); 1214 } 1215 } 1216 1217 /** 1218 * Obtain a set of links for highlights (from bookmarks and history). 1219 * 1220 * Based on the query for Activity^ Stream (desktop): 1221 * https://github.com/mozilla/activity-stream/blob/9eb9f451b553bb62ae9b8d6b41a8ef94a2e020ea/addon/PlacesProvider.js#L578 1222 */ getHighlights(final SQLiteDatabase db, String limit)1223 public Cursor getHighlights(final SQLiteDatabase db, String limit) { 1224 final int totalLimit = limit == null ? 20 : Integer.parseInt(limit); 1225 1226 final long threeDaysAgo = System.currentTimeMillis() - (1000 * 60 * 60 * 24 * 3); 1227 final long bookmarkLimit = 1; 1228 1229 // Select recent bookmarks that have not been visited much 1230 final String bookmarksQuery = "SELECT * FROM (SELECT " + 1231 "-1 AS " + Combined.HISTORY_ID + ", " + 1232 DBUtils.qualifyColumn(Bookmarks.TABLE_NAME, Bookmarks._ID) + " AS " + Combined.BOOKMARK_ID + ", " + 1233 DBUtils.qualifyColumn(Bookmarks.TABLE_NAME, Bookmarks.URL) + ", " + 1234 DBUtils.qualifyColumn(Bookmarks.TABLE_NAME, Bookmarks.TITLE) + ", " + 1235 DBUtils.qualifyColumn(Bookmarks.TABLE_NAME, Bookmarks.DATE_CREATED) + " AS " + Highlights.DATE + " " + 1236 "FROM " + Bookmarks.TABLE_NAME + " " + 1237 "LEFT JOIN " + History.TABLE_NAME + " ON " + 1238 DBUtils.qualifyColumn(Bookmarks.TABLE_NAME, Bookmarks.URL) + " = " + 1239 DBUtils.qualifyColumn(History.TABLE_NAME, History.URL) + " " + 1240 "WHERE " + DBUtils.qualifyColumn(Bookmarks.TABLE_NAME, Bookmarks.DATE_CREATED) + " > " + threeDaysAgo + " " + 1241 "AND (" + DBUtils.qualifyColumn(History.TABLE_NAME, History.VISITS) + " <= 3 " + 1242 "OR " + DBUtils.qualifyColumn(History.TABLE_NAME, History.VISITS) + " IS NULL) " + 1243 "AND " + DBUtils.qualifyColumn(Bookmarks.TABLE_NAME, Bookmarks.IS_DELETED) + " = 0 " + 1244 "AND " + DBUtils.qualifyColumn(Bookmarks.TABLE_NAME, Bookmarks.TYPE) + " = " + Bookmarks.TYPE_BOOKMARK + " " + 1245 "AND " + DBUtils.qualifyColumn(Bookmarks.TABLE_NAME, Bookmarks.URL) + " NOT IN (SELECT " + ActivityStreamBlocklist.URL + " FROM " + ActivityStreamBlocklist.TABLE_NAME + " )" + 1246 "ORDER BY " + DBUtils.qualifyColumn(Bookmarks.TABLE_NAME, Bookmarks.DATE_CREATED) + " DESC " + 1247 "LIMIT " + bookmarkLimit + ")"; 1248 1249 final long last30Minutes = System.currentTimeMillis() - (1000 * 60 * 30); 1250 final long historyLimit = totalLimit - bookmarkLimit; 1251 1252 // Select recent history that has not been visited much. 1253 final String historyQuery = "SELECT * FROM (SELECT " + 1254 History._ID + " AS " + Combined.HISTORY_ID + ", " + 1255 "-1 AS " + Combined.BOOKMARK_ID + ", " + 1256 History.URL + ", " + 1257 History.TITLE + ", " + 1258 History.DATE_LAST_VISITED + " AS " + Highlights.DATE + " " + 1259 "FROM " + History.TABLE_NAME + " " + 1260 "WHERE " + History.DATE_LAST_VISITED + " < " + last30Minutes + " " + 1261 "AND " + History.VISITS + " <= 3 " + 1262 "AND " + History.TITLE + " NOT NULL AND " + History.TITLE + " != '' " + 1263 "AND " + History.IS_DELETED + " = 0 " + 1264 "AND " + History.URL + " NOT IN (SELECT " + ActivityStreamBlocklist.URL + " FROM " + ActivityStreamBlocklist.TABLE_NAME + " )" + 1265 // TODO: Implement domain black list (bug 1298786) 1266 // TODO: Group by host (bug 1298785) 1267 "ORDER BY " + History.DATE_LAST_VISITED + " DESC " + 1268 "LIMIT " + historyLimit + ")"; 1269 1270 final String query = "SELECT DISTINCT * " + 1271 "FROM (" + bookmarksQuery + " " + 1272 "UNION ALL " + historyQuery + ") " + 1273 "GROUP BY " + Combined.URL + ";"; 1274 1275 final Cursor cursor = db.rawQuery(query, null); 1276 1277 cursor.setNotificationUri(getContext().getContentResolver(), 1278 BrowserContract.AUTHORITY_URI); 1279 1280 return cursor; 1281 } 1282 1283 @Override query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)1284 public Cursor query(Uri uri, String[] projection, String selection, 1285 String[] selectionArgs, String sortOrder) { 1286 final int match = URI_MATCHER.match(uri); 1287 1288 // Handle only queries requiring a writable DB connection here: most queries need only a readable 1289 // connection, hence we can get a readable DB once, and then handle most queries within a switch. 1290 // TopSites requires a writable connection (because of the temporary tables it uses), hence 1291 // we handle that separately, i.e. before retrieving a readable connection. 1292 if (match == TOPSITES) { 1293 if (uri.getBooleanQueryParameter(BrowserContract.PARAM_TOPSITES_DISABLE_PINNED, false)) { 1294 return getPlainTopSites(uri); 1295 } else { 1296 return getTopSites(uri); 1297 } 1298 } 1299 1300 SQLiteDatabase db = getReadableDatabase(uri); 1301 1302 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 1303 String limit = uri.getQueryParameter(BrowserContract.PARAM_LIMIT); 1304 String groupBy = null; 1305 1306 switch (match) { 1307 case BOOKMARKS_FOLDER_ID: 1308 case BOOKMARKS_ID: 1309 case BOOKMARKS: { 1310 debug("Query is on bookmarks: " + uri); 1311 1312 if (match == BOOKMARKS_ID) { 1313 selection = DBUtils.concatenateWhere(selection, Bookmarks._ID + " = ?"); 1314 selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, 1315 new String[] { Long.toString(ContentUris.parseId(uri)) }); 1316 } else if (match == BOOKMARKS_FOLDER_ID) { 1317 selection = DBUtils.concatenateWhere(selection, Bookmarks.PARENT + " = ?"); 1318 selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, 1319 new String[] { Long.toString(ContentUris.parseId(uri)) }); 1320 } 1321 1322 if (!shouldShowDeleted(uri)) 1323 selection = DBUtils.concatenateWhere(Bookmarks.IS_DELETED + " = 0", selection); 1324 1325 if (TextUtils.isEmpty(sortOrder)) { 1326 sortOrder = DEFAULT_BOOKMARKS_SORT_ORDER; 1327 } else { 1328 debug("Using sort order " + sortOrder + "."); 1329 } 1330 1331 qb.setProjectionMap(BOOKMARKS_PROJECTION_MAP); 1332 1333 if (hasFaviconsInProjection(projection)) { 1334 qb.setTables(VIEW_BOOKMARKS_WITH_FAVICONS); 1335 } else if (selection != null && selection.contains(Bookmarks.ANNOTATION_KEY)) { 1336 qb.setTables(VIEW_BOOKMARKS_WITH_ANNOTATIONS); 1337 1338 groupBy = uri.getQueryParameter(BrowserContract.PARAM_GROUP_BY); 1339 } else { 1340 qb.setTables(TABLE_BOOKMARKS); 1341 } 1342 1343 break; 1344 } 1345 1346 case HISTORY_ID: 1347 selection = DBUtils.concatenateWhere(selection, History._ID + " = ?"); 1348 selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, 1349 new String[] { Long.toString(ContentUris.parseId(uri)) }); 1350 // fall through 1351 case HISTORY: { 1352 debug("Query is on history: " + uri); 1353 1354 if (!shouldShowDeleted(uri)) 1355 selection = DBUtils.concatenateWhere(History.IS_DELETED + " = 0", selection); 1356 1357 if (TextUtils.isEmpty(sortOrder)) 1358 sortOrder = DEFAULT_HISTORY_SORT_ORDER; 1359 1360 qb.setProjectionMap(HISTORY_PROJECTION_MAP); 1361 1362 if (hasFaviconsInProjection(projection)) 1363 qb.setTables(VIEW_HISTORY_WITH_FAVICONS); 1364 else 1365 qb.setTables(TABLE_HISTORY); 1366 1367 break; 1368 } 1369 1370 case VISITS: 1371 debug("Query is on visits: " + uri); 1372 qb.setProjectionMap(VISIT_PROJECTION_MAP); 1373 qb.setTables(TABLE_VISITS); 1374 1375 if (TextUtils.isEmpty(sortOrder)) { 1376 sortOrder = DEFAULT_VISITS_SORT_ORDER; 1377 } 1378 break; 1379 1380 case FAVICON_ID: 1381 selection = DBUtils.concatenateWhere(selection, Favicons._ID + " = ?"); 1382 selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, 1383 new String[] { Long.toString(ContentUris.parseId(uri)) }); 1384 // fall through 1385 case FAVICONS: { 1386 debug("Query is on favicons: " + uri); 1387 1388 qb.setProjectionMap(FAVICONS_PROJECTION_MAP); 1389 qb.setTables(TABLE_FAVICONS); 1390 1391 break; 1392 } 1393 1394 case THUMBNAIL_ID: 1395 selection = DBUtils.concatenateWhere(selection, Thumbnails._ID + " = ?"); 1396 selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, 1397 new String[] { Long.toString(ContentUris.parseId(uri)) }); 1398 // fall through 1399 case THUMBNAILS: { 1400 debug("Query is on thumbnails: " + uri); 1401 1402 qb.setProjectionMap(THUMBNAILS_PROJECTION_MAP); 1403 qb.setTables(TABLE_THUMBNAILS); 1404 1405 break; 1406 } 1407 1408 case URL_ANNOTATIONS: 1409 debug("Query is on url annotations: " + uri); 1410 1411 qb.setProjectionMap(URL_ANNOTATIONS_PROJECTION_MAP); 1412 qb.setTables(TABLE_URL_ANNOTATIONS); 1413 break; 1414 1415 case SCHEMA: { 1416 debug("Query is on schema."); 1417 MatrixCursor schemaCursor = new MatrixCursor(new String[] { Schema.VERSION }); 1418 schemaCursor.newRow().add(BrowserDatabaseHelper.DATABASE_VERSION); 1419 1420 return schemaCursor; 1421 } 1422 1423 case COMBINED: { 1424 debug("Query is on combined: " + uri); 1425 1426 if (TextUtils.isEmpty(sortOrder)) 1427 sortOrder = DEFAULT_HISTORY_SORT_ORDER; 1428 1429 // This will avoid duplicate entries in the awesomebar 1430 // results when a history entry has multiple bookmarks. 1431 groupBy = Combined.URL; 1432 1433 qb.setProjectionMap(COMBINED_PROJECTION_MAP); 1434 1435 if (hasFaviconsInProjection(projection)) 1436 qb.setTables(VIEW_COMBINED_WITH_FAVICONS); 1437 else 1438 qb.setTables(Combined.VIEW_NAME); 1439 1440 break; 1441 } 1442 1443 case HIGHLIGHTS: { 1444 debug("Highlights query: " + uri); 1445 1446 return getHighlights(db, limit); 1447 } 1448 1449 case PAGE_METADATA: { 1450 debug("PageMetadata query: " + uri); 1451 1452 qb.setProjectionMap(PAGE_METADATA_PROJECTION_MAP); 1453 qb.setTables(TABLE_PAGE_METADATA); 1454 break; 1455 } 1456 1457 default: { 1458 Table table = findTableFor(match); 1459 if (table == null) { 1460 throw new UnsupportedOperationException("Unknown query URI " + uri); 1461 } 1462 trace("Update TABLE: " + uri); 1463 return table.query(db, uri, match, projection, selection, selectionArgs, sortOrder, groupBy, limit); 1464 } 1465 } 1466 1467 trace("Running built query."); 1468 Cursor cursor = qb.query(db, projection, selection, selectionArgs, groupBy, 1469 null, sortOrder, limit); 1470 cursor.setNotificationUri(getContext().getContentResolver(), 1471 BrowserContract.AUTHORITY_URI); 1472 1473 return cursor; 1474 } 1475 1476 /** 1477 * Update the positions of bookmarks in batches. 1478 * 1479 * Begins and ends its own transactions. 1480 * 1481 * @see #updateBookmarkPositionsInTransaction(SQLiteDatabase, String[], int, int) 1482 */ updateBookmarkPositions(Uri uri, String[] guids)1483 private int updateBookmarkPositions(Uri uri, String[] guids) { 1484 if (guids == null) { 1485 return 0; 1486 } 1487 1488 int guidsCount = guids.length; 1489 if (guidsCount == 0) { 1490 return 0; 1491 } 1492 1493 int offset = 0; 1494 int updated = 0; 1495 1496 final SQLiteDatabase db = getWritableDatabase(uri); 1497 db.beginTransaction(); 1498 1499 while (offset < guidsCount) { 1500 try { 1501 updated += updateBookmarkPositionsInTransaction(db, guids, offset, 1502 MAX_POSITION_UPDATES_PER_QUERY); 1503 } catch (SQLException e) { 1504 Log.e(LOGTAG, "Got SQLite exception updating bookmark positions at offset " + offset, e); 1505 1506 // Need to restart the transaction. 1507 // The only way a caller knows that anything failed is that the 1508 // returned update count will be smaller than the requested 1509 // number of records. 1510 db.setTransactionSuccessful(); 1511 db.endTransaction(); 1512 1513 db.beginTransaction(); 1514 } 1515 1516 offset += MAX_POSITION_UPDATES_PER_QUERY; 1517 } 1518 1519 db.setTransactionSuccessful(); 1520 db.endTransaction(); 1521 1522 return updated; 1523 } 1524 1525 /** 1526 * Construct and execute an update expression that will modify the positions 1527 * of records in-place. 1528 */ updateBookmarkPositionsInTransaction(final SQLiteDatabase db, final String[] guids, final int offset, final int max)1529 private static int updateBookmarkPositionsInTransaction(final SQLiteDatabase db, final String[] guids, 1530 final int offset, final int max) { 1531 int guidsCount = guids.length; 1532 int processCount = Math.min(max, guidsCount - offset); 1533 1534 // Each must appear twice: once in a CASE, and once in the IN clause. 1535 String[] args = new String[processCount * 2]; 1536 System.arraycopy(guids, offset, args, 0, processCount); 1537 System.arraycopy(guids, offset, args, processCount, processCount); 1538 1539 StringBuilder b = new StringBuilder("UPDATE " + TABLE_BOOKMARKS + 1540 " SET " + Bookmarks.POSITION + 1541 " = CASE guid"); 1542 1543 // Build the CASE statement body for GUID/index pairs from offset up to 1544 // the computed limit. 1545 final int end = offset + processCount; 1546 int i = offset; 1547 for (; i < end; ++i) { 1548 if (guids[i] == null) { 1549 // We don't want to issue the query if not every GUID is specified. 1550 debug("updateBookmarkPositions called with null GUID at index " + i); 1551 return 0; 1552 } 1553 b.append(" WHEN ? THEN " + i); 1554 } 1555 1556 b.append(" END WHERE " + DBUtils.computeSQLInClause(processCount, Bookmarks.GUID)); 1557 db.execSQL(b.toString(), args); 1558 1559 // We can't easily get a modified count without calling something like changes(). 1560 return processCount; 1561 } 1562 1563 /** 1564 * Construct an update expression that will modify the parents of any records 1565 * that match. 1566 */ updateBookmarkParents(SQLiteDatabase db, ContentValues values, String selection, String[] selectionArgs)1567 private int updateBookmarkParents(SQLiteDatabase db, ContentValues values, String selection, String[] selectionArgs) { 1568 trace("Updating bookmark parents of " + selection + " (" + selectionArgs[0] + ")"); 1569 String where = Bookmarks._ID + " IN (" + 1570 " SELECT DISTINCT " + Bookmarks.PARENT + 1571 " FROM " + TABLE_BOOKMARKS + 1572 " WHERE " + selection + " )"; 1573 return db.update(TABLE_BOOKMARKS, values, where, selectionArgs); 1574 } 1575 insertBookmark(Uri uri, ContentValues values)1576 private long insertBookmark(Uri uri, ContentValues values) { 1577 // Generate values if not specified. Don't overwrite 1578 // if specified by caller. 1579 long now = System.currentTimeMillis(); 1580 if (!values.containsKey(Bookmarks.DATE_CREATED)) { 1581 values.put(Bookmarks.DATE_CREATED, now); 1582 } 1583 1584 if (!values.containsKey(Bookmarks.DATE_MODIFIED)) { 1585 values.put(Bookmarks.DATE_MODIFIED, now); 1586 } 1587 1588 if (!values.containsKey(Bookmarks.GUID)) { 1589 values.put(Bookmarks.GUID, Utils.generateGuid()); 1590 } 1591 1592 if (!values.containsKey(Bookmarks.POSITION)) { 1593 debug("Inserting bookmark with no position for URI"); 1594 values.put(Bookmarks.POSITION, 1595 Long.toString(BrowserContract.Bookmarks.DEFAULT_POSITION)); 1596 } 1597 1598 if (!values.containsKey(Bookmarks.TITLE)) { 1599 // Desktop Places barfs on insertion of a bookmark with no title, 1600 // so we don't store them that way. 1601 values.put(Bookmarks.TITLE, ""); 1602 } 1603 1604 String url = values.getAsString(Bookmarks.URL); 1605 1606 debug("Inserting bookmark in database with URL: " + url); 1607 final SQLiteDatabase db = getWritableDatabase(uri); 1608 beginWrite(db); 1609 return db.insertOrThrow(TABLE_BOOKMARKS, Bookmarks.TITLE, values); 1610 } 1611 1612 updateOrInsertBookmark(Uri uri, ContentValues values, String selection, String[] selectionArgs)1613 private int updateOrInsertBookmark(Uri uri, ContentValues values, String selection, 1614 String[] selectionArgs) { 1615 int updated = updateBookmarks(uri, values, selection, selectionArgs); 1616 if (updated > 0) { 1617 return updated; 1618 } 1619 1620 // Transaction already begun by updateBookmarks. 1621 if (0 <= insertBookmark(uri, values)) { 1622 // We 'updated' one row. 1623 return 1; 1624 } 1625 1626 // If something went wrong, then we updated zero rows. 1627 return 0; 1628 } 1629 updateBookmarks(Uri uri, ContentValues values, String selection, String[] selectionArgs)1630 private int updateBookmarks(Uri uri, ContentValues values, String selection, 1631 String[] selectionArgs) { 1632 trace("Updating bookmarks on URI: " + uri); 1633 1634 final String[] bookmarksProjection = new String[] { 1635 Bookmarks._ID, // 0 1636 }; 1637 1638 if (!values.containsKey(Bookmarks.DATE_MODIFIED)) { 1639 values.put(Bookmarks.DATE_MODIFIED, System.currentTimeMillis()); 1640 } 1641 1642 trace("Querying bookmarks to update on URI: " + uri); 1643 final SQLiteDatabase db = getWritableDatabase(uri); 1644 1645 // Compute matching IDs. 1646 final Cursor cursor = db.query(TABLE_BOOKMARKS, bookmarksProjection, 1647 selection, selectionArgs, null, null, null); 1648 1649 // Now that we're done reading, open a transaction. 1650 final String inClause; 1651 try { 1652 inClause = DBUtils.computeSQLInClauseFromLongs(cursor, Bookmarks._ID); 1653 } finally { 1654 cursor.close(); 1655 } 1656 1657 beginWrite(db); 1658 return db.update(TABLE_BOOKMARKS, values, inClause, null); 1659 } 1660 insertHistory(Uri uri, ContentValues values)1661 private long insertHistory(Uri uri, ContentValues values) { 1662 final long now = System.currentTimeMillis(); 1663 values.put(History.DATE_CREATED, now); 1664 values.put(History.DATE_MODIFIED, now); 1665 1666 // Generate GUID for new history entry. Don't override specified GUIDs. 1667 if (!values.containsKey(History.GUID)) { 1668 values.put(History.GUID, Utils.generateGuid()); 1669 } 1670 1671 String url = values.getAsString(History.URL); 1672 1673 debug("Inserting history in database with URL: " + url); 1674 final SQLiteDatabase db = getWritableDatabase(uri); 1675 beginWrite(db); 1676 return db.insertOrThrow(TABLE_HISTORY, History.VISITS, values); 1677 } 1678 updateOrInsertHistory(Uri uri, ContentValues values, String selection, String[] selectionArgs)1679 private int updateOrInsertHistory(Uri uri, ContentValues values, String selection, 1680 String[] selectionArgs) { 1681 final int updated = updateHistory(uri, values, selection, selectionArgs); 1682 if (updated > 0) { 1683 return updated; 1684 } 1685 1686 // Insert a new entry if necessary, setting visit and date aggregate values. 1687 if (!values.containsKey(History.VISITS)) { 1688 values.put(History.VISITS, 1); 1689 values.put(History.LOCAL_VISITS, 1); 1690 } else { 1691 values.put(History.LOCAL_VISITS, values.getAsInteger(History.VISITS)); 1692 } 1693 if (values.containsKey(History.DATE_LAST_VISITED)) { 1694 values.put(History.LOCAL_DATE_LAST_VISITED, values.getAsLong(History.DATE_LAST_VISITED)); 1695 } 1696 if (!values.containsKey(History.TITLE)) { 1697 values.put(History.TITLE, values.getAsString(History.URL)); 1698 } 1699 1700 if (0 <= insertHistory(uri, values)) { 1701 return 1; 1702 } 1703 1704 return 0; 1705 } 1706 updateHistory(Uri uri, ContentValues values, String selection, String[] selectionArgs)1707 private int updateHistory(Uri uri, ContentValues values, String selection, 1708 String[] selectionArgs) { 1709 trace("Updating history on URI: " + uri); 1710 1711 final SQLiteDatabase db = getWritableDatabase(uri); 1712 1713 if (!values.containsKey(History.DATE_MODIFIED)) { 1714 values.put(History.DATE_MODIFIED, System.currentTimeMillis()); 1715 } 1716 1717 // Use the simple code path for easy updates. 1718 if (!shouldIncrementVisits(uri) && !shouldIncrementRemoteAggregates(uri)) { 1719 trace("Updating history meta data only"); 1720 return db.update(TABLE_HISTORY, values, selection, selectionArgs); 1721 } 1722 1723 trace("Updating history meta data and incrementing visits"); 1724 1725 if (values.containsKey(History.DATE_LAST_VISITED)) { 1726 values.put(History.LOCAL_DATE_LAST_VISITED, values.getAsLong(History.DATE_LAST_VISITED)); 1727 } 1728 1729 // Create a separate set of values that will be updated as an expression. 1730 final ContentValues visits = new ContentValues(); 1731 if (shouldIncrementVisits(uri)) { 1732 // Update data and increment visits by 1. 1733 final long incVisits = 1; 1734 1735 visits.put(History.VISITS, History.VISITS + " + " + incVisits); 1736 visits.put(History.LOCAL_VISITS, History.LOCAL_VISITS + " + " + incVisits); 1737 } 1738 1739 if (shouldIncrementRemoteAggregates(uri)) { 1740 // Let's fail loudly instead of trying to assume what users of this API meant to do. 1741 if (!values.containsKey(History.REMOTE_VISITS)) { 1742 throw new IllegalArgumentException( 1743 "Tried incrementing History.REMOTE_VISITS by unknown value"); 1744 } 1745 visits.put( 1746 History.REMOTE_VISITS, 1747 History.REMOTE_VISITS + " + " + values.getAsInteger(History.REMOTE_VISITS) 1748 ); 1749 // Need to remove passed in value, so that we increment REMOTE_VISITS, and not just set it. 1750 values.remove(History.REMOTE_VISITS); 1751 } 1752 1753 final ContentValues[] valuesAndVisits = { values, visits }; 1754 final UpdateOperation[] ops = { UpdateOperation.ASSIGN, UpdateOperation.EXPRESSION }; 1755 1756 return DBUtils.updateArrays(db, TABLE_HISTORY, valuesAndVisits, ops, selection, selectionArgs); 1757 } 1758 insertVisitForHistory(Uri uri, ContentValues values, String selection, String[] selectionArgs)1759 private long insertVisitForHistory(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 1760 trace("Inserting visit for history on URI: " + uri); 1761 1762 final SQLiteDatabase db = getReadableDatabase(uri); 1763 1764 final Cursor cursor = db.query( 1765 History.TABLE_NAME, new String[] {History.GUID}, selection, selectionArgs, 1766 null, null, null); 1767 if (cursor == null) { 1768 Log.e(LOGTAG, "Null cursor while trying to insert visit for history URI: " + uri); 1769 return 0; 1770 } 1771 final ContentValues[] visitValues; 1772 try { 1773 visitValues = new ContentValues[cursor.getCount()]; 1774 1775 if (!cursor.moveToFirst()) { 1776 Log.e(LOGTAG, "No history records found while inserting visit(s) for history URI: " + uri); 1777 return 0; 1778 } 1779 1780 // Sync works in microseconds, so we store visit timestamps in microseconds as well. 1781 // History timestamps are in milliseconds. 1782 // This is the conversion point for locally generated visits. 1783 final long visitDate; 1784 if (values.containsKey(History.DATE_LAST_VISITED)) { 1785 visitDate = values.getAsLong(History.DATE_LAST_VISITED) * 1000; 1786 } else { 1787 visitDate = System.currentTimeMillis() * 1000; 1788 } 1789 1790 final int guidColumn = cursor.getColumnIndexOrThrow(History.GUID); 1791 while (!cursor.isAfterLast()) { 1792 final ContentValues visit = new ContentValues(); 1793 visit.put(Visits.HISTORY_GUID, cursor.getString(guidColumn)); 1794 visit.put(Visits.DATE_VISITED, visitDate); 1795 visitValues[cursor.getPosition()] = visit; 1796 cursor.moveToNext(); 1797 } 1798 } finally { 1799 cursor.close(); 1800 } 1801 1802 if (visitValues.length == 1) { 1803 return insertVisit(Visits.CONTENT_URI, visitValues[0]); 1804 } else { 1805 return bulkInsert(Visits.CONTENT_URI, visitValues); 1806 } 1807 } 1808 insertVisit(Uri uri, ContentValues values)1809 private long insertVisit(Uri uri, ContentValues values) { 1810 final SQLiteDatabase db = getWritableDatabase(uri); 1811 1812 debug("Inserting history in database with URL: " + uri); 1813 beginWrite(db); 1814 1815 // We ignore insert conflicts here to simplify inserting visits records coming in from Sync. 1816 // Visits table has a unique index on (history_guid,date), so a conflict might arise when we're 1817 // trying to insert history record visits coming in from sync which are already present locally 1818 // as a result of previous sync operations. 1819 // An alternative to doing this is to filter out already present records when we're doing history inserts 1820 // from Sync, which is a costly operation to do en masse. 1821 return db.insertWithOnConflict( 1822 TABLE_VISITS, null, values, SQLiteDatabase.CONFLICT_IGNORE); 1823 } 1824 updateFaviconIdsForUrl(SQLiteDatabase db, String pageUrl, Long faviconId)1825 private void updateFaviconIdsForUrl(SQLiteDatabase db, String pageUrl, Long faviconId) { 1826 ContentValues updateValues = new ContentValues(1); 1827 updateValues.put(FaviconColumns.FAVICON_ID, faviconId); 1828 db.update(TABLE_HISTORY, 1829 updateValues, 1830 History.URL + " = ?", 1831 new String[] { pageUrl }); 1832 db.update(TABLE_BOOKMARKS, 1833 updateValues, 1834 Bookmarks.URL + " = ?", 1835 new String[] { pageUrl }); 1836 } 1837 insertFavicon(Uri uri, ContentValues values)1838 private long insertFavicon(Uri uri, ContentValues values) { 1839 return insertFavicon(getWritableDatabase(uri), values); 1840 } 1841 insertFavicon(SQLiteDatabase db, ContentValues values)1842 private long insertFavicon(SQLiteDatabase db, ContentValues values) { 1843 String faviconUrl = values.getAsString(Favicons.URL); 1844 String pageUrl = null; 1845 1846 trace("Inserting favicon for URL: " + faviconUrl); 1847 1848 DBUtils.stripEmptyByteArray(values, Favicons.DATA); 1849 1850 // Extract the page URL from the ContentValues 1851 if (values.containsKey(Favicons.PAGE_URL)) { 1852 pageUrl = values.getAsString(Favicons.PAGE_URL); 1853 values.remove(Favicons.PAGE_URL); 1854 } 1855 1856 // If no URL is provided, insert using the default one. 1857 if (TextUtils.isEmpty(faviconUrl) && !TextUtils.isEmpty(pageUrl)) { 1858 values.put(Favicons.URL, IconsHelper.guessDefaultFaviconURL(pageUrl)); 1859 } 1860 1861 final long now = System.currentTimeMillis(); 1862 values.put(Favicons.DATE_CREATED, now); 1863 values.put(Favicons.DATE_MODIFIED, now); 1864 1865 beginWrite(db); 1866 final long faviconId = db.insertOrThrow(TABLE_FAVICONS, null, values); 1867 1868 if (pageUrl != null) { 1869 updateFaviconIdsForUrl(db, pageUrl, faviconId); 1870 } 1871 return faviconId; 1872 } 1873 updateOrInsertFavicon(Uri uri, ContentValues values, String selection, String[] selectionArgs)1874 private int updateOrInsertFavicon(Uri uri, ContentValues values, String selection, 1875 String[] selectionArgs) { 1876 return updateFavicon(uri, values, selection, selectionArgs, 1877 true /* insert if needed */); 1878 } 1879 updateExistingFavicon(Uri uri, ContentValues values, String selection, String[] selectionArgs)1880 private int updateExistingFavicon(Uri uri, ContentValues values, String selection, 1881 String[] selectionArgs) { 1882 return updateFavicon(uri, values, selection, selectionArgs, 1883 false /* only update, no insert */); 1884 } 1885 updateFavicon(Uri uri, ContentValues values, String selection, String[] selectionArgs, boolean insertIfNeeded)1886 private int updateFavicon(Uri uri, ContentValues values, String selection, 1887 String[] selectionArgs, boolean insertIfNeeded) { 1888 String faviconUrl = values.getAsString(Favicons.URL); 1889 String pageUrl = null; 1890 int updated = 0; 1891 Long faviconId = null; 1892 long now = System.currentTimeMillis(); 1893 1894 trace("Updating favicon for URL: " + faviconUrl); 1895 1896 DBUtils.stripEmptyByteArray(values, Favicons.DATA); 1897 1898 // Extract the page URL from the ContentValues 1899 if (values.containsKey(Favicons.PAGE_URL)) { 1900 pageUrl = values.getAsString(Favicons.PAGE_URL); 1901 values.remove(Favicons.PAGE_URL); 1902 } 1903 1904 values.put(Favicons.DATE_MODIFIED, now); 1905 1906 final SQLiteDatabase db = getWritableDatabase(uri); 1907 1908 // If there's no favicon URL given and we're inserting if needed, skip 1909 // the update and only do an insert (otherwise all rows would be 1910 // updated). 1911 if (!(insertIfNeeded && (faviconUrl == null))) { 1912 updated = db.update(TABLE_FAVICONS, values, selection, selectionArgs); 1913 } 1914 1915 if (updated > 0) { 1916 if ((faviconUrl != null) && (pageUrl != null)) { 1917 final Cursor cursor = db.query(TABLE_FAVICONS, 1918 new String[] { Favicons._ID }, 1919 Favicons.URL + " = ?", 1920 new String[] { faviconUrl }, 1921 null, null, null); 1922 try { 1923 if (cursor.moveToFirst()) { 1924 faviconId = cursor.getLong(cursor.getColumnIndexOrThrow(Favicons._ID)); 1925 } 1926 } finally { 1927 cursor.close(); 1928 } 1929 } 1930 if (pageUrl != null) { 1931 beginWrite(db); 1932 } 1933 } else if (insertIfNeeded) { 1934 values.put(Favicons.DATE_CREATED, now); 1935 1936 trace("No update, inserting favicon for URL: " + faviconUrl); 1937 beginWrite(db); 1938 faviconId = db.insert(TABLE_FAVICONS, null, values); 1939 updated = 1; 1940 } 1941 1942 if (pageUrl != null) { 1943 updateFaviconIdsForUrl(db, pageUrl, faviconId); 1944 } 1945 1946 return updated; 1947 } 1948 insertThumbnail(Uri uri, ContentValues values)1949 private long insertThumbnail(Uri uri, ContentValues values) { 1950 final String url = values.getAsString(Thumbnails.URL); 1951 1952 trace("Inserting thumbnail for URL: " + url); 1953 1954 DBUtils.stripEmptyByteArray(values, Thumbnails.DATA); 1955 1956 final SQLiteDatabase db = getWritableDatabase(uri); 1957 beginWrite(db); 1958 return db.insertOrThrow(TABLE_THUMBNAILS, null, values); 1959 } 1960 insertActivityStreamBlocklistSite(final Uri uri, final ContentValues values)1961 private long insertActivityStreamBlocklistSite(final Uri uri, final ContentValues values) { 1962 final String url = values.getAsString(ActivityStreamBlocklist.URL); 1963 trace("Inserting url into highlights blocklist, URL: " + url); 1964 1965 final SQLiteDatabase db = getWritableDatabase(uri); 1966 values.put(ActivityStreamBlocklist.CREATED, System.currentTimeMillis()); 1967 1968 beginWrite(db); 1969 return db.insertOrThrow(TABLE_ACTIVITY_STREAM_BLOCKLIST, null, values); 1970 } 1971 insertPageMetadata(final Uri uri, final ContentValues values)1972 private long insertPageMetadata(final Uri uri, final ContentValues values) { 1973 final SQLiteDatabase db = getWritableDatabase(uri); 1974 1975 if (!values.containsKey(PageMetadata.DATE_CREATED)) { 1976 values.put(PageMetadata.DATE_CREATED, System.currentTimeMillis()); 1977 } 1978 1979 beginWrite(db); 1980 1981 // Perform INSERT OR REPLACE, there might be page metadata present and we want to replace it. 1982 // Depends on a conflict arising from unique foreign key (history_guid) constraint violation. 1983 return db.insertWithOnConflict( 1984 TABLE_PAGE_METADATA, null, values, SQLiteDatabase.CONFLICT_REPLACE); 1985 } 1986 insertUrlAnnotation(final Uri uri, final ContentValues values)1987 private long insertUrlAnnotation(final Uri uri, final ContentValues values) { 1988 final String url = values.getAsString(UrlAnnotations.URL); 1989 trace("Inserting url annotations for URL: " + url); 1990 1991 final SQLiteDatabase db = getWritableDatabase(uri); 1992 beginWrite(db); 1993 return db.insertOrThrow(TABLE_URL_ANNOTATIONS, null, values); 1994 } 1995 deleteUrlAnnotation(final Uri uri, final String selection, final String[] selectionArgs)1996 private void deleteUrlAnnotation(final Uri uri, final String selection, final String[] selectionArgs) { 1997 trace("Deleting url annotation for URI: " + uri); 1998 1999 final SQLiteDatabase db = getWritableDatabase(uri); 2000 db.delete(TABLE_URL_ANNOTATIONS, selection, selectionArgs); 2001 } 2002 deletePageMetadata(final Uri uri, final String selection, final String[] selectionArgs)2003 private int deletePageMetadata(final Uri uri, final String selection, final String[] selectionArgs) { 2004 trace("Deleting page metadata for URI: " + uri); 2005 2006 final SQLiteDatabase db = getWritableDatabase(uri); 2007 return db.delete(TABLE_PAGE_METADATA, selection, selectionArgs); 2008 } 2009 updateUrlAnnotation(final Uri uri, final ContentValues values, final String selection, final String[] selectionArgs)2010 private void updateUrlAnnotation(final Uri uri, final ContentValues values, final String selection, final String[] selectionArgs) { 2011 trace("Updating url annotation for URI: " + uri); 2012 2013 final SQLiteDatabase db = getWritableDatabase(uri); 2014 db.update(TABLE_URL_ANNOTATIONS, values, selection, selectionArgs); 2015 } 2016 updateOrInsertThumbnail(Uri uri, ContentValues values, String selection, String[] selectionArgs)2017 private int updateOrInsertThumbnail(Uri uri, ContentValues values, String selection, 2018 String[] selectionArgs) { 2019 return updateThumbnail(uri, values, selection, selectionArgs, 2020 true /* insert if needed */); 2021 } 2022 updateExistingThumbnail(Uri uri, ContentValues values, String selection, String[] selectionArgs)2023 private int updateExistingThumbnail(Uri uri, ContentValues values, String selection, 2024 String[] selectionArgs) { 2025 return updateThumbnail(uri, values, selection, selectionArgs, 2026 false /* only update, no insert */); 2027 } 2028 updateThumbnail(Uri uri, ContentValues values, String selection, String[] selectionArgs, boolean insertIfNeeded)2029 private int updateThumbnail(Uri uri, ContentValues values, String selection, 2030 String[] selectionArgs, boolean insertIfNeeded) { 2031 final String url = values.getAsString(Thumbnails.URL); 2032 DBUtils.stripEmptyByteArray(values, Thumbnails.DATA); 2033 2034 trace("Updating thumbnail for URL: " + url); 2035 2036 final SQLiteDatabase db = getWritableDatabase(uri); 2037 beginWrite(db); 2038 int updated = db.update(TABLE_THUMBNAILS, values, selection, selectionArgs); 2039 2040 if (updated == 0 && insertIfNeeded) { 2041 trace("No update, inserting thumbnail for URL: " + url); 2042 db.insert(TABLE_THUMBNAILS, null, values); 2043 updated = 1; 2044 } 2045 2046 return updated; 2047 } 2048 2049 /** 2050 * This method does not create a new transaction. Its first operation is 2051 * guaranteed to be a write, which in the case of a new enclosing 2052 * transaction will guarantee that a read does not need to be upgraded to 2053 * a write. 2054 */ deleteHistory(SQLiteDatabase db, Uri uri, String selection, String[] selectionArgs)2055 private int deleteHistory(SQLiteDatabase db, Uri uri, String selection, String[] selectionArgs) { 2056 debug("Deleting history entry for URI: " + uri); 2057 2058 if (isCallerSync(uri)) { 2059 return db.delete(TABLE_HISTORY, selection, selectionArgs); 2060 } 2061 2062 debug("Marking history entry as deleted for URI: " + uri); 2063 2064 ContentValues values = new ContentValues(); 2065 values.put(History.IS_DELETED, 1); 2066 2067 // Wipe sensitive data. 2068 values.putNull(History.TITLE); 2069 values.put(History.URL, ""); // Column is NOT NULL. 2070 values.put(History.DATE_CREATED, 0); 2071 values.put(History.DATE_LAST_VISITED, 0); 2072 values.put(History.VISITS, 0); 2073 values.put(History.DATE_MODIFIED, System.currentTimeMillis()); 2074 2075 // Doing this UPDATE (or the DELETE above) first ensures that the 2076 // first operation within a new enclosing transaction is a write. 2077 // The cleanup call below will do a SELECT first, and thus would 2078 // require the transaction to be upgraded from a reader to a writer. 2079 // In some cases that upgrade can fail (SQLITE_BUSY), so we avoid 2080 // it if we can. 2081 final int updated = db.update(TABLE_HISTORY, values, selection, selectionArgs); 2082 try { 2083 cleanUpSomeDeletedRecords(uri, TABLE_HISTORY); 2084 } catch (Exception e) { 2085 // We don't care. 2086 Log.e(LOGTAG, "Unable to clean up deleted history records: ", e); 2087 } 2088 return updated; 2089 } 2090 getHistoryGUIDsFromSelection(SQLiteDatabase db, Uri uri, String selection, String[] selectionArgs)2091 private ArrayList<String> getHistoryGUIDsFromSelection(SQLiteDatabase db, Uri uri, String selection, String[] selectionArgs) { 2092 final ArrayList<String> historyGUIDs = new ArrayList<>(); 2093 2094 final Cursor cursor = db.query( 2095 History.TABLE_NAME, new String[] {History.GUID}, selection, selectionArgs, 2096 null, null, null); 2097 if (cursor == null) { 2098 Log.e(LOGTAG, "Null cursor while trying to delete visits for history URI: " + uri); 2099 return historyGUIDs; 2100 } 2101 2102 try { 2103 if (!cursor.moveToFirst()) { 2104 trace("No history items for which to remove visits matched for URI: " + uri); 2105 return historyGUIDs; 2106 } 2107 final int historyColumn = cursor.getColumnIndexOrThrow(History.GUID); 2108 while (!cursor.isAfterLast()) { 2109 historyGUIDs.add(cursor.getString(historyColumn)); 2110 cursor.moveToNext(); 2111 } 2112 } finally { 2113 cursor.close(); 2114 } 2115 2116 return historyGUIDs; 2117 } 2118 deletePageMetadataForHistory(SQLiteDatabase db, ArrayList<String> historyGUIDs)2119 private int deletePageMetadataForHistory(SQLiteDatabase db, ArrayList<String> historyGUIDs) { 2120 return bulkDeleteByHistoryGUID(db, historyGUIDs, PageMetadata.TABLE_NAME, PageMetadata.HISTORY_GUID); 2121 } 2122 deleteVisitsForHistory(SQLiteDatabase db, ArrayList<String> historyGUIDs)2123 private int deleteVisitsForHistory(SQLiteDatabase db, ArrayList<String> historyGUIDs) { 2124 return bulkDeleteByHistoryGUID(db, historyGUIDs, Visits.TABLE_NAME, Visits.HISTORY_GUID); 2125 } 2126 bulkDeleteByHistoryGUID(SQLiteDatabase db, ArrayList<String> historyGUIDs, String table, String historyGUIDColumn)2127 private int bulkDeleteByHistoryGUID(SQLiteDatabase db, ArrayList<String> historyGUIDs, String table, String historyGUIDColumn) { 2128 // Due to SQLite's maximum variable limitation, we need to chunk our delete statements. 2129 // For example, if there were 1200 GUIDs, this will perform 2 delete statements. 2130 int deleted = 0; 2131 for (int chunk = 0; chunk <= historyGUIDs.size() / DBUtils.SQLITE_MAX_VARIABLE_NUMBER; chunk++) { 2132 final int chunkStart = chunk * DBUtils.SQLITE_MAX_VARIABLE_NUMBER; 2133 int chunkEnd = (chunk + 1) * DBUtils.SQLITE_MAX_VARIABLE_NUMBER; 2134 if (chunkEnd > historyGUIDs.size()) { 2135 chunkEnd = historyGUIDs.size(); 2136 } 2137 final List<String> chunkGUIDs = historyGUIDs.subList(chunkStart, chunkEnd); 2138 deleted += db.delete( 2139 table, 2140 DBUtils.computeSQLInClause(chunkGUIDs.size(), historyGUIDColumn), 2141 chunkGUIDs.toArray(new String[chunkGUIDs.size()]) 2142 ); 2143 } 2144 2145 return deleted; 2146 } 2147 deleteVisits(Uri uri, String selection, String[] selectionArgs)2148 private int deleteVisits(Uri uri, String selection, String[] selectionArgs) { 2149 debug("Deleting visits for URI: " + uri); 2150 2151 final SQLiteDatabase db = getWritableDatabase(uri); 2152 2153 beginWrite(db); 2154 return db.delete(TABLE_VISITS, selection, selectionArgs); 2155 } 2156 deleteBookmarks(Uri uri, String selection, String[] selectionArgs)2157 private int deleteBookmarks(Uri uri, String selection, String[] selectionArgs) { 2158 debug("Deleting bookmarks for URI: " + uri); 2159 2160 final SQLiteDatabase db = getWritableDatabase(uri); 2161 2162 if (isCallerSync(uri)) { 2163 beginWrite(db); 2164 return db.delete(TABLE_BOOKMARKS, selection, selectionArgs); 2165 } 2166 2167 debug("Marking bookmarks as deleted for URI: " + uri); 2168 2169 ContentValues values = new ContentValues(); 2170 values.put(Bookmarks.IS_DELETED, 1); 2171 values.put(Bookmarks.POSITION, 0); 2172 values.putNull(Bookmarks.PARENT); 2173 values.putNull(Bookmarks.URL); 2174 values.putNull(Bookmarks.TITLE); 2175 values.putNull(Bookmarks.DESCRIPTION); 2176 values.putNull(Bookmarks.KEYWORD); 2177 values.putNull(Bookmarks.TAGS); 2178 values.putNull(Bookmarks.FAVICON_ID); 2179 2180 // Doing this UPDATE (or the DELETE above) first ensures that the 2181 // first operation within this transaction is a write. 2182 // The cleanup call below will do a SELECT first, and thus would 2183 // require the transaction to be upgraded from a reader to a writer. 2184 final int updated = updateBookmarks(uri, values, selection, selectionArgs); 2185 try { 2186 cleanUpSomeDeletedRecords(uri, TABLE_BOOKMARKS); 2187 } catch (Exception e) { 2188 // We don't care. 2189 Log.e(LOGTAG, "Unable to clean up deleted bookmark records: ", e); 2190 } 2191 return updated; 2192 } 2193 deleteFavicons(Uri uri, String selection, String[] selectionArgs)2194 private int deleteFavicons(Uri uri, String selection, String[] selectionArgs) { 2195 debug("Deleting favicons for URI: " + uri); 2196 2197 final SQLiteDatabase db = getWritableDatabase(uri); 2198 2199 return db.delete(TABLE_FAVICONS, selection, selectionArgs); 2200 } 2201 deleteThumbnails(Uri uri, String selection, String[] selectionArgs)2202 private int deleteThumbnails(Uri uri, String selection, String[] selectionArgs) { 2203 debug("Deleting thumbnails for URI: " + uri); 2204 2205 final SQLiteDatabase db = getWritableDatabase(uri); 2206 2207 return db.delete(TABLE_THUMBNAILS, selection, selectionArgs); 2208 } 2209 deleteUnusedImages(Uri uri)2210 private int deleteUnusedImages(Uri uri) { 2211 debug("Deleting all unused favicons and thumbnails for URI: " + uri); 2212 2213 String faviconSelection = Favicons._ID + " NOT IN " 2214 + "(SELECT " + History.FAVICON_ID 2215 + " FROM " + TABLE_HISTORY 2216 + " WHERE " + History.IS_DELETED + " = 0" 2217 + " AND " + History.FAVICON_ID + " IS NOT NULL" 2218 + " UNION ALL SELECT " + Bookmarks.FAVICON_ID 2219 + " FROM " + TABLE_BOOKMARKS 2220 + " WHERE " + Bookmarks.IS_DELETED + " = 0" 2221 + " AND " + Bookmarks.FAVICON_ID + " IS NOT NULL)"; 2222 2223 String thumbnailSelection = Thumbnails.URL + " NOT IN " 2224 + "(SELECT " + History.URL 2225 + " FROM " + TABLE_HISTORY 2226 + " WHERE " + History.IS_DELETED + " = 0" 2227 + " AND " + History.URL + " IS NOT NULL" 2228 + " UNION ALL SELECT " + Bookmarks.URL 2229 + " FROM " + TABLE_BOOKMARKS 2230 + " WHERE " + Bookmarks.IS_DELETED + " = 0" 2231 + " AND " + Bookmarks.URL + " IS NOT NULL)"; 2232 2233 return deleteFavicons(uri, faviconSelection, null) + 2234 deleteThumbnails(uri, thumbnailSelection, null) + 2235 getURLMetadataTable().deleteUnused(getWritableDatabase(uri)); 2236 } 2237 2238 @Override applyBatch(ArrayList<ContentProviderOperation> operations)2239 public ContentProviderResult[] applyBatch (ArrayList<ContentProviderOperation> operations) 2240 throws OperationApplicationException { 2241 final int numOperations = operations.size(); 2242 final ContentProviderResult[] results = new ContentProviderResult[numOperations]; 2243 2244 if (numOperations < 1) { 2245 debug("applyBatch: no operations; returning immediately."); 2246 // The original Android implementation returns a zero-length 2247 // array in this case. We do the same. 2248 return results; 2249 } 2250 2251 boolean failures = false; 2252 2253 // We only have 1 database for all Uris that we can get. 2254 SQLiteDatabase db = getWritableDatabase(operations.get(0).getUri()); 2255 2256 // Note that the apply() call may cause us to generate 2257 // additional transactions for the individual operations. 2258 // But Android's wrapper for SQLite supports nested transactions, 2259 // so this will do the right thing. 2260 // 2261 // Note further that in some circumstances this can result in 2262 // exceptions: if this transaction is first involved in reading, 2263 // and then (naturally) tries to perform writes, SQLITE_BUSY can 2264 // be raised. See Bug 947939 and friends. 2265 beginBatch(db); 2266 2267 for (int i = 0; i < numOperations; i++) { 2268 try { 2269 final ContentProviderOperation operation = operations.get(i); 2270 results[i] = operation.apply(this, results, i); 2271 } catch (SQLException e) { 2272 Log.w(LOGTAG, "SQLite Exception during applyBatch.", e); 2273 // The Android API makes it implementation-defined whether 2274 // the failure of a single operation makes all others abort 2275 // or not. For our use cases, best-effort operation makes 2276 // more sense. Rolling back and forcing the caller to retry 2277 // after it figures out what went wrong isn't very convenient 2278 // anyway. 2279 // Signal failed operation back, so the caller knows what 2280 // went through and what didn't. 2281 results[i] = new ContentProviderResult(0); 2282 failures = true; 2283 // http://www.sqlite.org/lang_conflict.html 2284 // Note that we need a new transaction, subsequent operations 2285 // on this one will fail (we're in ABORT by default, which 2286 // isn't IGNORE). We still need to set it as successful to let 2287 // everything before the failed op go through. 2288 // We can't set conflict resolution on API level < 8, and even 2289 // above 8 it requires splitting the call per operation 2290 // (insert/update/delete). 2291 db.setTransactionSuccessful(); 2292 db.endTransaction(); 2293 db.beginTransaction(); 2294 } catch (OperationApplicationException e) { 2295 // Repeat of above. 2296 results[i] = new ContentProviderResult(0); 2297 failures = true; 2298 db.setTransactionSuccessful(); 2299 db.endTransaction(); 2300 db.beginTransaction(); 2301 } 2302 } 2303 2304 trace("Flushing DB applyBatch..."); 2305 markBatchSuccessful(db); 2306 endBatch(db); 2307 2308 if (failures) { 2309 throw new OperationApplicationException(); 2310 } 2311 2312 return results; 2313 } 2314 findTableFor(int id)2315 private static Table findTableFor(int id) { 2316 for (Table table : sTables) { 2317 for (Table.ContentProviderInfo type : table.getContentProviderInfo()) { 2318 if (type.id == id) { 2319 return table; 2320 } 2321 } 2322 } 2323 return null; 2324 } 2325 addTablesToMatcher(Table[] tables, final UriMatcher matcher)2326 private static void addTablesToMatcher(Table[] tables, final UriMatcher matcher) { 2327 } 2328 getContentItemType(final int match)2329 private static String getContentItemType(final int match) { 2330 for (Table table : sTables) { 2331 for (Table.ContentProviderInfo type : table.getContentProviderInfo()) { 2332 if (type.id == match) { 2333 return "vnd.android.cursor.item/" + type.name; 2334 } 2335 } 2336 } 2337 2338 return null; 2339 } 2340 } 2341