From 29a79ad111a32986109ba4e822076f4ad874655c Mon Sep 17 00:00:00 2001 From: Grisha Kruglov Date: Tue, 29 Nov 2016 13:42:53 -0800 Subject: [PATCH] Bug 1291821 - Move bulk insert logic for new history to BrowserProvider r=rnewman This commit does two things: 1) It simplifies history insertion logic, which wrongly assumed that history which was being inserted might be not new. As such, it was necessary to check for collisions of visit inserts, record number of visits actually inserted, and update remote visit counts correspondingly in a separate step, making history insert a three step operation (insert history record, insert its visits, update history record with a count). However, bulkInsert runs only for records which were determined to be entirely new, so it's possible to drop the third step. 2) Makes all of the insertions (history records and their visits) run in one transaction. Prepared statements for both history and visit inserts are used are used as a performance optimization measure. MozReview-Commit-ID: 48T4G5IsQNS --HG-- extra : rebase_source : 280d468ef9b57163a178e42707aee610977625c4 --- .../org/mozilla/gecko/db/BrowserContract.java | 5 + .../org/mozilla/gecko/db/BrowserProvider.java | 230 ++++++++++++++++++ .../AndroidBrowserHistoryDataAccessor.java | 82 +++---- ...ndroidBrowserHistoryRepositorySession.java | 9 +- .../db/DelegatingTestContentProvider.java | 8 + .../gecko/db/BrowserProviderHistoryTest.java | 93 +++++++ .../BrowserProviderHistoryVisitsTestBase.java | 14 +- 7 files changed, 378 insertions(+), 63 deletions(-) diff --git a/mobile/android/base/java/org/mozilla/gecko/db/BrowserContract.java b/mobile/android/base/java/org/mozilla/gecko/db/BrowserContract.java index 62883ec604b4..05601208a852 100644 --- a/mobile/android/base/java/org/mozilla/gecko/db/BrowserContract.java +++ b/mobile/android/base/java/org/mozilla/gecko/db/BrowserContract.java @@ -58,6 +58,11 @@ public class BrowserContract { public static final String PARAM_DATASET_ID = "dataset_id"; public static final String PARAM_GROUP_BY = "group_by"; + public static final String METHOD_INSERT_HISTORY_WITH_VISITS_FROM_SYNC = "insertHistoryWithVisitsSync"; + public static final String METHOD_RESULT = "methodResult"; + public static final String METHOD_PARAM_OBJECT = "object"; + public static final String METHOD_PARAM_DATA = "data"; + static public enum ExpirePriority { NORMAL, AGGRESSIVE diff --git a/mobile/android/base/java/org/mozilla/gecko/db/BrowserProvider.java b/mobile/android/base/java/org/mozilla/gecko/db/BrowserProvider.java index 79dba66b81ab..a15fc145f7f7 100644 --- a/mobile/android/base/java/org/mozilla/gecko/db/BrowserProvider.java +++ b/mobile/android/base/java/org/mozilla/gecko/db/BrowserProvider.java @@ -32,6 +32,7 @@ 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.sync.repositories.android.BrowserContractHelpers; import org.mozilla.gecko.util.ThreadUtils; import android.content.BroadcastReceiver; @@ -49,10 +50,15 @@ import android.database.DatabaseUtils; import android.database.MatrixCursor; import android.database.MergeCursor; import android.database.SQLException; +import android.database.sqlite.SQLiteConstraintException; import android.database.sqlite.SQLiteCursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteQueryBuilder; +import android.database.sqlite.SQLiteStatement; import android.net.Uri; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v4.content.LocalBroadcastManager; import android.text.TextUtils; import android.util.Log; @@ -2207,6 +2213,230 @@ public class BrowserProvider extends SharedBrowserDatabaseProvider { getURLImageDataTable().deleteUnused(getWritableDatabase(uri)); } + @Nullable + @Override + public Bundle call(@NonNull String method, String uriArg, Bundle extras) { + if (uriArg == null) { + throw new IllegalArgumentException("Missing required Uri argument."); + } + final Bundle result = new Bundle(); + switch (method) { + case BrowserContract.METHOD_INSERT_HISTORY_WITH_VISITS_FROM_SYNC: + try { + final Uri uri = Uri.parse(uriArg); + final SQLiteDatabase db = getWritableDatabase(uri); + bulkInsertHistoryWithVisits(db, extras); + result.putSerializable(BrowserContract.METHOD_RESULT, null); + + // If anything went wrong during insertion, we know that changes were rolled back. + // Inform our caller that we have failed. + } catch (Exception e) { + Log.e(LOGTAG, "Unexpected error while bulk inserting history", e); + result.putSerializable(BrowserContract.METHOD_RESULT, e); + } + break; + default: + throw new IllegalArgumentException("Unknown method call: " + method); + } + + return result; + } + + private void bulkInsertHistoryWithVisits(final SQLiteDatabase db, @NonNull Bundle dataBundle) { + // NB: dataBundle structure: + // Key METHOD_PARAM_DATA=[Bundle,...] + // Each Bundle has keys METHOD_PARAM_OBJECT=ContentValues{HistoryRecord}, VISITS=ContentValues[]{visits} + final Bundle[] recordBundles = (Bundle[]) dataBundle.getSerializable(BrowserContract.METHOD_PARAM_DATA); + + if (recordBundles == null) { + throw new IllegalArgumentException("Received null recordBundle while bulk inserting history."); + } + + if (recordBundles.length == 0) { + return; + } + + final ContentValues[][] visitsValueSet = new ContentValues[recordBundles.length][]; + final ContentValues[] historyValueSet = new ContentValues[recordBundles.length]; + for (int i = 0; i < recordBundles.length; i++) { + historyValueSet[i] = recordBundles[i].getParcelable(BrowserContract.METHOD_PARAM_OBJECT); + visitsValueSet[i] = (ContentValues[]) recordBundles[i].getSerializable(History.VISITS); + } + + // Wrap the whole operation in a transaction. + beginBatch(db); + + final int historyInserted; + try { + // First, insert history records. + historyInserted = bulkInsertHistory(db, historyValueSet); + if (historyInserted != recordBundles.length) { + Log.w(LOGTAG, "Expected to insert " + recordBundles.length + " history records, " + + "but actually inserted " + historyInserted); + } + + // Second, insert visit records. + bulkInsertVisits(db, visitsValueSet); + + // Finally, commit all of the insertions we just made. + markBatchSuccessful(db); + + // We're done with our database operations. + } finally { + endBatch(db); + } + + // Notify listeners that we've just inserted new history records. + if (historyInserted > 0) { + getContext().getContentResolver().notifyChange( + BrowserContractHelpers.HISTORY_CONTENT_URI, null, + // Do not sync these changes. + false + ); + } + } + + private int bulkInsertHistory(final SQLiteDatabase db, ContentValues[] values) { + int inserted = 0; + final String fullInsertSqlStatement = "INSERT INTO " + History.TABLE_NAME + " (" + + History.GUID + "," + + History.TITLE + "," + + History.URL + "," + + History.DATE_LAST_VISITED + "," + + History.REMOTE_DATE_LAST_VISITED + "," + + History.VISITS + "," + + History.REMOTE_VISITS + ") VALUES (?, ?, ?, ?, ?, ?, ?)"; + final String shortInsertSqlStatement = "INSERT INTO " + History.TABLE_NAME + " (" + + History.GUID + "," + + History.TITLE + "," + + History.URL + ") VALUES (?, ?, ?)"; + final SQLiteStatement compiledFullStatement = db.compileStatement(fullInsertSqlStatement); + final SQLiteStatement compiledShortStatement = db.compileStatement(shortInsertSqlStatement); + SQLiteStatement statementToExec; + + beginWrite(db); + try { + for (ContentValues cv : values) { + final String guid = cv.getAsString(History.GUID); + final String title = cv.getAsString(History.TITLE); + final String url = cv.getAsString(History.URL); + final Long dateLastVisited = cv.getAsLong(History.DATE_LAST_VISITED); + final Long remoteDateLastVisited = cv.getAsLong(History.REMOTE_DATE_LAST_VISITED); + final Integer visits = cv.getAsInteger(History.VISITS); + + // If dateLastVisited is null, so will be remoteDateLastVisited and visits. + // We will use the short compiled statement in this case. + // See implementation in AndroidBrowserHistoryDataAccessor@getContentValues. + if (dateLastVisited == null) { + statementToExec = compiledShortStatement; + } else { + statementToExec = compiledFullStatement; + } + + statementToExec.clearBindings(); + statementToExec.bindString(1, guid); + // Title is allowed to be null. + if (title != null) { + statementToExec.bindString(2, title); + } else { + statementToExec.bindNull(2); + } + statementToExec.bindString(3, url); + if (dateLastVisited != null) { + statementToExec.bindLong(4, dateLastVisited); + statementToExec.bindLong(5, remoteDateLastVisited); + + // NB: + // Both of these count values might be slightly off unless we recalculate them + // from data in the visits table at some point. + // See note about visit insertion failures below in the bulkInsertVisits method. + + // Visit count + statementToExec.bindLong(6, visits); + // Remote visit count. + statementToExec.bindLong(7, visits); + } + + try { + if (statementToExec.executeInsert() != -1) { + inserted += 1; + } + + // NB: Constraint violation might occur if we're trying to insert a duplicate GUID. + // This should not happen but it does in practice, possibly due to reconciliation bugs. + // For now we catch and log the error without failing the whole bulk insert. + } catch (SQLiteConstraintException e) { + Log.w(LOGTAG, "Unexpected constraint violation while inserting history with GUID " + guid, e); + } + } + markWriteSuccessful(db); + } finally { + endWrite(db); + } + + if (inserted != values.length) { + Log.w(LOGTAG, "Failed to insert some of the history. " + + "Expected: " + values.length + ", actual: " + inserted); + } + + return inserted; + } + + private int bulkInsertVisits(SQLiteDatabase db, ContentValues[][] valueSets) { + final String insertSqlStatement = "INSERT INTO " + Visits.TABLE_NAME + " (" + + Visits.DATE_VISITED + "," + + Visits.VISIT_TYPE + "," + + Visits.HISTORY_GUID + "," + + Visits.IS_LOCAL + ") VALUES (?, ?, ?, ?)"; + final SQLiteStatement compiledInsertStatement = db.compileStatement(insertSqlStatement); + + int totalInserted = 0; + beginWrite(db); + try { + for (ContentValues[] valueSet : valueSets) { + int inserted = 0; + for (ContentValues values : valueSet) { + final long date = values.getAsLong(Visits.DATE_VISITED); + final long visitType = values.getAsLong(Visits.VISIT_TYPE); + final String guid = values.getAsString(Visits.HISTORY_GUID); + final Integer isLocal = values.getAsInteger(Visits.IS_LOCAL); + + // Bind parameters use a 1-based index. + compiledInsertStatement.clearBindings(); + compiledInsertStatement.bindLong(1, date); + compiledInsertStatement.bindLong(2, visitType); + compiledInsertStatement.bindString(3, guid); + compiledInsertStatement.bindLong(4, isLocal); + + try { + if (compiledInsertStatement.executeInsert() != -1) { + inserted++; + } + + // NB: + // Constraint exception will be thrown if we try to insert a visit violating + // unique(guid, date) constraint. We don't expect to do that, but our incoming + // data might not be clean - either due to duplicate entries in the sync data, + // or, less likely, due to record reconciliation bugs at the RepositorySession + // level. + } catch (SQLiteConstraintException e) { + Log.w(LOGTAG, "Unexpected constraint exception while inserting a visit", e); + } + } + if (inserted != valueSet.length) { + Log.w(LOGTAG, "Failed to insert some of the visits. " + + "Expected: " + valueSet.length + ", actual: " + inserted); + } + totalInserted += inserted; + } + markWriteSuccessful(db); + } finally { + endWrite(db); + } + + return totalInserted; + } + @Override public ContentProviderResult[] applyBatch (ArrayList operations) throws OperationApplicationException { diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserHistoryDataAccessor.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserHistoryDataAccessor.java index c09d64708a84..a0265dcc146f 100644 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserHistoryDataAccessor.java +++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserHistoryDataAccessor.java @@ -17,6 +17,7 @@ import org.mozilla.gecko.sync.repositories.domain.Record; import android.content.ContentValues; import android.content.Context; import android.net.Uri; +import android.os.Bundle; public class AndroidBrowserHistoryDataAccessor extends AndroidBrowserRepositoryDataAccessor { @@ -45,7 +46,7 @@ public class AndroidBrowserHistoryDataAccessor extends // The rest of Sync works in microseconds. This is the conversion point for records coming form Sync. cv.put(BrowserContract.History.DATE_LAST_VISITED, mostRecent / 1000); cv.put(BrowserContract.History.REMOTE_DATE_LAST_VISITED, mostRecent / 1000); - cv.put(BrowserContract.History.VISITS, Long.toString(visits.size())); + cv.put(BrowserContract.History.VISITS, visits.size()); } return cv; } @@ -109,63 +110,38 @@ public class AndroidBrowserHistoryDataAccessor extends * the number of records actually inserted. * @throws NullCursorException */ - public int bulkInsert(ArrayList records) throws NullCursorException { - if (records.isEmpty()) { - Logger.debug(LOG_TAG, "No records to insert, returning."); - } - - int size = records.size(); - ContentValues[] cvs = new ContentValues[size]; - int index = 0; - for (Record record : records) { + public boolean bulkInsert(ArrayList records) throws NullCursorException { + final Bundle[] historyBundles = new Bundle[records.size()]; + int i = 0; + for (HistoryRecord record : records) { if (record.guid == null) { - throw new IllegalArgumentException("Record with null GUID passed in to bulkInsert."); + throw new IllegalArgumentException("Record with null GUID passed into bulkInsert."); } - cvs[index] = getContentValues(record); - index += 1; + final Bundle historyBundle = new Bundle(); + historyBundle.putParcelable(BrowserContract.METHOD_PARAM_OBJECT, getContentValues(record)); + historyBundle.putSerializable( + BrowserContract.History.VISITS, + VisitsHelper.getVisitsContentValues(record.guid, record.visits) + ); + historyBundles[i] = historyBundle; + i++; } - // First update the history records. - int inserted = context.getContentResolver().bulkInsert(getUri(), cvs); - if (inserted == size) { - Logger.debug(LOG_TAG, "Inserted " + inserted + " records, as expected."); - } else { - Logger.debug(LOG_TAG, "Inserted " + - inserted + " records but expected " + - size + " records; continuing to update visits."); + final Bundle data = new Bundle(); + data.putSerializable(BrowserContract.METHOD_PARAM_DATA, historyBundles); + + // Let our ContentProvider handle insertion of everything. + final Bundle result = context.getContentResolver().call( + getUri(), + BrowserContract.METHOD_INSERT_HISTORY_WITH_VISITS_FROM_SYNC, + getUri().toString(), + data + ); + if (result == null) { + throw new IllegalStateException("Unexpected null result while bulk inserting history"); } - - final ContentValues remoteVisitAggregateValues = new ContentValues(); - final Uri historyIncrementRemoteAggregateUri = getUri().buildUpon() - .appendQueryParameter(BrowserContract.PARAM_INCREMENT_REMOTE_AGGREGATES, "true") - .build(); - for (Record record : records) { - HistoryRecord rec = (HistoryRecord) record; - if (rec.visits != null && rec.visits.size() != 0) { - int remoteVisitsInserted = context.getContentResolver().bulkInsert( - BrowserContract.Visits.CONTENT_URI, - VisitsHelper.getVisitsContentValues(rec.guid, rec.visits) - ); - - // If we just inserted any visits, update remote visit aggregate values. - // While inserting visits, we might not insert all of rec.visits - if we already have a local - // visit record with matching (guid,date), we will skip that visit. - // Remote visits aggregate value will be incremented by number of visits inserted. - // Note that we don't need to set REMOTE_DATE_LAST_VISITED, because it already gets set above. - if (remoteVisitsInserted > 0) { - // Note that REMOTE_VISITS must be set before calling cr.update(...) with a URI - // that has PARAM_INCREMENT_REMOTE_AGGREGATES=true. - remoteVisitAggregateValues.put(BrowserContract.History.REMOTE_VISITS, remoteVisitsInserted); - context.getContentResolver().update( - historyIncrementRemoteAggregateUri, - remoteVisitAggregateValues, - BrowserContract.History.GUID + " = ?", new String[] {rec.guid} - ); - } - } - } - - return inserted; + final Exception thrownException = (Exception) result.getSerializable(BrowserContract.METHOD_RESULT); + return thrownException == null; } /** diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserHistoryRepositorySession.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserHistoryRepositorySession.java index 4dc7c788c1be..16bfdce057a2 100644 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserHistoryRepositorySession.java +++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserHistoryRepositorySession.java @@ -28,7 +28,7 @@ public class AndroidBrowserHistoryRepositorySession extends AndroidBrowserReposi /** * The number of records to queue for insertion before writing to databases. */ - public static final int INSERT_RECORD_THRESHOLD = 50; + public static final int INSERT_RECORD_THRESHOLD = 5000; public static final int RECENT_VISITS_LIMIT = 20; public AndroidBrowserHistoryRepositorySession(Repository repository, Context context) { @@ -162,11 +162,8 @@ public class AndroidBrowserHistoryRepositorySession extends AndroidBrowserReposi final ArrayList outgoing = recordsBuffer; recordsBuffer = new ArrayList(); Logger.debug(LOG_TAG, "Flushing " + outgoing.size() + " records to database."); - // TODO: move bulkInsert to AndroidBrowserDataAccessor? - int inserted = ((AndroidBrowserHistoryDataAccessor) dbHelper).bulkInsert(outgoing); - if (inserted != outgoing.size()) { - // Something failed; most pessimistic action is to declare that all insertions failed. - // TODO: perform the bulkInsert in a transaction and rollback unless all insertions succeed? + boolean transactionSuccess = ((AndroidBrowserHistoryDataAccessor) dbHelper).bulkInsert(outgoing); + if (!transactionSuccess) { for (HistoryRecord failed : outgoing) { storeDelegate.onRecordStoreFailed(new RuntimeException("Failed to insert history item with guid " + failed.guid + "."), failed.guid); } diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/db/DelegatingTestContentProvider.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/db/DelegatingTestContentProvider.java index 91a36f7d14c8..b1d52f49dbd2 100644 --- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/db/DelegatingTestContentProvider.java +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/db/DelegatingTestContentProvider.java @@ -11,6 +11,8 @@ import android.content.ContentValues; import android.content.OperationApplicationException; import android.database.Cursor; import android.net.Uri; +import android.os.Bundle; +import android.support.annotation.Nullable; import org.mozilla.gecko.db.BrowserContract; @@ -80,6 +82,12 @@ public class DelegatingTestContentProvider extends ContentProvider { return mTargetProvider.bulkInsert(appendTestParam(uri), values); } + @Nullable + @Override + public Bundle call(String method, String arg, Bundle extras) { + return mTargetProvider.call(method, arg, extras); + } + public ContentProvider getTargetProvider() { return mTargetProvider; } diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserProviderHistoryTest.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserProviderHistoryTest.java index 850841432d13..7c5ceaf51823 100644 --- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserProviderHistoryTest.java +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserProviderHistoryTest.java @@ -8,6 +8,7 @@ import android.content.ContentProviderClient; import android.content.ContentValues; import android.database.Cursor; import android.net.Uri; +import android.os.Bundle; import android.os.RemoteException; import org.junit.After; @@ -259,6 +260,98 @@ public class BrowserProviderHistoryTest extends BrowserProviderHistoryVisitsTest } } + @Test + public void testBulkHistoryInsert() throws Exception { + // Test basic error conditions. + String historyTestUriArg = historyTestUri.toString(); + Bundle result = historyClient.call(BrowserContract.METHOD_INSERT_HISTORY_WITH_VISITS_FROM_SYNC, historyTestUriArg, new Bundle()); + assertNotNull(result); + assertNotNull(result.getSerializable(BrowserContract.METHOD_RESULT)); + + final Bundle data = new Bundle(); + + Bundle[] recordBundles = new Bundle[0]; + data.putSerializable(BrowserContract.METHOD_PARAM_DATA, recordBundles); + result = historyClient.call(BrowserContract.METHOD_INSERT_HISTORY_WITH_VISITS_FROM_SYNC, historyTestUriArg, data); + assertNotNull(result); + assertNull(result.getSerializable(BrowserContract.METHOD_RESULT)); + assertRowCount(historyClient, historyTestUri, 0); + + // Test insert three history records with 10 visits each. + recordBundles = new Bundle[3]; + for (int i = 0; i < 3; i++) { + final Bundle bundle = new Bundle(); + bundle.putParcelable(BrowserContract.METHOD_PARAM_OBJECT, buildHistoryCV("guid" + i, "Test", "https://www.mozilla.org/" + i, 10L, 10L, 10)); + bundle.putSerializable(BrowserContract.History.VISITS, buildHistoryVisitsCVs(10, "guid" + i, 1L, 3, false)); + recordBundles[i] = bundle; + } + data.putSerializable(BrowserContract.METHOD_PARAM_DATA, recordBundles); + + result = historyClient.call(BrowserContract.METHOD_INSERT_HISTORY_WITH_VISITS_FROM_SYNC, historyTestUriArg, data); + assertNotNull(result); + assertNull(result.getSerializable(BrowserContract.METHOD_RESULT)); + assertRowCount(historyClient, historyTestUri, 3); + assertRowCount(visitsClient, visitsTestUri, 30); + + // Test insert mixed data. + recordBundles = new Bundle[3]; + final Bundle bundle = new Bundle(); + bundle.putParcelable(BrowserContract.METHOD_PARAM_OBJECT, buildHistoryCV("guid4", null, "https://www.mozilla.org/1", null, null, null)); + bundle.putSerializable(BrowserContract.History.VISITS, new ContentValues[0]); + recordBundles[0] = bundle; + final Bundle bundle2 = new Bundle(); + bundle2.putParcelable(BrowserContract.METHOD_PARAM_OBJECT, buildHistoryCV("guid5", "Test", "https://www.mozilla.org/2", null, null, null)); + bundle2.putSerializable(BrowserContract.History.VISITS, new ContentValues[0]); + recordBundles[1] = bundle2; + final Bundle bundle3 = new Bundle(); + bundle3.putParcelable(BrowserContract.METHOD_PARAM_OBJECT, buildHistoryCV("guid6", "Test", "https://www.mozilla.org/3", 5L, 5L, 5)); + bundle3.putSerializable(BrowserContract.History.VISITS, buildHistoryVisitsCVs(5, "guid6", 1L, 2, false)); + recordBundles[2] = bundle3; + data.putSerializable(BrowserContract.METHOD_PARAM_DATA, recordBundles); + + result = historyClient.call(BrowserContract.METHOD_INSERT_HISTORY_WITH_VISITS_FROM_SYNC, historyTestUriArg, data); + assertNotNull(result); + assertNull(result.getSerializable(BrowserContract.METHOD_RESULT)); + assertRowCount(historyClient, historyTestUri, 6); + assertRowCount(visitsClient, visitsTestUri, 35); + + assertHistoryAggregates(BrowserContract.History.URL + " = ?", new String[] {"https://www.mozilla.org/3"}, + 5, 0, 0, 5, 5); + } + + private ContentValues[] buildHistoryVisitsCVs(int numberOfVisits, String guid, long baseDate, int visitType, boolean isLocal) { + final ContentValues[] visits = new ContentValues[numberOfVisits]; + for (int i = 0; i < numberOfVisits; i++) { + final ContentValues visit = new ContentValues(); + visit.put(BrowserContract.Visits.HISTORY_GUID, guid); + visit.put(BrowserContract.Visits.DATE_VISITED, baseDate + i); + visit.put(BrowserContract.Visits.VISIT_TYPE, visitType); + visit.put(BrowserContract.Visits.IS_LOCAL, isLocal ? BrowserContract.Visits.VISIT_IS_LOCAL : BrowserContract.Visits.VISIT_IS_REMOTE); + visits[i] = visit; + } + return visits; + } + + private ContentValues buildHistoryCV(String guid, String title, String url, Long lastVisited, Long remoteLastVisited, Integer visits) { + ContentValues cv = new ContentValues(); + cv.put(BrowserContract.History.GUID, guid); + if (title != null) { + cv.put(BrowserContract.History.TITLE, title); + } + cv.put(BrowserContract.History.URL, url); + if (lastVisited != null) { + cv.put(BrowserContract.History.DATE_LAST_VISITED, lastVisited); + } + if (remoteLastVisited != null) { + cv.put(BrowserContract.History.REMOTE_DATE_LAST_VISITED, remoteLastVisited); + } + if (visits != null) { + cv.put(BrowserContract.History.VISITS, visits); + cv.put(BrowserContract.History.REMOTE_VISITS, visits); + } + return cv; + } + private void assertHistoryAggregates(String selection, String[] selectionArg, int visits, int localVisits, long localLastVisited, int remoteVisits, long remoteLastVisited) throws Exception { final Cursor c = historyClient.query(historyTestUri, new String[] { BrowserContract.History.VISITS, diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserProviderHistoryVisitsTestBase.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserProviderHistoryVisitsTestBase.java index b8ee0bb362e3..808a71aaefab 100644 --- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserProviderHistoryVisitsTestBase.java +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserProviderHistoryVisitsTestBase.java @@ -22,8 +22,7 @@ public class BrowserProviderHistoryVisitsTestBase { /* package-private */ ContentProviderClient visitsClient; /* package-private */ Uri historyTestUri; /* package-private */ Uri visitsTestUri; - - private BrowserProvider provider; + /* package-private */ BrowserProvider provider; @Before public void setUp() throws Exception { @@ -51,14 +50,18 @@ public class BrowserProviderHistoryVisitsTestBase { } /* package-private */ Uri insertHistoryItem(String url, String guid) throws RemoteException { - return insertHistoryItem(url, guid, System.currentTimeMillis(), null, null); + return insertHistoryItem(url, guid, System.currentTimeMillis(), null, null, null); } /* package-private */ Uri insertHistoryItem(String url, String guid, Long lastVisited, Integer visitCount) throws RemoteException { - return insertHistoryItem(url, guid, lastVisited, visitCount, null); + return insertHistoryItem(url, guid, lastVisited, visitCount, null, null); } /* package-private */ Uri insertHistoryItem(String url, String guid, Long lastVisited, Integer visitCount, String title) throws RemoteException { + return insertHistoryItem(url, guid, lastVisited, visitCount, null, title); + } + + /* package-private */ Uri insertHistoryItem(String url, String guid, Long lastVisited, Integer visitCount, Integer remoteVisits, String title) throws RemoteException { ContentValues historyItem = new ContentValues(); historyItem.put(BrowserContract.History.URL, url); if (guid != null) { @@ -67,6 +70,9 @@ public class BrowserProviderHistoryVisitsTestBase { if (visitCount != null) { historyItem.put(BrowserContract.History.VISITS, visitCount); } + if (remoteVisits != null) { + historyItem.put(BrowserContract.History.REMOTE_VISITS, remoteVisits); + } historyItem.put(BrowserContract.History.DATE_LAST_VISITED, lastVisited); if (title != null) { historyItem.put(BrowserContract.History.TITLE, title);