Bug 1046709 - Part 2: CRUD for Visits - query/insert/delete; tests. r=nalexander,rnewman

Note: need to set package name in robolectric.properties so that Robolectric reads correct resources

MozReview-Commit-ID: 6wrh8kzJlXI

--HG--
extra : transplant_source : %86T%8BUB%ABe%0A%DF8%F0%81%0C%ACi%D1Rx%E2%EC
This commit is contained in:
Grigory Kruglov 2016-04-16 02:19:53 -07:00
parent 1ab053d2cf
commit 1dab7ae855
7 changed files with 878 additions and 17 deletions

View File

@ -8,6 +8,7 @@ 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;
@ -18,6 +19,7 @@ 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;
@ -63,6 +65,7 @@ public class BrowserProvider extends SharedBrowserDatabaseProvider {
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;
@ -110,11 +113,14 @@ public class BrowserProvider extends SharedBrowserDatabaseProvider {
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);
@ -125,6 +131,7 @@ public class BrowserProvider extends SharedBrowserDatabaseProvider {
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 {
@ -181,6 +188,17 @@ public class BrowserProvider extends SharedBrowserDatabaseProvider {
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);
@ -423,11 +441,27 @@ public class BrowserProvider extends SharedBrowserDatabaseProvider {
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;
@ -512,6 +546,12 @@ public class BrowserProvider extends SharedBrowserDatabaseProvider {
break;
}
case VISITS: {
trace("Insert on VISITS: " + uri);
id = insertVisit(uri, values);
break;
}
case FAVICONS: {
trace("Insert on FAVICONS: " + uri);
id = insertFavicon(uri, values);
@ -617,6 +657,9 @@ public class BrowserProvider extends SharedBrowserDatabaseProvider {
} else {
updated = updateHistory(uri, values, selection, selectionArgs);
}
if (shouldIncrementVisits(uri)) {
insertVisitForHistory(uri, values, selection, selectionArgs);
}
break;
}
@ -1039,6 +1082,16 @@ public class BrowserProvider extends SharedBrowserDatabaseProvider {
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,
@ -1364,32 +1417,85 @@ public class BrowserProvider extends SharedBrowserDatabaseProvider {
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);
@ -1625,6 +1731,61 @@ public class BrowserProvider extends SharedBrowserDatabaseProvider {
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);

View File

@ -27,6 +27,8 @@ 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;
}

View File

@ -33,6 +33,7 @@ public class BrowserContractHelpers extends BrowserContract {
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);

View File

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

View File

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

View File

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

View File

@ -1324,7 +1324,10 @@ public class testBrowserProvider extends ContentProviderTest {
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);
@ -1341,7 +1344,7 @@ public class testBrowserProvider extends ContentProviderTest {
"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");