author | Grisha Kruglov <gkruglov@mozilla.com> |
Tue, 29 Nov 2016 13:42:53 -0800 | |
changeset 344873 | 28164480660dba21fba8e217c74a32b53edf8246 |
parent 344872 | c1069ad96647a8d0117ec976d70c6bf3523c48aa |
child 344874 | 3c89bba23c2d344c514b2d59ca52c377d55c541f |
push id | 37970 |
push user | gkruglov@mozilla.com |
push date | Sat, 25 Feb 2017 01:09:28 +0000 |
treeherder | autoland@bd232d46a396 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | rnewman |
bugs | 1291821 |
milestone | 54.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/mobile/android/base/java/org/mozilla/gecko/db/BrowserContract.java +++ b/mobile/android/base/java/org/mozilla/gecko/db/BrowserContract.java @@ -53,16 +53,21 @@ public class BrowserContract { public static final String PARAM_INSERT_IF_NEEDED = "insert_if_needed"; public static final String PARAM_INCREMENT_VISITS = "increment_visits"; public static final String PARAM_INCREMENT_REMOTE_AGGREGATES = "increment_remote_aggregates"; public static final String PARAM_NON_POSITIONED_PINS = "non_positioned_pins"; public static final String PARAM_EXPIRE_PRIORITY = "priority"; 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 } /** * Produces a SQL expression used for sorting results of the "combined" view by frecency. * Combines remote and local frecency calculations, weighting local visits much heavier.
--- a/mobile/android/base/java/org/mozilla/gecko/db/BrowserProvider.java +++ b/mobile/android/base/java/org/mozilla/gecko/db/BrowserProvider.java @@ -27,16 +27,17 @@ import org.mozilla.gecko.db.BrowserContr 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.sync.repositories.android.BrowserContractHelpers; import org.mozilla.gecko.util.ThreadUtils; import android.content.BroadcastReceiver; import android.content.ContentProviderOperation; import android.content.ContentProviderResult; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; @@ -44,20 +45,25 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.OperationApplicationException; import android.content.UriMatcher; import android.database.Cursor; 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; public class BrowserProvider extends SharedBrowserDatabaseProvider { public static final String ACTION_SHRINK_MEMORY = "org.mozilla.gecko.db.intent.action.SHRINK_MEMORY"; private static final String LOGTAG = "GeckoBrowserProvider"; @@ -2202,16 +2208,240 @@ public class BrowserProvider extends Sha + " WHERE " + Bookmarks.IS_DELETED + " = 0" + " AND " + Bookmarks.URL + " IS NOT NULL)"; return deleteFavicons(uri, faviconSelection, null) + deleteThumbnails(uri, thumbnailSelection, null) + 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<ContentProviderOperation> operations) throws OperationApplicationException { final int numOperations = operations.size(); final ContentProviderResult[] results = new ContentProviderResult[numOperations]; if (numOperations < 1) { debug("applyBatch: no operations; returning immediately.");
--- 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 @@ -12,16 +12,17 @@ import org.mozilla.gecko.background.comm import org.mozilla.gecko.db.BrowserContract; import org.mozilla.gecko.sync.repositories.NullCursorException; import org.mozilla.gecko.sync.repositories.domain.HistoryRecord; 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 { public AndroidBrowserHistoryDataAccessor(Context context) { super(context); } @@ -40,17 +41,17 @@ public class AndroidBrowserHistoryDataAc if (rec.visits != null) { JSONArray visits = rec.visits; long mostRecent = getLastVisited(visits); // Fennec stores history timestamps in milliseconds, and visit timestamps in microseconds. // 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; } @Override protected String[] getAllColumns() { return BrowserContractHelpers.HistoryColumns; } @@ -104,73 +105,48 @@ public class AndroidBrowserHistoryDataAc * then inserts all the visit information (also using <code>ContentProvider.bulkInsert</code>). * * @param records * the records to insert. * @return * the number of records actually inserted. * @throws NullCursorException */ - public int bulkInsert(ArrayList<HistoryRecord> 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<HistoryRecord> 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; - } - - // 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 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++; } - 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) - ); + final Bundle data = new Bundle(); + data.putSerializable(BrowserContract.METHOD_PARAM_DATA, historyBundles); - // 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} - ); - } - } + // 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"); } - - return inserted; + final Exception thrownException = (Exception) result.getSerializable(BrowserContract.METHOD_RESULT); + return thrownException == null; } /** * Helper method used to find largest <code>VisitsHelper.SYNC_DATE_KEY</code> value in a provided JSONArray. * * @param visits Array of objects which will be searched. * @return largest value of <code>VisitsHelper.SYNC_DATE_KEY</code>. */
--- 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 @@ -23,17 +23,17 @@ import android.database.Cursor; import android.os.RemoteException; public class AndroidBrowserHistoryRepositorySession extends AndroidBrowserRepositorySession { public static final String LOG_TAG = "ABHistoryRepoSess"; /** * 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) { super(repository); dbHelper = new AndroidBrowserHistoryDataAccessor(context); } @Override @@ -157,21 +157,18 @@ public class AndroidBrowserHistoryReposi if (recordsBuffer.size() < 1) { Logger.debug(LOG_TAG, "No records to flush, returning."); return; } final ArrayList<HistoryRecord> outgoing = recordsBuffer; recordsBuffer = new ArrayList<HistoryRecord>(); 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); } return; } // All good, everybody succeeded. for (HistoryRecord succeeded : outgoing) {
--- 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 @@ -6,16 +6,18 @@ package org.mozilla.gecko.background.db; import android.content.ContentProvider; import android.content.ContentProviderOperation; import android.content.ContentProviderResult; 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; import java.util.ArrayList; /** * Wrap a ContentProvider, appending &test=1 to all queries. */ @@ -75,12 +77,18 @@ public class DelegatingTestContentProvid return mTargetProvider.applyBatch(operations); } @Override public int bulkInsert(Uri uri, ContentValues[] values) { 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; } }
--- 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 @@ -3,16 +3,17 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ package org.mozilla.gecko.db; 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; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mozilla.gecko.background.testhelpers.TestRunner; import org.robolectric.shadows.ShadowContentResolver; @@ -254,16 +255,108 @@ public class BrowserProviderHistoryTest assertTrue(true); // NB: same values as above, to ensure throwing update didn't actually change anything. assertHistoryAggregates(BrowserContract.History.URL + " = ?", new String[] {url}, 2, 19, lastVisited3, 8, lastVisited3); } } + @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, BrowserContract.History.LOCAL_VISITS, BrowserContract.History.REMOTE_VISITS, BrowserContract.History.LOCAL_DATE_LAST_VISITED, BrowserContract.History.REMOTE_DATE_LAST_VISITED }, selection, selectionArg, null);
--- 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 @@ -17,18 +17,17 @@ import org.robolectric.shadows.ShadowCon import java.util.UUID; public class BrowserProviderHistoryVisitsTestBase { /* package-private */ ShadowContentResolver contentResolver; /* package-private */ ContentProviderClient historyClient; /* 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 { provider = new BrowserProvider(); provider.onCreate(); ShadowContentResolver.registerProvider(BrowserContract.AUTHORITY, new DelegatingTestContentProvider(provider)); contentResolver = new ShadowContentResolver(); @@ -46,32 +45,39 @@ public class BrowserProviderHistoryVisit provider.shutdown(); } /* package-private */ Uri testUri(Uri baseUri) { return baseUri.buildUpon().appendQueryParameter(BrowserContract.PARAM_IS_TEST, "1").build(); } /* 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) { historyItem.put(BrowserContract.History.GUID, guid); } 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); } return historyClient.insert(historyTestUri, historyItem); } }