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