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
This commit is contained in:
Grisha Kruglov 2016-11-29 13:42:53 -08:00
Родитель 277bfc33aa
Коммит 29a79ad111
7 изменённых файлов: 378 добавлений и 63 удалений

Просмотреть файл

@ -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

Просмотреть файл

@ -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<ContentProviderOperation> operations)
throws OperationApplicationException {

Просмотреть файл

@ -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<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;
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;
}
/**

Просмотреть файл

@ -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<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);
}

Просмотреть файл

@ -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;
}

Просмотреть файл

@ -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,

Просмотреть файл

@ -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);