Bug 1301717 - Allow querying, inserting and cleaning up page metadata via BrowserProvider r=sebastian draft
authorGrisha Kruglov <gkruglov@mozilla.com>
Thu, 03 Nov 2016 12:04:31 -0700
changeset 434089 c31fa6da650a061f2c6c06642b1f6ec8712846cd
parent 434088 a4e3c46b73ca3482f63269f924d691b2c3653550
child 434090 ecc499269264ece6413ef8e05658cf2c9a1e4e5b
push id34718
push userbmo:gkruglov@mozilla.com
push dateFri, 04 Nov 2016 20:41:37 +0000
reviewerssebastian
bugs1301717
milestone52.0a1
Bug 1301717 - Allow querying, inserting and cleaning up page metadata via BrowserProvider r=sebastian MozReview-Commit-ID: 9SxriBMVgcv
mobile/android/base/java/org/mozilla/gecko/db/BrowserProvider.java
--- a/mobile/android/base/java/org/mozilla/gecko/db/BrowserProvider.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/BrowserProvider.java
@@ -23,16 +23,17 @@ import org.mozilla.gecko.db.BrowserContr
 import org.mozilla.gecko.db.BrowserContract.Highlights;
 import org.mozilla.gecko.db.BrowserContract.History;
 import org.mozilla.gecko.db.BrowserContract.Visits;
 import org.mozilla.gecko.db.BrowserContract.Schema;
 import org.mozilla.gecko.db.BrowserContract.Tabs;
 import org.mozilla.gecko.db.BrowserContract.Thumbnails;
 import org.mozilla.gecko.db.BrowserContract.TopSites;
 import org.mozilla.gecko.db.BrowserContract.UrlAnnotations;
+import org.mozilla.gecko.db.BrowserContract.PageMetadata;
 import org.mozilla.gecko.db.DBUtils.UpdateOperation;
 import org.mozilla.gecko.icons.IconsHelper;
 import org.mozilla.gecko.sync.Utils;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import android.content.BroadcastReceiver;
 import android.content.ContentProviderOperation;
 import android.content.ContentProviderResult;
@@ -82,16 +83,17 @@ public class BrowserProvider extends Sha
     static final String TABLE_BOOKMARKS = Bookmarks.TABLE_NAME;
     static final String TABLE_HISTORY = History.TABLE_NAME;
     static final String TABLE_VISITS = Visits.TABLE_NAME;
     static final String TABLE_FAVICONS = Favicons.TABLE_NAME;
     static final String TABLE_THUMBNAILS = Thumbnails.TABLE_NAME;
     static final String TABLE_TABS = Tabs.TABLE_NAME;
     static final String TABLE_URL_ANNOTATIONS = UrlAnnotations.TABLE_NAME;
     static final String TABLE_ACTIVITY_STREAM_BLOCKLIST = ActivityStreamBlocklist.TABLE_NAME;
+    static final String TABLE_PAGE_METADATA = PageMetadata.TABLE_NAME;
 
     static final String VIEW_COMBINED = Combined.VIEW_NAME;
     static final String VIEW_BOOKMARKS_WITH_FAVICONS = Bookmarks.VIEW_WITH_FAVICONS;
     static final String VIEW_BOOKMARKS_WITH_ANNOTATIONS = Bookmarks.VIEW_WITH_ANNOTATIONS;
     static final String VIEW_HISTORY_WITH_FAVICONS = History.VIEW_WITH_FAVICONS;
     static final String VIEW_COMBINED_WITH_FAVICONS = Combined.VIEW_WITH_FAVICONS;
 
     // Bookmark matches
@@ -133,16 +135,18 @@ public class BrowserProvider extends Sha
     static final int VISITS = 1100;
 
     static final int METADATA = 1200;
 
     static final int HIGHLIGHTS = 1300;
 
     static final int ACTIVITY_STREAM_BLOCKLIST = 1400;
 
+    static final int PAGE_METADATA = 1500;
+
     static final String DEFAULT_BOOKMARKS_SORT_ORDER = Bookmarks.TYPE
             + " ASC, " + Bookmarks.POSITION + " ASC, " + Bookmarks._ID
             + " ASC";
 
     static final String DEFAULT_HISTORY_SORT_ORDER = History.DATE_LAST_VISITED + " DESC";
     static final String DEFAULT_VISITS_SORT_ORDER = Visits.DATE_VISITED + " DESC";
 
     static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
