--- a/mobile/android/base/java/org/mozilla/gecko/db/BrowserProvider.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/BrowserProvider.java
@@ -3,26 +3,28 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.db;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import org.mozilla.gecko.AboutPages;
import org.mozilla.gecko.GeckoProfile;
import org.mozilla.gecko.R;
import org.mozilla.gecko.db.BrowserContract.Bookmarks;
import org.mozilla.gecko.db.BrowserContract.Combined;
import org.mozilla.gecko.db.BrowserContract.FaviconColumns;
import org.mozilla.gecko.db.BrowserContract.Favicons;
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.DBUtils.UpdateOperation;
import org.mozilla.gecko.sync.Utils;
@@ -58,16 +60,17 @@ public class BrowserProvider extends Sha
// Minimum duration to keep when expiring.
static final long DEFAULT_EXPIRY_PRESERVE_WINDOW = 1000L * 60L * 60L * 24L * 28L; // Four weeks.
// Minimum number of thumbnails to keep around.
static final int DEFAULT_EXPIRY_THUMBNAIL_COUNT = 15;
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 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;
@@ -105,31 +108,35 @@ public class BrowserProvider extends Sha
// Thumbnail matches
static final int THUMBNAILS = 800;
static final int THUMBNAIL_ID = 801;
static final int URL_ANNOTATIONS = 900;
static final int TOPSITES = 1000;
+ static final int VISITS = 1100;
+
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);
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 Table[] sTables;
static {
sTables = new Table[] {
// See awful shortcut assumption hack in getURLMetadataTable.
new URLMetadataTable()
};
// We will reuse this.
@@ -176,16 +183,27 @@ public class BrowserProvider extends Sha
map.put(History.VISITS, History.VISITS);
map.put(History.DATE_LAST_VISITED, History.DATE_LAST_VISITED);
map.put(History.DATE_CREATED, History.DATE_CREATED);
map.put(History.DATE_MODIFIED, History.DATE_MODIFIED);
map.put(History.GUID, History.GUID);
map.put(History.IS_DELETED, History.IS_DELETED);
HISTORY_PROJECTION_MAP = Collections.unmodifiableMap(map);
+ // Visits
+ URI_MATCHER.addURI(BrowserContract.AUTHORITY, "visits", VISITS);
+
+ map = new HashMap<String, String>();
+ map.put(Visits._ID, Visits._ID);
+ map.put(Visits.HISTORY_GUID, Visits.HISTORY_GUID);
+ map.put(Visits.VISIT_TYPE, Visits.VISIT_TYPE);
+ map.put(Visits.DATE_VISITED, Visits.DATE_VISITED);
+ map.put(Visits.IS_LOCAL, Visits.IS_LOCAL);
+ VISIT_PROJECTION_MAP = Collections.unmodifiableMap(map);
+
// Favicons
URI_MATCHER.addURI(BrowserContract.AUTHORITY, "favicons", FAVICONS);
URI_MATCHER.addURI(BrowserContract.AUTHORITY, "favicons/#", FAVICON_ID);
map = new HashMap<String, String>();
map.put(Favicons._ID, Favicons._ID);
map.put(Favicons.URL, Favicons.URL);
map.put(Favicons.DATA, Favicons.DATA);
@@ -418,21 +436,37 @@ public class BrowserProvider extends Sha
selection = DBUtils.concatenateWhere(selection, TABLE_HISTORY + "._id = ?");
selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
new String[] { Long.toString(ContentUris.parseId(uri)) });
// fall through
case HISTORY: {
trace("Deleting history: " + uri);
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)}.
+ */
+ if (!isCallerSync(uri)) {
+ deleteVisitsForHistory(uri, selection, selectionArgs);
+ }
deleted = deleteHistory(uri, selection, selectionArgs);
deleteUnusedImages(uri);
break;
}
+ case VISITS:
+ trace("Deleting visits: " + uri);
+ beginWrite(db);
+ deleted = deleteVisits(uri, selection, selectionArgs);
+ break;
+
case HISTORY_OLD: {
String priority = uri.getQueryParameter(BrowserContract.PARAM_EXPIRE_PRIORITY);
long keepAfter = System.currentTimeMillis() - DEFAULT_EXPIRY_PRESERVE_WINDOW;
int retainCount = DEFAULT_EXPIRY_RETAIN_COUNT;
if (BrowserContract.ExpirePriority.AGGRESSIVE.toString().equals(priority)) {
keepAfter = 0;
retainCount = AGGRESSIVE_EXPIRY_RETAIN_COUNT;
@@ -507,16 +541,22 @@ public class BrowserProvider extends Sha
}
case HISTORY: {
trace("Insert on HISTORY: " + uri);
id = insertHistory(uri, values);
break;
}
+ case VISITS: {
+ trace("Insert on VISITS: " + uri);
+ id = insertVisit(uri, values);
+ break;
+ }
+
case FAVICONS: {
trace("Insert on FAVICONS: " + uri);
id = insertFavicon(uri, values);
break;
}
case THUMBNAILS: {
trace("Insert on THUMBNAILS: " + uri);
@@ -612,16 +652,19 @@ public class BrowserProvider extends Sha
// fall through
case HISTORY: {
debug("Updating history: " + uri);
if (shouldUpdateOrInsert(uri)) {
updated = updateOrInsertHistory(uri, values, selection, selectionArgs);
} else {
updated = updateHistory(uri, values, selection, selectionArgs);
}
+ if (shouldIncrementVisits(uri)) {
+ insertVisitForHistory(uri, values, selection, selectionArgs);
+ }
break;
}
case FAVICONS: {
debug("Update on FAVICONS: " + uri);
String url = values.getAsString(Favicons.URL);
String faviconSelection = null;
@@ -1034,16 +1077,26 @@ public class BrowserProvider extends Sha
if (hasFaviconsInProjection(projection))
qb.setTables(VIEW_HISTORY_WITH_FAVICONS);
else
qb.setTables(TABLE_HISTORY);
break;
}
+ case VISITS:
+ debug("Query is on visits: " + uri);
+ qb.setProjectionMap(VISIT_PROJECTION_MAP);
+ qb.setTables(TABLE_VISITS);
+
+ if (TextUtils.isEmpty(sortOrder)) {
+ sortOrder = DEFAULT_VISITS_SORT_ORDER;
+ }
+ break;
+
case FAVICON_ID:
selection = DBUtils.concatenateWhere(selection, Favicons._ID + " = ?");
selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
new String[] { Long.toString(ContentUris.parseId(uri)) });
// fall through
case FAVICONS: {
debug("Query is on favicons: " + uri);
@@ -1359,42 +1412,95 @@ public class BrowserProvider extends Sha
// Use the simple code path for easy updates.
if (!shouldIncrementVisits(uri)) {
trace("Updating history meta data only");
return db.update(TABLE_HISTORY, values, selection, selectionArgs);
}
trace("Updating history meta data and incrementing visits");
- // We might be altering the ContentValues, so let's use a copy.
- final ContentValues valuesForUpdate = new ContentValues(values);
-
// Update data and increment visits by 1.
- long incVisits = 1;
- if (valuesForUpdate.containsKey(History.VISITS)) {
- // Use a given visit count, if found.
- Long additional = valuesForUpdate.getAsLong(History.VISITS);
- if (additional != null) {
- incVisits = additional;
- }
- // Remove the visits from this set of values so we can pass the visits
- // as an expression.
- valuesForUpdate.remove(History.VISITS);
- }
+ final long incVisits = 1;
// Create a separate set of values that will be updated as an expression.
final ContentValues visits = new ContentValues();
visits.put(History.VISITS, History.VISITS + " + " + incVisits);
- final ContentValues[] valuesAndVisits = { valuesForUpdate, visits };
+ final ContentValues[] valuesAndVisits = { values, visits };
final UpdateOperation[] ops = { UpdateOperation.ASSIGN, UpdateOperation.EXPRESSION };
return DBUtils.updateArrays(db, TABLE_HISTORY, valuesAndVisits, ops, selection, selectionArgs);
}
+ private long insertVisitForHistory(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ trace("Inserting visit for history on URI: " + uri);
+
+ final SQLiteDatabase db = getReadableDatabase(uri);
+
+ 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 insert visit for history URI: " + uri);
+ return 0;
+ }
+ final ContentValues[] visitValues;
+ try {
+ visitValues = new ContentValues[cursor.getCount()];
+
+ if (!cursor.moveToFirst()) {
+ Log.e(LOGTAG, "No history records found while inserting visit(s) for history URI: " + uri);
+ return 0;
+ }
+
+ // Sync works in microseconds, so we store visit timestamps in microseconds as well.
+ // History timestamps are in milliseconds.
+ // This is the conversion point for locally generated visits.
+ final long visitDate;
+ if (values.containsKey(History.DATE_LAST_VISITED)) {
+ visitDate = values.getAsLong(History.DATE_LAST_VISITED) * 1000;
+ } else {
+ visitDate = System.currentTimeMillis() * 1000;
+ }
+
+ final int guidColumn = cursor.getColumnIndexOrThrow(History.GUID);
+ while (!cursor.isAfterLast()) {
+ final ContentValues visit = new ContentValues();
+ visit.put(Visits.HISTORY_GUID, cursor.getString(guidColumn));
+ visit.put(Visits.DATE_VISITED, visitDate);
+ visitValues[cursor.getPosition()] = visit;
+ cursor.moveToNext();
+ }
+ } finally {
+ cursor.close();
+ }
+
+ if (visitValues.length == 1) {
+ return insertVisit(Visits.CONTENT_URI, visitValues[0]);
+ } else {
+ return bulkInsert(Visits.CONTENT_URI, visitValues);
+ }
+ }
+
+ private long insertVisit(Uri uri, ContentValues values) {
+ final SQLiteDatabase db = getWritableDatabase(uri);
+
+ debug("Inserting history in database with URL: " + uri);
+ beginWrite(db);
+
+ // We ignore insert conflicts here to simplify inserting visits records coming in from Sync.
+ // Visits table has a unique index on (history_guid,date), so a conflict might arise when we're
+ // trying to insert history record visits coming in from sync which are already present locally
+ // as a result of previous sync operations.
+ // An alternative to doing this is to filter out already present records when we're doing history inserts
+ // from Sync, which is a costly operation to do en masse.
+ return db.insertWithOnConflict(
+ TABLE_VISITS, null, values, SQLiteDatabase.CONFLICT_IGNORE);
+ }
+
private void updateFaviconIdsForUrl(SQLiteDatabase db, String pageUrl, Long faviconId) {
ContentValues updateValues = new ContentValues(1);
updateValues.put(FaviconColumns.FAVICON_ID, faviconId);
db.update(TABLE_HISTORY,
updateValues,
History.URL + " = ?",
new String[] { pageUrl });
db.update(TABLE_BOOKMARKS,
@@ -1620,16 +1726,71 @@ 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);
+
+ 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;
+ }
+
+ ArrayList<String> historyGUIDs = new ArrayList<>();
+ try {
+ if (!cursor.moveToFirst()) {
+ trace("No history items for which to remove visits matched for URI: " + uri);
+ return 0;
+ }
+ final int historyColumn = cursor.getColumnIndexOrThrow(History.GUID);
+ while (!cursor.isAfterLast()) {
+ historyGUIDs.add(cursor.getString(historyColumn));
+ cursor.moveToNext();
+ }
+ } finally {
+ cursor.close();
+ }
+
+ // 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),
+ chunkGUIDs.toArray(new String[chunkGUIDs.size()])
+ );
+ }
+
+ return deleted;
+ }
+
+ private int deleteVisits(Uri uri, String selection, String[] selectionArgs) {
+ debug("Deleting visits for URI: " + uri);
+
+ final SQLiteDatabase db = getWritableDatabase(uri);
+
+ beginWrite(db);
+ return db.delete(TABLE_VISITS, selection, selectionArgs);
+ }
+
private int deleteBookmarks(Uri uri, String selection, String[] selectionArgs) {
debug("Deleting bookmarks for URI: " + uri);
final SQLiteDatabase db = getWritableDatabase(uri);
if (isCallerSync(uri)) {
beginWrite(db);
return db.delete(TABLE_BOOKMARKS, selection, selectionArgs);
--- a/mobile/android/base/java/org/mozilla/gecko/db/DBUtils.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/DBUtils.java
@@ -22,16 +22,18 @@ import android.util.Log;
import org.mozilla.gecko.annotation.RobocopTarget;
import org.mozilla.gecko.Telemetry;
import java.util.Map;
public class DBUtils {
private static final String LOGTAG = "GeckoDBUtils";
+ public static final int SQLITE_MAX_VARIABLE_NUMBER = 999;
+
public static final String qualifyColumn(String table, String column) {
return table + "." + column;
}
// This is available in Android >= 11. Implemented locally to be
// compatible with older versions.
public static String concatenateWhere(String a, String b) {
if (TextUtils.isEmpty(a)) {
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/BrowserContractHelpers.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/BrowserContractHelpers.java
@@ -28,16 +28,17 @@ public class BrowserContractHelpers exte
.appendQueryParameter(PARAM_IS_SYNC, "true")
.build();
}
public static final Uri BOOKMARKS_CONTENT_URI = withSyncAndDeletedAndProfile(Bookmarks.CONTENT_URI);
public static final Uri BOOKMARKS_PARENTS_CONTENT_URI = withSyncAndDeletedAndProfile(Bookmarks.PARENTS_CONTENT_URI);
public static final Uri BOOKMARKS_POSITIONS_CONTENT_URI = withSyncAndDeletedAndProfile(Bookmarks.POSITIONS_CONTENT_URI);
public static final Uri HISTORY_CONTENT_URI = withSyncAndDeletedAndProfile(History.CONTENT_URI);
+ public static final Uri VISITS_CONTENT_URI = withSyncAndDeletedAndProfile(Visits.CONTENT_URI);
public static final Uri SCHEMA_CONTENT_URI = withSyncAndDeletedAndProfile(Schema.CONTENT_URI);
public static final Uri PASSWORDS_CONTENT_URI = withSyncAndDeletedAndProfile(Passwords.CONTENT_URI);
public static final Uri DELETED_PASSWORDS_CONTENT_URI = withSyncAndDeletedAndProfile(DeletedPasswords.CONTENT_URI);
public static final Uri FORM_HISTORY_CONTENT_URI = withSyncAndProfile(FormHistory.CONTENT_URI);
public static final Uri DELETED_FORM_HISTORY_CONTENT_URI = withSyncAndProfile(DeletedFormHistory.CONTENT_URI);
public static final Uri TABS_CONTENT_URI = withSyncAndProfile(Tabs.CONTENT_URI);
public static final Uri CLIENTS_CONTENT_URI = withSyncAndProfile(Clients.CONTENT_URI);
public static final Uri LOGINS_CONTENT_URI = withSyncAndProfile(Logins.CONTENT_URI);
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserProviderHistoryVisitsTest.java
@@ -0,0 +1,338 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.db;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+
+import org.mozilla.gecko.db.BrowserContract.History;
+
+import static org.junit.Assert.*;
+
+@RunWith(TestRunner.class)
+/**
+ * Testing insertion/deletion of visits as by-product of updating history records through BrowserProvider
+ */
+public class BrowserProviderHistoryVisitsTest extends BrowserProviderHistoryVisitsTestBase {
+ @Test
+ /**
+ * Testing updating history records without affecting visits
+ */
+ public void testUpdateNoVisit() throws Exception {
+ insertHistoryItem("https://www.mozilla.org", "testGUID");
+
+ Cursor cursor = visitsClient.query(visitsTestUri, null, null, null, null);
+ assertNotNull(cursor);
+ assertEquals(0, cursor.getCount());
+ cursor.close();
+
+ ContentValues historyUpdate = new ContentValues();
+ historyUpdate.put(History.TITLE, "Mozilla!");
+ assertEquals(1,
+ historyClient.update(
+ historyTestUri, historyUpdate, History.URL + " = ?", new String[] {"https://www.mozilla.org"}
+ )
+ );
+
+ cursor = visitsClient.query(visitsTestUri, null, null, null, null);
+ assertNotNull(cursor);
+ assertEquals(0, cursor.getCount());
+ cursor.close();
+
+ ContentValues historyToInsert = new ContentValues();
+ historyToInsert.put(History.URL, "https://www.eff.org");
+ assertEquals(1,
+ historyClient.update(
+ historyTestUri.buildUpon()
+ .appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build(),
+ historyToInsert, null, null
+ )
+ );
+
+ cursor = visitsClient.query(visitsTestUri, null, null, null, null);
+ assertNotNull(cursor);
+ assertEquals(0, cursor.getCount());
+ cursor.close();
+ }
+
+ @Test
+ /**
+ * Testing INCREMENT_VISITS flag for multiple history records at once
+ */
+ public void testUpdateMultipleHistoryIncrementVisit() throws Exception {
+ insertHistoryItem("https://www.mozilla.org", "testGUID");
+ insertHistoryItem("https://www.mozilla.org", "testGUID2");
+
+ // test that visits get inserted when updating existing history records
+ assertEquals(2, historyClient.update(
+ historyTestUri.buildUpon()
+ .appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true").build(),
+ new ContentValues(), History.URL + " = ?", new String[] {"https://www.mozilla.org"}
+ ));
+
+ Cursor cursor = visitsClient.query(
+ visitsTestUri, new String[] {BrowserContract.Visits.HISTORY_GUID}, null, null, null);
+ assertNotNull(cursor);
+ assertEquals(2, cursor.getCount());
+ assertTrue(cursor.moveToFirst());
+
+ String guid1 = cursor.getString(cursor.getColumnIndex(BrowserContract.Visits.HISTORY_GUID));
+ cursor.moveToNext();
+ String guid2 = cursor.getString(cursor.getColumnIndex(BrowserContract.Visits.HISTORY_GUID));
+ cursor.close();
+
+ assertNotEquals(guid1, guid2);
+
+ assertTrue(guid1.equals("testGUID") || guid1.equals("testGUID2"));
+ }
+
+ @Test
+ /**
+ * Testing INCREMENT_VISITS flag and its interplay with INSERT_IF_NEEDED
+ */
+ public void testUpdateHistoryIncrementVisit() throws Exception {
+ insertHistoryItem("https://www.mozilla.org", "testGUID");
+
+ // test that visit gets inserted when updating an existing histor record
+ assertEquals(1, historyClient.update(
+ historyTestUri.buildUpon()
+ .appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true").build(),
+ new ContentValues(), History.URL + " = ?", new String[] {"https://www.mozilla.org"}
+ ));
+
+ Cursor cursor = visitsClient.query(
+ visitsTestUri, new String[] {BrowserContract.Visits.HISTORY_GUID}, null, null, null);
+ assertNotNull(cursor);
+ assertEquals(1, cursor.getCount());
+ assertTrue(cursor.moveToFirst());
+ assertEquals(
+ "testGUID",
+ cursor.getString(cursor.getColumnIndex(BrowserContract.Visits.HISTORY_GUID))
+ );
+ cursor.close();
+
+ // test that visit gets inserted when updatingOrInserting a new history record
+ ContentValues historyItem = new ContentValues();
+ historyItem.put(History.URL, "https://www.eff.org");
+
+ assertEquals(1, historyClient.update(
+ historyTestUri.buildUpon()
+ .appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true")
+ .appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build(),
+ historyItem, null, null
+ ));
+
+ cursor = historyClient.query(
+ historyTestUri,
+ new String[] {History.GUID}, History.URL + " = ?", new String[] {"https://www.eff.org"}, null
+ );
+ assertNotNull(cursor);
+ assertEquals(1, cursor.getCount());
+ assertTrue(cursor.moveToFirst());
+ String insertedGUID = cursor.getString(cursor.getColumnIndex(History.GUID));
+ cursor.close();
+
+ cursor = visitsClient.query(
+ visitsTestUri, new String[] {BrowserContract.Visits.HISTORY_GUID}, null, null, null);
+ assertNotNull(cursor);
+ assertEquals(2, cursor.getCount());
+ assertTrue(cursor.moveToFirst());
+ assertEquals(insertedGUID,
+ cursor.getString(cursor.getColumnIndex(BrowserContract.Visits.HISTORY_GUID))
+ );
+ cursor.close();
+ }
+
+ @Test
+ /**
+ * Test that for locally generated visits, we store their timestamps in microseconds, and not in
+ * milliseconds like history does.
+ */
+ public void testTimestampConversionOnInsertion() throws Exception {
+ insertHistoryItem("https://www.mozilla.org", "testGUID");
+
+ Long lastVisited = System.currentTimeMillis();
+ ContentValues updatedVisitedTime = new ContentValues();
+ updatedVisitedTime.put(History.DATE_LAST_VISITED, lastVisited);
+
+ // test with last visited date passed in
+ assertEquals(1, historyClient.update(
+ historyTestUri.buildUpon()
+ .appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true").build(),
+ updatedVisitedTime, History.URL + " = ?", new String[] {"https://www.mozilla.org"}
+ ));
+
+ Cursor cursor = visitsClient.query(visitsTestUri, new String[] {BrowserContract.Visits.DATE_VISITED}, null, null, null);
+ assertNotNull(cursor);
+ assertEquals(1, cursor.getCount());
+ assertTrue(cursor.moveToFirst());
+
+ assertEquals(lastVisited * 1000, cursor.getLong(cursor.getColumnIndex(BrowserContract.Visits.DATE_VISITED)));
+ cursor.close();
+
+ // test without last visited date
+ assertEquals(1, historyClient.update(
+ historyTestUri.buildUpon()
+ .appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true").build(),
+ new ContentValues(), History.URL + " = ?", new String[] {"https://www.mozilla.org"}
+ ));
+
+ cursor = visitsClient.query(visitsTestUri, new String[] {BrowserContract.Visits.DATE_VISITED}, null, null, null);
+ assertNotNull(cursor);
+ assertEquals(2, cursor.getCount());
+ assertTrue(cursor.moveToFirst());
+
+ // CP should generate time off of current time upon insertion and convert to microseconds.
+ // This also tests correct ordering (DESC on date).
+ assertTrue(lastVisited * 1000 < cursor.getLong(cursor.getColumnIndex(BrowserContract.Visits.DATE_VISITED)));
+ cursor.close();
+ }
+
+ @Test
+ /**
+ * This should perform `DELETE FROM visits WHERE history_guid in IN (?, ?, ?, ..., ?)` sort of statement
+ * SQLite has a variable count limit (999 by default), so we're testing here that our deletion
+ * code does the right thing and chunks deletes to account for this limitation.
+ */
+ public void testDeletingLotsOfHistory() throws Exception {
+ Uri incrementUri = historyTestUri.buildUpon()
+ .appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true").build();
+
+ // insert bunch of history records, and for each insert a visit
+ for (int i = 0; i < 2100; i++) {
+ final String url = "https://www.mozilla" + i + ".org";
+ insertHistoryItem(url, "testGUID" + i);
+ assertEquals(1, historyClient.update(incrementUri, new ContentValues(), History.URL + " = ?", new String[] {url}));
+ }
+
+ // sanity check
+ Cursor cursor = visitsClient.query(visitsTestUri, null, null, null, null);
+ assertNotNull(cursor);
+ assertEquals(2100, cursor.getCount());
+ cursor.close();
+
+ // delete all of the history items - this will trigger chunked deletion of visits as well
+ assertEquals(2100,
+ historyClient.delete(historyTestUri, null, null)
+ );
+
+ // check that all visits where deleted
+ cursor = visitsClient.query(visitsTestUri, null, null, null, null);
+ assertNotNull(cursor);
+ assertEquals(0, cursor.getCount());
+ cursor.close();
+ }
+
+ @Test
+ /**
+ * Test visit deletion as by-product of history deletion - both explicit (from outside of Sync),
+ * and implicit (cascaded, from Sync).
+ */
+ public void testDeletingHistory() throws Exception {
+ insertHistoryItem("https://www.mozilla.org", "testGUID");
+ insertHistoryItem("https://www.eff.org", "testGUID2");
+
+ // insert some visits
+ assertEquals(1, historyClient.update(
+ historyTestUri.buildUpon()
+ .appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true").build(),
+ new ContentValues(), History.URL + " = ?", new String[] {"https://www.mozilla.org"}
+ ));
+ assertEquals(1, historyClient.update(
+ historyTestUri.buildUpon()
+ .appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true").build(),
+ new ContentValues(), History.URL + " = ?", new String[] {"https://www.mozilla.org"}
+ ));
+ assertEquals(1, historyClient.update(
+ historyTestUri.buildUpon()
+ .appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true").build(),
+ new ContentValues(), History.URL + " = ?", new String[] {"https://www.eff.org"}
+ ));
+
+ Cursor cursor = visitsClient.query(visitsTestUri, null, null, null, null);
+ assertNotNull(cursor);
+ assertEquals(3, cursor.getCount());
+ cursor.close();
+
+ // test that corresponding visit records are deleted if Sync isn't involved
+ assertEquals(1,
+ historyClient.delete(historyTestUri, History.URL + " = ?", new String[] {"https://www.mozilla.org"})
+ );
+
+ cursor = visitsClient.query(visitsTestUri, null, null, null, null);
+ assertNotNull(cursor);
+ assertEquals(1, cursor.getCount());
+ cursor.close();
+
+ // test that corresponding visit records are deleted if Sync is involved
+ // insert some more visits
+ ContentValues moz = new ContentValues();
+ moz.put(History.URL, "https://www.mozilla.org");
+ moz.put(History.GUID, "testGUID3");
+ assertEquals(1, historyClient.update(
+ historyTestUri.buildUpon()
+ .appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true")
+ .appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build(),
+ moz, History.URL + " = ?", new String[] {"https://www.mozilla.org"}
+ ));
+ assertEquals(1, historyClient.update(
+ historyTestUri.buildUpon()
+ .appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true")
+ .appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build(),
+ new ContentValues(), History.URL + " = ?", new String[] {"https://www.eff.org"}
+ ));
+
+ assertEquals(1,
+ historyClient.delete(
+ historyTestUri.buildUpon().appendQueryParameter(BrowserContract.PARAM_IS_SYNC, "true").build(),
+ History.URL + " = ?", new String[] {"https://www.eff.org"})
+ );
+
+ cursor = visitsClient.query(visitsTestUri, new String[] {BrowserContract.Visits.HISTORY_GUID}, null, null, null);
+ assertNotNull(cursor);
+ assertEquals(1, cursor.getCount());
+ assertTrue(cursor.moveToFirst());
+ assertEquals("testGUID3", cursor.getString(cursor.getColumnIndex(BrowserContract.Visits.HISTORY_GUID)));
+ cursor.close();
+ }
+
+ @Test
+ /**
+ * Test that changes to History GUID are cascaded to individual visits.
+ * See UPDATE CASCADED on Visit's HISTORY_GUID foreign key.
+ */
+ public void testHistoryGUIDUpdate() throws Exception {
+ insertHistoryItem("https://www.mozilla.org", "testGUID");
+ insertHistoryItem("https://www.eff.org", "testGUID2");
+
+ // insert some visits
+ assertEquals(1, historyClient.update(
+ historyTestUri.buildUpon()
+ .appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true").build(),
+ new ContentValues(), History.URL + " = ?", new String[] {"https://www.mozilla.org"}
+ ));
+ assertEquals(1, historyClient.update(
+ historyTestUri.buildUpon()
+ .appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true").build(),
+ new ContentValues(), History.URL + " = ?", new String[] {"https://www.mozilla.org"}
+ ));
+
+ // change testGUID -> testGUIDNew
+ ContentValues newGuid = new ContentValues();
+ newGuid.put(History.GUID, "testGUIDNew");
+ assertEquals(1, historyClient.update(
+ historyTestUri, newGuid, History.URL + " = ?", new String[] {"https://www.mozilla.org"}
+ ));
+
+ Cursor cursor = visitsClient.query(visitsTestUri, null, BrowserContract.Visits.HISTORY_GUID + " = ?", new String[] {"testGUIDNew"}, null);
+ assertNotNull(cursor);
+ assertEquals(2, cursor.getCount());
+ cursor.close();
+ }
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserProviderHistoryVisitsTestBase.java
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.db;
+
+import android.content.ContentProviderClient;
+import android.content.ContentValues;
+import android.net.Uri;
+import android.os.RemoteException;
+
+import org.junit.After;
+import org.junit.Before;
+import org.mozilla.gecko.sync.repositories.android.BrowserContractHelpers;
+import org.robolectric.shadows.ShadowContentResolver;
+
+public class BrowserProviderHistoryVisitsTestBase {
+ protected BrowserProvider provider;
+ protected ContentProviderClient historyClient;
+ protected ContentProviderClient visitsClient;
+ protected Uri historyTestUri;
+ protected Uri visitsTestUri;
+
+ @Before
+ public void setUp() throws Exception {
+ provider = new BrowserProvider();
+ provider.onCreate();
+ ShadowContentResolver.registerProvider(BrowserContract.AUTHORITY_URI.toString(), provider);
+
+ final ShadowContentResolver cr = new ShadowContentResolver();
+ historyClient = cr.acquireContentProviderClient(BrowserContractHelpers.HISTORY_CONTENT_URI);
+ visitsClient = cr.acquireContentProviderClient(BrowserContractHelpers.VISITS_CONTENT_URI);
+
+ historyTestUri = testUri(BrowserContract.History.CONTENT_URI);
+ visitsTestUri = testUri(BrowserContract.Visits.CONTENT_URI);
+ }
+
+ @After
+ public void tearDown() {
+ historyClient.release();
+ visitsClient.release();
+ provider.shutdown();
+ }
+
+ protected Uri testUri(Uri baseUri) {
+ return baseUri.buildUpon().appendQueryParameter(BrowserContract.PARAM_IS_TEST, "1").build();
+ }
+
+ protected Uri insertHistoryItem(String url, String guid) throws RemoteException {
+ ContentValues historyItem = new ContentValues();
+ historyItem.put(BrowserContract.History.URL, url);
+ historyItem.put(BrowserContract.History.GUID, guid);
+
+ return historyClient.insert(historyTestUri, historyItem);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserProviderVisitsTest.java
@@ -0,0 +1,301 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.db;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.RemoteException;
+
+import static org.junit.Assert.*;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.db.BrowserContract.Visits;
+
+@RunWith(TestRunner.class)
+/**
+ * Testing direct interactions with visits through BrowserProvider
+ */
+public class BrowserProviderVisitsTest extends BrowserProviderHistoryVisitsTestBase {
+ @Test
+ /**
+ * Test that default visit parameters are set on insert.
+ */
+ public void testDefaultVisit() throws RemoteException {
+ String url = "https://www.mozilla.org";
+ String guid = "testGuid";
+
+ assertNotNull(insertHistoryItem(url, guid));
+
+ ContentValues visitItem = new ContentValues();
+ Long visitedDate = System.currentTimeMillis();
+ visitItem.put(Visits.HISTORY_GUID, guid);
+ visitItem.put(Visits.DATE_VISITED, visitedDate);
+ Uri insertedVisitUri = visitsClient.insert(visitsTestUri, visitItem);
+ assertNotNull(insertedVisitUri);
+
+ Cursor cursor = visitsClient.query(visitsTestUri, null, null, null, null);
+ assertNotNull(cursor);
+ try {
+ assertTrue(cursor.moveToFirst());
+ String insertedGuid = cursor.getString(cursor.getColumnIndex(Visits.HISTORY_GUID));
+ assertEquals(guid, insertedGuid);
+
+ Long insertedDate = cursor.getLong(cursor.getColumnIndex(Visits.DATE_VISITED));
+ assertEquals(visitedDate, insertedDate);
+
+ Integer insertedType = cursor.getInt(cursor.getColumnIndex(Visits.VISIT_TYPE));
+ assertEquals(insertedType, Integer.valueOf(1));
+
+ Integer insertedIsLocal = cursor.getInt(cursor.getColumnIndex(Visits.IS_LOCAL));
+ assertEquals(insertedIsLocal, Integer.valueOf(1));
+ } finally {
+ cursor.close();
+ }
+ }
+
+ @Test
+ /**
+ * Test that we can't insert visit for non-existing GUID.
+ */
+ public void testMissingHistoryGuid() throws RemoteException {
+ ContentValues visitItem = new ContentValues();
+ visitItem.put(Visits.HISTORY_GUID, "blah");
+ visitItem.put(Visits.DATE_VISITED, System.currentTimeMillis());
+ assertNull(visitsClient.insert(visitsTestUri, visitItem));
+ }
+
+ @Test
+ /**
+ * Test that visit insert uses non-conflict insert.
+ */
+ public void testNonConflictInsert() throws RemoteException {
+ String url = "https://www.mozilla.org";
+ String guid = "testGuid";
+
+ assertNotNull(insertHistoryItem(url, guid));
+
+ ContentValues visitItem = new ContentValues();
+ Long visitedDate = System.currentTimeMillis();
+ visitItem.put(Visits.HISTORY_GUID, guid);
+ visitItem.put(Visits.DATE_VISITED, visitedDate);
+ Uri insertedVisitUri = visitsClient.insert(visitsTestUri, visitItem);
+ assertNotNull(insertedVisitUri);
+
+ Uri insertedVisitUri2 = visitsClient.insert(visitsTestUri, visitItem);
+ assertEquals(insertedVisitUri, insertedVisitUri2);
+ }
+
+ @Test
+ /**
+ * Test that non-default visit parameters won't get overridden.
+ */
+ public void testNonDefaultInsert() throws RemoteException {
+ assertNotNull(insertHistoryItem("https://www.mozilla.org", "testGuid"));
+
+ Integer typeToInsert = 5;
+ Integer isLocalToInsert = 0;
+
+ ContentValues visitItem = new ContentValues();
+ visitItem.put(Visits.HISTORY_GUID, "testGuid");
+ visitItem.put(Visits.DATE_VISITED, System.currentTimeMillis());
+ visitItem.put(Visits.VISIT_TYPE, typeToInsert);
+ visitItem.put(Visits.IS_LOCAL, isLocalToInsert);
+
+ assertNotNull(visitsClient.insert(visitsTestUri, visitItem));
+
+ Cursor cursor = visitsClient.query(visitsTestUri, null, null, null, null);
+ assertNotNull(cursor);
+ try {
+ assertTrue(cursor.moveToFirst());
+
+ Integer insertedVisitType = cursor.getInt(cursor.getColumnIndex(Visits.VISIT_TYPE));
+ assertEquals(typeToInsert, insertedVisitType);
+
+ Integer insertedIsLocal = cursor.getInt(cursor.getColumnIndex(Visits.IS_LOCAL));
+ assertEquals(isLocalToInsert, insertedIsLocal);
+ } finally {
+ cursor.close();
+ }
+ }
+
+ @Test
+ /**
+ * Test that default sorting order (DATE_VISITED DESC) is set if we don't specify any sorting params
+ */
+ public void testDefaultSortingOrder() throws RemoteException {
+ assertNotNull(insertHistoryItem("https://www.mozilla.org", "testGuid"));
+
+ Long time1 = System.currentTimeMillis();
+ Long time2 = time1 + 100;
+ Long time3 = time1 + 200;
+
+ ContentValues visitItem = new ContentValues();
+ visitItem.put(Visits.DATE_VISITED, time1);
+ visitItem.put(Visits.HISTORY_GUID, "testGuid");
+ assertNotNull(visitsClient.insert(visitsTestUri, visitItem));
+
+ visitItem.put(Visits.DATE_VISITED, time3);
+ assertNotNull(visitsClient.insert(visitsTestUri, visitItem));
+
+ visitItem.put(Visits.DATE_VISITED, time2);
+ assertNotNull(visitsClient.insert(visitsTestUri, visitItem));
+
+ Cursor cursor = visitsClient.query(visitsTestUri, null, null, null, null);
+ assertNotNull(cursor);
+ try {
+ assertEquals(3, cursor.getCount());
+ assertTrue(cursor.moveToFirst());
+
+ Long timeInserted = cursor.getLong(cursor.getColumnIndex(Visits.DATE_VISITED));
+ assertEquals(time3, timeInserted);
+
+ cursor.moveToNext();
+
+ timeInserted = cursor.getLong(cursor.getColumnIndex(Visits.DATE_VISITED));
+ assertEquals(time2, timeInserted);
+
+ cursor.moveToNext();
+
+ timeInserted = cursor.getLong(cursor.getColumnIndex(Visits.DATE_VISITED));
+ assertEquals(time1, timeInserted);
+ } finally {
+ cursor.close();
+ }
+ }
+
+ @Test
+ /**
+ * Test that if we pass sorting params, they're not overridden
+ */
+ public void testNonDefaultSortingOrder() throws RemoteException {
+ assertNotNull(insertHistoryItem("https://www.mozilla.org", "testGuid"));
+
+ Long time1 = System.currentTimeMillis();
+ Long time2 = time1 + 100;
+ Long time3 = time1 + 200;
+
+ ContentValues visitItem = new ContentValues();
+ visitItem.put(Visits.DATE_VISITED, time1);
+ visitItem.put(Visits.HISTORY_GUID, "testGuid");
+ assertNotNull(visitsClient.insert(visitsTestUri, visitItem));
+
+ visitItem.put(Visits.DATE_VISITED, time3);
+ assertNotNull(visitsClient.insert(visitsTestUri, visitItem));
+
+ visitItem.put(Visits.DATE_VISITED, time2);
+ assertNotNull(visitsClient.insert(visitsTestUri, visitItem));
+
+ Cursor cursor = visitsClient.query(visitsTestUri, null, null, null, Visits.DATE_VISITED + " ASC");
+ assertNotNull(cursor);
+ assertEquals(3, cursor.getCount());
+ assertTrue(cursor.moveToFirst());
+
+ Long timeInserted = cursor.getLong(cursor.getColumnIndex(Visits.DATE_VISITED));
+ assertEquals(time1, timeInserted);
+
+ cursor.moveToNext();
+
+ timeInserted = cursor.getLong(cursor.getColumnIndex(Visits.DATE_VISITED));
+ assertEquals(time2, timeInserted);
+
+ cursor.moveToNext();
+
+ timeInserted = cursor.getLong(cursor.getColumnIndex(Visits.DATE_VISITED));
+ assertEquals(time3, timeInserted);
+
+ cursor.close();
+ }
+
+ @Test
+ /**
+ * Tests deletion of all visits, and by some selection (GUID, IS_LOCAL)
+ */
+ public void testVisitDeletion() throws RemoteException {
+ assertNotNull(insertHistoryItem("https://www.mozilla.org", "testGuid"));
+ assertNotNull(insertHistoryItem("https://www.eff.org", "testGuid2"));
+
+ Long time1 = System.currentTimeMillis();
+
+ ContentValues visitItem = new ContentValues();
+ visitItem.put(Visits.DATE_VISITED, time1);
+ visitItem.put(Visits.HISTORY_GUID, "testGuid");
+ assertNotNull(visitsClient.insert(visitsTestUri, visitItem));
+
+ visitItem = new ContentValues();
+ visitItem.put(Visits.DATE_VISITED, time1 + 100);
+ visitItem.put(Visits.HISTORY_GUID, "testGuid");
+ assertNotNull(visitsClient.insert(visitsTestUri, visitItem));
+
+ ContentValues visitItem2 = new ContentValues();
+ visitItem2.put(Visits.DATE_VISITED, time1);
+ visitItem2.put(Visits.HISTORY_GUID, "testGuid2");
+ assertNotNull(visitsClient.insert(visitsTestUri, visitItem2));
+
+ Cursor cursor = visitsClient.query(visitsTestUri, null, null, null, null);
+ assertNotNull(cursor);
+ assertEquals(3, cursor.getCount());
+ cursor.close();
+
+ assertEquals(3, visitsClient.delete(visitsTestUri, null, null));
+
+ cursor = visitsClient.query(visitsTestUri, null, null, null, null);
+ assertNotNull(cursor);
+ assertEquals(0, cursor.getCount());
+ cursor.close();
+
+ // test selective deletion - by IS_LOCAL
+ visitItem = new ContentValues();
+ visitItem.put(Visits.DATE_VISITED, time1);
+ visitItem.put(Visits.HISTORY_GUID, "testGuid");
+ visitItem.put(Visits.IS_LOCAL, 0);
+ assertNotNull(visitsClient.insert(visitsTestUri, visitItem));
+
+ visitItem = new ContentValues();
+ visitItem.put(Visits.DATE_VISITED, time1 + 100);
+ visitItem.put(Visits.HISTORY_GUID, "testGuid");
+ visitItem.put(Visits.IS_LOCAL, 1);
+ assertNotNull(visitsClient.insert(visitsTestUri, visitItem));
+
+ visitItem2 = new ContentValues();
+ visitItem2.put(Visits.DATE_VISITED, time1);
+ visitItem2.put(Visits.HISTORY_GUID, "testGuid2");
+ visitItem2.put(Visits.IS_LOCAL, 0);
+ assertNotNull(visitsClient.insert(visitsTestUri, visitItem2));
+
+ cursor = visitsClient.query(visitsTestUri, null, null, null, null);
+ assertNotNull(cursor);
+ assertEquals(3, cursor.getCount());
+ cursor.close();
+
+ assertEquals(2,
+ visitsClient.delete(visitsTestUri, Visits.IS_LOCAL + " = ?", new String[]{"0"}));
+ cursor = visitsClient.query(visitsTestUri, null, null, null, null);
+ assertNotNull(cursor);
+ assertEquals(1, cursor.getCount());
+ assertTrue(cursor.moveToFirst());
+ assertEquals(time1 + 100, cursor.getLong(cursor.getColumnIndex(Visits.DATE_VISITED)));
+ assertEquals("testGuid", cursor.getString(cursor.getColumnIndex(Visits.HISTORY_GUID)));
+ assertEquals(1, cursor.getInt(cursor.getColumnIndex(Visits.IS_LOCAL)));
+ cursor.close();
+
+ // test selective deletion - by HISTORY_GUID
+ assertNotNull(visitsClient.insert(visitsTestUri, visitItem2));
+ cursor = visitsClient.query(visitsTestUri, null, null, null, null);
+ assertNotNull(cursor);
+ assertEquals(2, cursor.getCount());
+ cursor.close();
+
+ assertEquals(1,
+ visitsClient.delete(visitsTestUri, Visits.HISTORY_GUID + " = ?", new String[]{"testGuid"}));
+ cursor = visitsClient.query(visitsTestUri, null, null, null, null);
+ assertNotNull(cursor);
+ assertEquals(1, cursor.getCount());
+ assertTrue(cursor.moveToFirst());
+ assertEquals("testGuid2", cursor.getString(cursor.getColumnIndex(Visits.HISTORY_GUID)));
+ cursor.close();
+ }
+}
\ No newline at end of file
--- a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBrowserProvider.java
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBrowserProvider.java
@@ -1319,34 +1319,37 @@ public class testBrowserProvider extends
dateCreated = c.getLong(c.getColumnIndex(BrowserContract.History.DATE_CREATED));
dateModified = c.getLong(c.getColumnIndex(BrowserContract.History.DATE_MODIFIED));
mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.History.VISITS)), 10L,
"Inserted history entry has correct specified number of visits");
mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.TITLE)), TEST_TITLE,
"Inserted history entry has correct specified title");
- // Update the history entry, specifying additional visit count
+ // Update the history entry, specifying additional visit count.
+ // The expectation is that the value is ignored, and count is bumped by 1 only.
+ // At the same time, a visit is inserted into the visits table.
+ // See junit4 tests in BrowserProviderHistoryVisitsTest.
values = new ContentValues();
values.put(BrowserContract.History.VISITS, 10);
updated = mProvider.update(updateOrInsertHistoryUri, values,
BrowserContract.History._ID + " = ?",
new String[] { String.valueOf(id) });
mAsserter.is((updated == 1), true, "Inserted history entry was updated");
c.close();
c = getHistoryEntryById(id);
mAsserter.is(c.moveToFirst(), true, "Updated history entry found");
mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.TITLE)), TEST_TITLE,
"Updated history entry has correct unchanged title");
mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.URL)), TEST_URL_2,
"Updated history entry has correct unchanged URL");
- mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.History.VISITS)), 20L,
+ mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.History.VISITS)), 11L,
"Updated history entry has correct number of visits");
mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.History.DATE_CREATED)), dateCreated,
"Updated history entry has same creation date");
mAsserter.isnot(c.getLong(c.getColumnIndex(BrowserContract.History.DATE_MODIFIED)), dateModified,
"Updated history entry has new modification date");
c.close();
}