@@ -150,16 +154,17 @@ public class BrowserProvider extends Sha
     static final Map<String, String> BOOKMARKS_PROJECTION_MAP;
     static final Map<String, String> HISTORY_PROJECTION_MAP;
     static final Map<String, String> COMBINED_PROJECTION_MAP;
     static final Map<String, String> SCHEMA_PROJECTION_MAP;
     static final Map<String, String> FAVICONS_PROJECTION_MAP;
     static final Map<String, String> THUMBNAILS_PROJECTION_MAP;
     static final Map<String, String> URL_ANNOTATIONS_PROJECTION_MAP;
     static final Map<String, String> VISIT_PROJECTION_MAP;
+    static final Map<String, String> PAGE_METADATA_PROJECTION_MAP;
     static final Table[] sTables;
 
     static {
         sTables = new Table[] {
             // See awful shortcut assumption hack in getURLMetadataTable.
             new URLMetadataTable()
         };
         // We will reuse this.
@@ -276,16 +281,26 @@ public class BrowserProvider extends Sha
         map.put(Combined.FAVICON_ID, Combined.FAVICON_ID);
         map.put(Combined.FAVICON_URL, Combined.FAVICON_URL);
         map.put(Combined.LOCAL_DATE_LAST_VISITED, Combined.LOCAL_DATE_LAST_VISITED);
         map.put(Combined.REMOTE_DATE_LAST_VISITED, Combined.REMOTE_DATE_LAST_VISITED);
         map.put(Combined.LOCAL_VISITS_COUNT, Combined.LOCAL_VISITS_COUNT);
         map.put(Combined.REMOTE_VISITS_COUNT, Combined.REMOTE_VISITS_COUNT);
         COMBINED_PROJECTION_MAP = Collections.unmodifiableMap(map);
 
+        map = new HashMap<>();
+        map.put(PageMetadata._ID, PageMetadata._ID);
+        map.put(PageMetadata.HISTORY_GUID, PageMetadata.HISTORY_GUID);
+        map.put(PageMetadata.DATE_CREATED, PageMetadata.DATE_CREATED);
+        map.put(PageMetadata.HAS_IMAGE, PageMetadata.HAS_IMAGE);
+        map.put(PageMetadata.JSON, PageMetadata.JSON);
+        PAGE_METADATA_PROJECTION_MAP = Collections.unmodifiableMap(map);
+
+        URI_MATCHER.addURI(BrowserContract.AUTHORITY, "page_metadata", PAGE_METADATA);
+
         // Schema
         URI_MATCHER.addURI(BrowserContract.AUTHORITY, "schema", SCHEMA);
 
         map = new HashMap<String, String>();
         map.put(Schema.VERSION, Schema.VERSION);
         SCHEMA_PROJECTION_MAP = Collections.unmodifiableMap(map);
 
 
@@ -553,20 +568,23 @@ public class BrowserProvider extends Sha
                 beginWrite(db);
                 /**
                  * Deletes from Sync are actual DELETE statements, which will cascade delete relevant visits.
                  * Fennec's deletes mark records as deleted and wipe out all information (except for GUID).
                  * Eventually, Fennec will purge history records that were marked as deleted for longer than some
                  * period of time (e.g. 20 days).
                  * See {@link SharedBrowserDatabaseProvider#cleanUpSomeDeletedRecords(Uri, String)}.
                  */
+                final ArrayList<String> historyGUIDs = getHistoryGUIDsFromSelection(db, uri, selection, selectionArgs);
+
                 if (!isCallerSync(uri)) {
-                    deleteVisitsForHistory(uri, selection, selectionArgs);
+                    deleteVisitsForHistory(db, historyGUIDs);
                 }
-                deleted = deleteHistory(uri, selection, selectionArgs);
+                deletePageMetadataForHistory(db, historyGUIDs);
+                deleted = deleteHistory(db, uri, selection, selectionArgs);
                 deleteUnusedImages(uri);
                 break;
             }
 
             case VISITS:
                 trace("Deleting visits: " + uri);
                 beginWrite(db);
                 deleted = deleteVisits(uri, selection, selectionArgs);
@@ -616,16 +634,21 @@ public class BrowserProvider extends Sha
                 break;
             }
 
             case URL_ANNOTATIONS:
                 trace("Delete on URL_ANNOTATIONS: " + uri);
                 deleteUrlAnnotation(uri, selection, selectionArgs);
                 break;
 
+            case PAGE_METADATA:
+                trace("Delete on PAGE_METADATA: " + uri);
+                deleted = deletePageMetadata(uri, selection, selectionArgs);
+                break;
+
             default: {
                 Table table = findTableFor(match);
                 if (table == null) {
                     throw new UnsupportedOperationException("Unknown delete URI " + uri);
                 }
                 trace("Deleting TABLE: " + uri);
                 beginWrite(db);
                 deleted = table.delete(db, uri, match, selection, selectionArgs);
@@ -682,16 +705,22 @@ public class BrowserProvider extends Sha
             }
 
             case ACTIVITY_STREAM_BLOCKLIST: {
                 trace("Insert on ACTIVITY_STREAM_BLOCKLIST: " + uri);
                 id = insertActivityStreamBlocklistSite(uri, values);
                 break;
             }
 
+            case PAGE_METADATA: {
+                trace("Insert on PAGE_METADATA: " + uri);
+                id = insertPageMetadata(uri, values);
+                break;
+            }
+
             default: {
                 Table table = findTableFor(match);
                 if (table == null) {
                     throw new UnsupportedOperationException("Unknown insert URI " + uri);
                 }
 
                 trace("Insert on TABLE: " + uri);
                 final SQLiteDatabase db = getWritableDatabase(uri);
@@ -1412,16 +1441,24 @@ public class BrowserProvider extends Sha
             }
 
             case HIGHLIGHTS: {
                 debug("Highlights query: " + uri);
 
                 return getHighlights(db, limit);
             }
 
+            case PAGE_METADATA: {
+                debug("PageMetadata query: " + uri);
+
+                qb.setProjectionMap(PAGE_METADATA_PROJECTION_MAP);
+                qb.setTables(TABLE_PAGE_METADATA);
+                break;
+            }
+
             default: {
                 Table table = findTableFor(match);
                 if (table == null) {
                     throw new UnsupportedOperationException("Unknown query URI " + uri);
                 }
                 trace("Update TABLE: " + uri);
                 return table.query(db, uri, match, projection, selection, selectionArgs, sortOrder, groupBy, limit);
             }
@@ -1927,32 +1964,54 @@ public class BrowserProvider extends Sha
 
         final SQLiteDatabase db = getWritableDatabase(uri);
         values.put(ActivityStreamBlocklist.CREATED, System.currentTimeMillis());
 
         beginWrite(db);
         return db.insertOrThrow(TABLE_ACTIVITY_STREAM_BLOCKLIST, null, values);
     }
 
+    private long insertPageMetadata(final Uri uri, final ContentValues values) {
+        final SQLiteDatabase db = getWritableDatabase(uri);
+
+        if (!values.containsKey(PageMetadata.DATE_CREATED)) {
+            values.put(PageMetadata.DATE_CREATED, System.currentTimeMillis());
+        }
+
+        beginWrite(db);
+
+        // Perform INSERT OR REPLACE, there might be page metadata present and we want to replace it.
+        // Depends on a conflict arising from unique foreign key (history_guid) constraint violation.
+        return db.insertWithOnConflict(
+                TABLE_PAGE_METADATA, null, values, SQLiteDatabase.CONFLICT_REPLACE);
+    }
+
     private long insertUrlAnnotation(final Uri uri, final ContentValues values) {
         final String url = values.getAsString(UrlAnnotations.URL);
         trace("Inserting url annotations for URL: " + url);
 
         final SQLiteDatabase db = getWritableDatabase(uri);
         beginWrite(db);
         return db.insertOrThrow(TABLE_URL_ANNOTATIONS, null, values);
     }
 
     private void deleteUrlAnnotation(final Uri uri, final String selection, final String[] selectionArgs) {
         trace("Deleting url annotation for URI: " + uri);
 
         final SQLiteDatabase db = getWritableDatabase(uri);
         db.delete(TABLE_URL_ANNOTATIONS, selection, selectionArgs);
     }
 
+    private int deletePageMetadata(final Uri uri, final String selection, final String[] selectionArgs) {
+        trace("Deleting page metadata for URI: " + uri);
+
+        final SQLiteDatabase db = getWritableDatabase(uri);
+        return db.delete(TABLE_PAGE_METADATA, selection, selectionArgs);
+    }
+
     private void updateUrlAnnotation(final Uri uri, final ContentValues values, final String selection, final String[] selectionArgs) {
         trace("Updating url annotation for URI: " + uri);
 
         final SQLiteDatabase db = getWritableDatabase(uri);
         db.update(TABLE_URL_ANNOTATIONS, values, selection, selectionArgs);
     }
 
     private int updateOrInsertThumbnail(Uri uri, ContentValues values, String selection,
@@ -1988,21 +2047,19 @@ public class BrowserProvider extends Sha
     }
 
     /**
      * This method does not create a new transaction. Its first operation is
      * guaranteed to be a write, which in the case of a new enclosing
      * transaction will guarantee that a read does not need to be upgraded to
      * a write.
      */
-    private int deleteHistory(Uri uri, String selection, String[] selectionArgs) {
+    private int deleteHistory(SQLiteDatabase db, Uri uri, String selection, String[] selectionArgs) {
         debug("Deleting history entry for URI: " + uri);
 
-        final SQLiteDatabase db = getWritableDatabase(uri);
-
         if (isCallerSync(uri)) {
             return db.delete(TABLE_HISTORY, selection, selectionArgs);
         }
 
         debug("Marking history entry as deleted for URI: " + uri);
 
         ContentValues values = new ContentValues();
         values.put(History.IS_DELETED, 1);
@@ -2026,55 +2083,66 @@ public class BrowserProvider extends Sha
             cleanUpSomeDeletedRecords(uri, TABLE_HISTORY);
         } catch (Exception e) {
             // We don't care.
             Log.e(LOGTAG, "Unable to clean up deleted history records: ", e);
         }
         return updated;
     }
 
-    private int deleteVisitsForHistory(Uri uri, String selection, String[] selectionArgs) {
-        final SQLiteDatabase db = getWritableDatabase(uri);
+    private ArrayList<String> getHistoryGUIDsFromSelection(SQLiteDatabase db, Uri uri, String selection, String[] selectionArgs) {
+        final ArrayList<String> historyGUIDs = new ArrayList<>();
 
         final Cursor cursor = db.query(
                 History.TABLE_NAME, new String[] {History.GUID}, selection, selectionArgs,
                 null, null, null);
         if (cursor == null) {
             Log.e(LOGTAG, "Null cursor while trying to delete visits for history URI: " + uri);
-            return 0;
+            return historyGUIDs;
         }
 
-        ArrayList<String> historyGUIDs = new ArrayList<>();
         try {
             if (!cursor.moveToFirst()) {
                 trace("No history items for which to remove visits matched for URI: " + uri);
-                return 0;
+                return historyGUIDs;
             }
             final int historyColumn = cursor.getColumnIndexOrThrow(History.GUID);
             while (!cursor.isAfterLast()) {
                 historyGUIDs.add(cursor.getString(historyColumn));
                 cursor.moveToNext();
             }
         } finally {
             cursor.close();
         }
 
+        return historyGUIDs;
+    }
+
+    private int deletePageMetadataForHistory(SQLiteDatabase db, ArrayList<String> historyGUIDs) {
+        return bulkDeleteByHistoryGUID(db, historyGUIDs, PageMetadata.TABLE_NAME, PageMetadata.HISTORY_GUID);
+    }
+
+    private int deleteVisitsForHistory(SQLiteDatabase db, ArrayList<String> historyGUIDs) {
+        return bulkDeleteByHistoryGUID(db, historyGUIDs, Visits.TABLE_NAME, Visits.HISTORY_GUID);
+    }
+
+    private int bulkDeleteByHistoryGUID(SQLiteDatabase db, ArrayList<String> historyGUIDs, String table, String historyGUIDColumn) {
         // Due to SQLite's maximum variable limitation, we need to chunk our delete statements.
         // For example, if there were 1200 GUIDs, this will perform 2 delete statements.
         int deleted = 0;
         for (int chunk = 0; chunk <= historyGUIDs.size() / DBUtils.SQLITE_MAX_VARIABLE_NUMBER; chunk++) {
             final int chunkStart = chunk * DBUtils.SQLITE_MAX_VARIABLE_NUMBER;
             int chunkEnd = (chunk + 1) * DBUtils.SQLITE_MAX_VARIABLE_NUMBER;
             if (chunkEnd > historyGUIDs.size()) {
                 chunkEnd = historyGUIDs.size();
             }
             final List<String> chunkGUIDs = historyGUIDs.subList(chunkStart, chunkEnd);
             deleted += db.delete(
-                    Visits.TABLE_NAME,
-                    DBUtils.computeSQLInClause(chunkGUIDs.size(), Visits.HISTORY_GUID),
+                    table,
+                    DBUtils.computeSQLInClause(chunkGUIDs.size(), historyGUIDColumn),
                     chunkGUIDs.toArray(new String[chunkGUIDs.size()])
             );
         }
 
         return deleted;
     }
 
     private int deleteVisits(Uri uri, String selection, String[] selectionArgs) {