--- a/mobile/android/base/java/org/mozilla/gecko/db/BrowserContract.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/BrowserContract.java
@@ -45,16 +45,17 @@ public class BrowserContract {
public static final String PARAM_PROFILE = "profile";
public static final String PARAM_PROFILE_PATH = "profilePath";
public static final String PARAM_LIMIT = "limit";
public static final String PARAM_SUGGESTEDSITES_LIMIT = "suggestedsites_limit";
public static final String PARAM_TOPSITES_EXCLUDE_REMOTE_ONLY = "topsites_exclude_remote_only";
public static final String PARAM_IS_SYNC = "sync";
public static final String PARAM_SHOW_DELETED = "show_deleted";
public static final String PARAM_IS_TEST = "test";
+ public static final String PARAM_OLD_BOOKMARK_PARENT = "old_bookmark_parent";
public static final String PARAM_INSERT_IF_NEEDED = "insert_if_needed";
public static final String PARAM_INCREMENT_VISITS = "increment_visits";
public static final String PARAM_INCREMENT_REMOTE_AGGREGATES = "increment_remote_aggregates";
public static final String PARAM_NON_POSITIONED_PINS = "non_positioned_pins";
public static final String PARAM_EXPIRE_PRIORITY = "priority";
public static final String PARAM_DATASET_ID = "dataset_id";
public static final String PARAM_GROUP_BY = "group_by";
--- a/mobile/android/base/java/org/mozilla/gecko/db/BrowserDB.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/BrowserDB.java
@@ -17,16 +17,17 @@ import org.mozilla.gecko.icons.decoders.
import android.content.ContentProviderClient;
import android.content.ContentProviderOperation;
import android.content.ContentResolver;
import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import android.graphics.drawable.BitmapDrawable;
+import android.net.Uri;
import android.support.v4.content.CursorLoader;
/**
* Interface for interactions with all databases. If you want an instance
* that implements this, you should go through GeckoProfile. E.g.,
* <code>BrowserDB.from(context)</code>.
*/
public abstract class BrowserDB {
@@ -101,21 +102,25 @@ public abstract class BrowserDB {
public abstract void clearHistory(ContentResolver cr, boolean clearSearchHistory);
public abstract String getUrlForKeyword(ContentResolver cr, String keyword);
public abstract boolean isBookmark(ContentResolver cr, String uri);
public abstract boolean addBookmark(ContentResolver cr, String title, String uri);
+ public abstract Uri addBookmarkFolder(ContentResolver cr, String title, long parentId);
public abstract Cursor getBookmarkForUrl(ContentResolver cr, String url);
public abstract Cursor getBookmarksForPartialUrl(ContentResolver cr, String partialUrl);
+ public abstract Cursor getBookmarkById(ContentResolver cr, long id);
public abstract void removeBookmarksWithURL(ContentResolver cr, String uri);
+ public abstract void removeBookmarkWithId(ContentResolver cr, long id);
public abstract void registerBookmarkObserver(ContentResolver cr, ContentObserver observer);
- public abstract void updateBookmark(ContentResolver cr, int id, String uri, String title, String keyword);
+ public abstract void updateBookmark(ContentResolver cr, long id, String uri, String title, String keyword);
+ public abstract void updateBookmark(ContentResolver cr, long id, String uri, String title, String keyword, long newParentId, long oldParentId);
public abstract boolean hasBookmarkWithGuid(ContentResolver cr, String guid);
public abstract boolean insertPageMetadata(ContentProviderClient contentProviderClient, String pageUrl, boolean hasImage, String metadataJSON);
public abstract int deletePageMetadata(ContentProviderClient contentProviderClient, String pageUrl);
/**
* Can return <code>null</code>.
*/
public abstract Cursor getBookmarksInFolder(ContentResolver cr, long folderId);
--- a/mobile/android/base/java/org/mozilla/gecko/db/BrowserProvider.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/BrowserProvider.java
@@ -10,33 +10,31 @@ 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.activitystream.ranking.HighlightsRanking;
import org.mozilla.gecko.db.BrowserContract.ActivityStreamBlocklist;
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.Highlights;
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.BrowserContract.PageMetadata;
import org.mozilla.gecko.db.DBUtils.UpdateOperation;
-import org.mozilla.gecko.home.activitystream.model.Highlight;
import org.mozilla.gecko.icons.IconsHelper;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.repositories.android.BrowserContractHelpers;
import org.mozilla.gecko.util.ThreadUtils;
import android.content.BroadcastReceiver;
import android.content.ContentProviderOperation;
import android.content.ContentProviderResult;
@@ -45,17 +43,16 @@ import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.OperationApplicationException;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.MatrixCursor;
-import android.database.MergeCursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteConstraintException;
import android.database.sqlite.SQLiteCursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.database.sqlite.SQLiteStatement;
import android.net.Uri;
import android.os.Bundle;
@@ -1500,17 +1497,22 @@ public class BrowserProvider extends Sha
return processCount;
}
/**
* Construct an update expression that will modify the parents of any records
* that match.
*/
private int updateBookmarkParents(SQLiteDatabase db, ContentValues values, String selection, String[] selectionArgs) {
- trace("Updating bookmark parents of " + selection + " (" + selectionArgs[0] + ")");
+ if (selectionArgs != null) {
+ trace("Updating bookmark parents of " + selection + " (" + selectionArgs[0] + ")");
+ } else {
+ trace("Updating bookmark parents of " + selection);
+ }
+
String where = Bookmarks._ID + " IN (" +
" SELECT DISTINCT " + Bookmarks.PARENT +
" FROM " + TABLE_BOOKMARKS +
" WHERE " + selection + " )";
return db.update(TABLE_BOOKMARKS, values, where, selectionArgs);
}
private long insertBookmark(Uri uri, ContentValues values) {
@@ -1541,17 +1543,42 @@ public class BrowserProvider extends Sha
values.put(Bookmarks.TITLE, "");
}
String url = values.getAsString(Bookmarks.URL);
debug("Inserting bookmark in database with URL: " + url);
final SQLiteDatabase db = getWritableDatabase(uri);
beginWrite(db);
- return db.insertOrThrow(TABLE_BOOKMARKS, Bookmarks.TITLE, values);
+ final long insertedId = db.insertOrThrow(TABLE_BOOKMARKS, Bookmarks.TITLE, values);
+
+ if (insertedId == -1) {
+ Log.e(LOGTAG, "Unable to insert bookmark in database with URL: " + url);
+ return insertedId;
+ }
+
+ if (isCallerSync(uri)) {
+ // Sync will handle timestamps on its own, so we don't perform the update here.
+ return insertedId;
+ }
+
+ // Bump parent's lastModified timestamp.
+ final long lastModified = values.getAsLong(Bookmarks.DATE_MODIFIED);
+ final ContentValues parentValues = new ContentValues();
+ parentValues.put(Bookmarks.DATE_MODIFIED, lastModified);
+
+ // The ContentValues should have parentId, or the insertion above would fail because of
+ // database schema foreign key constraint.
+ final long parentId = values.getAsLong(Bookmarks.PARENT);
+ db.update(TABLE_BOOKMARKS,
+ parentValues,
+ Bookmarks._ID + " = ?",
+ new String[] { String.valueOf(parentId) });
+
+ return insertedId;
}
private int updateOrInsertBookmark(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
int updated = updateBookmarks(uri, values, selection, selectionArgs);
if (updated > 0) {
return updated;
@@ -1590,17 +1617,63 @@ public class BrowserProvider extends Sha
final String inClause;
try {
inClause = DBUtils.computeSQLInClauseFromLongs(cursor, Bookmarks._ID);
} finally {
cursor.close();
}
beginWrite(db);
- return db.update(TABLE_BOOKMARKS, values, inClause, null);
+
+ final int updated = db.update(TABLE_BOOKMARKS, values, inClause, null);
+ if (updated == 0) {
+ trace("No update on URI: " + uri);
+ return updated;
+ }
+
+ if (isCallerSync(uri)) {
+ // Sync will handle timestamps on its own, so we don't perform the update here.
+ return updated;
+ }
+
+ final long oldParentId = getOldParentIdIfParentChanged(uri);
+ if (oldParentId == -1) {
+ // Parent isn't changed, don't bump its timestamps.
+ return updated;
+ }
+
+ final long newParentId = values.getAsLong(Bookmarks.PARENT);
+ final long lastModified = values.getAsLong(Bookmarks.DATE_MODIFIED);
+ final ContentValues parentValues = new ContentValues();
+ parentValues.put(Bookmarks.DATE_MODIFIED, lastModified);
+
+ // Bump old/new parent's lastModified timestamps.
+ db.update(TABLE_BOOKMARKS, parentValues,
+ Bookmarks._ID + " in (?, ?)",
+ new String[] { String.valueOf(oldParentId), String.valueOf(newParentId) });
+
+ return updated;
+ }
+
+ /**
+ * Use the query key {@link BrowserContract#PARAM_OLD_BOOKMARK_PARENT} to check if parent is changed or not.
+ *
+ * @return old parent id if uri has the key, or -1 otherwise.
+ */
+ private long getOldParentIdIfParentChanged(Uri uri) {
+ final String oldParentId = uri.getQueryParameter(BrowserContract.PARAM_OLD_BOOKMARK_PARENT);
+ if (TextUtils.isEmpty(oldParentId)) {
+ return -1;
+ }
+
+ try {
+ return Long.parseLong(oldParentId);
+ } catch (NumberFormatException ignored) {
+ return -1;
+ }
}
private long insertHistory(Uri uri, ContentValues values) {
final long now = System.currentTimeMillis();
values.put(History.DATE_CREATED, now);
values.put(History.DATE_MODIFIED, now);
// Generate GUID for new history entry. Don't override specified GUIDs.
@@ -2093,25 +2166,30 @@ public class BrowserProvider extends Sha
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);
+ beginWrite(db);
if (isCallerSync(uri)) {
- beginWrite(db);
return db.delete(TABLE_BOOKMARKS, selection, selectionArgs);
}
debug("Marking bookmarks as deleted for URI: " + uri);
- ContentValues values = new ContentValues();
+ // Bump parent's lastModified timestamp before record deleted.
+ final ContentValues parentValues = new ContentValues();
+ parentValues.put(Bookmarks.DATE_MODIFIED, System.currentTimeMillis());
+ updateBookmarkParents(db, parentValues, selection, selectionArgs);
+
+ final ContentValues values = new ContentValues();
values.put(Bookmarks.IS_DELETED, 1);
values.put(Bookmarks.POSITION, 0);
values.putNull(Bookmarks.PARENT);
values.putNull(Bookmarks.URL);
values.putNull(Bookmarks.TITLE);
values.putNull(Bookmarks.DESCRIPTION);
values.putNull(Bookmarks.KEYWORD);
values.putNull(Bookmarks.TAGS);
--- a/mobile/android/base/java/org/mozilla/gecko/db/LocalBrowserDB.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/LocalBrowserDB.java
@@ -31,17 +31,16 @@ import org.mozilla.gecko.db.BrowserContr
import org.mozilla.gecko.db.BrowserContract.Bookmarks;
import org.mozilla.gecko.db.BrowserContract.Combined;
import org.mozilla.gecko.db.BrowserContract.ExpirePriority;
import org.mozilla.gecko.db.BrowserContract.Favicons;
import org.mozilla.gecko.db.BrowserContract.History;
import org.mozilla.gecko.db.BrowserContract.SyncColumns;
import org.mozilla.gecko.db.BrowserContract.Thumbnails;
import org.mozilla.gecko.db.BrowserContract.TopSites;
-import org.mozilla.gecko.db.BrowserContract.Highlights;
import org.mozilla.gecko.db.BrowserContract.PageMetadata;
import org.mozilla.gecko.distribution.Distribution;
import org.mozilla.gecko.icons.decoders.FaviconDecoder;
import org.mozilla.gecko.icons.decoders.LoadFaviconResult;
import org.mozilla.gecko.gfx.BitmapUtils;
import org.mozilla.gecko.restrictions.Restrictions;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.util.GeckoJarReader;
@@ -1116,30 +1115,16 @@ public class LocalBrowserDB extends Brow
final long id = c.getLong(col);
mFolderIdMap.put(guid, id);
return id;
} finally {
c.close();
}
}
- /**
- * Find parents of records that match the provided criteria, and bump their
- * modified timestamp.
- */
- protected void bumpParents(ContentResolver cr, String param, String value) {
- ContentValues values = new ContentValues();
- values.put(Bookmarks.DATE_MODIFIED, System.currentTimeMillis());
-
- String where = param + " = ?";
- String[] args = new String[] { value };
- int updated = cr.update(mParentsUriWithProfile, values, where, args);
- debug("Updated " + updated + " rows to new modified time.");
- }
-
private void addBookmarkItem(ContentResolver cr, String title, String uri, long folderId) {
final long now = System.currentTimeMillis();
ContentValues values = new ContentValues();
if (title != null) {
values.put(Bookmarks.TITLE, title);
}
values.put(Bookmarks.URL, uri);
@@ -1170,42 +1155,32 @@ public class LocalBrowserDB extends Brow
.appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true")
.build();
cr.update(bookmarksWithInsert,
values,
Bookmarks.URL + " = ? AND " +
Bookmarks.PARENT + " = " + folderId,
new String[] { uri });
- // Bump parent modified time using its ID.
- debug("Bumping parent modified time for addition to: " + folderId);
- final String where = Bookmarks._ID + " = ?";
- final String[] args = new String[] { String.valueOf(folderId) };
-
- ContentValues bumped = new ContentValues();
- bumped.put(Bookmarks.DATE_MODIFIED, now);
-
- final int updated = cr.update(mBookmarksUriWithProfile, bumped, where, args);
- debug("Updated " + updated + " rows to new modified time.");
+ // BrowserProvider will handle updating parent's lastModified timestamp, nothing else to do.
}
@Override
@RobocopTarget
public boolean addBookmark(ContentResolver cr, String title, String uri) {
long folderId = getFolderIdFromGuid(cr, Bookmarks.MOBILE_FOLDER_GUID);
if (isBookmarkForUrlInFolder(cr, uri, folderId)) {
// Bookmark added already.
return false;
}
// Add a new bookmark.
addBookmarkItem(cr, title, uri, folderId);
return true;
}
-
private boolean isBookmarkForUrlInFolder(ContentResolver cr, String uri, long folderId) {
final Cursor c = cr.query(bookmarksUriWithLimit(1),
new String[] { Bookmarks._ID },
Bookmarks.URL + " = ? AND " + Bookmarks.PARENT + " = ? AND " + Bookmarks.IS_DELETED + " == 0",
new String[] { uri, String.valueOf(folderId) },
Bookmarks.URL);
if (c == null) {
@@ -1215,50 +1190,88 @@ public class LocalBrowserDB extends Brow
try {
return c.getCount() > 0;
} finally {
c.close();
}
}
@Override
+ public Uri addBookmarkFolder(ContentResolver cr, String title, long parentId) {
+ final ContentValues values = new ContentValues();
+ final long now = System.currentTimeMillis();
+ values.put(Bookmarks.DATE_CREATED, now);
+ values.put(Bookmarks.DATE_MODIFIED, now);
+ values.put(Bookmarks.GUID, Utils.generateGuid());
+ values.put(Bookmarks.PARENT, parentId);
+ values.put(Bookmarks.TITLE, title);
+ values.put(Bookmarks.TYPE, Bookmarks.TYPE_FOLDER);
+
+ // BrowserProvider will bump parent's lastModified timestamp after successful insertion.
+ return cr.insert(mBookmarksUriWithProfile, values);
+ }
+
+ @Override
@RobocopTarget
public void removeBookmarksWithURL(ContentResolver cr, String uri) {
- Uri contentUri = mBookmarksUriWithProfile;
-
- // Do this now so that the items still exist!
- bumpParents(cr, Bookmarks.URL, uri);
+ // BrowserProvider will bump parent's lastModified timestamp after successful deletion.
+ cr.delete(mBookmarksUriWithProfile,
+ Bookmarks.URL + " = ? AND " + Bookmarks.PARENT + " != ? ",
+ new String[] { uri, String.valueOf(Bookmarks.FIXED_PINNED_LIST_ID) });
+ }
- final String[] urlArgs = new String[] { uri, String.valueOf(Bookmarks.FIXED_PINNED_LIST_ID) };
- final String urlEquals = Bookmarks.URL + " = ? AND " + Bookmarks.PARENT + " != ? ";
-
- cr.delete(contentUri, urlEquals, urlArgs);
+ @Override
+ public void removeBookmarkWithId(ContentResolver cr, long id) {
+ // BrowserProvider will bump parent's lastModified timestamp after successful deletion.
+ cr.delete(mBookmarksUriWithProfile,
+ Bookmarks._ID + " = ? AND " + Bookmarks.PARENT + " != ? ",
+ new String[] { String.valueOf(id), String.valueOf(Bookmarks.FIXED_PINNED_LIST_ID) });
}
@Override
public void registerBookmarkObserver(ContentResolver cr, ContentObserver observer) {
cr.registerContentObserver(mBookmarksUriWithProfile, false, observer);
}
@Override
@RobocopTarget
- public void updateBookmark(ContentResolver cr, int id, String uri, String title, String keyword) {
+ public void updateBookmark(ContentResolver cr, long id, String uri, String title, String keyword) {
ContentValues values = new ContentValues();
values.put(Bookmarks.TITLE, title);
values.put(Bookmarks.URL, uri);
values.put(Bookmarks.KEYWORD, keyword);
values.put(Bookmarks.DATE_MODIFIED, System.currentTimeMillis());
cr.update(mBookmarksUriWithProfile,
values,
Bookmarks._ID + " = ?",
new String[] { String.valueOf(id) });
}
@Override
+ public void updateBookmark(ContentResolver cr, long id, String uri, String title, String keyword,
+ long newParentId, long oldParentId) {
+ final ContentValues values = new ContentValues();
+ values.put(Bookmarks.TITLE, title);
+ values.put(Bookmarks.URL, uri);
+ values.put(Bookmarks.KEYWORD, keyword);
+ values.put(Bookmarks.PARENT, newParentId);
+ values.put(Bookmarks.DATE_MODIFIED, System.currentTimeMillis());
+
+ final Uri contentUri = mBookmarksUriWithProfile.buildUpon()
+ .appendQueryParameter(BrowserContract.PARAM_OLD_BOOKMARK_PARENT,
+ String.valueOf(oldParentId))
+ .build();
+ cr.update(contentUri,
+ values,
+ Bookmarks._ID + " = ?",
+ new String[] { String.valueOf(id) });
+ }
+
+ @Override
public boolean hasBookmarkWithGuid(ContentResolver cr, String guid) {
Cursor c = cr.query(bookmarksUriWithLimit(1),
new String[] { Bookmarks.GUID },
Bookmarks.GUID + " = ?",
new String[] { guid },
null);
try {
@@ -1780,30 +1793,55 @@ public class LocalBrowserDB extends Brow
@Override
@RobocopTarget
public Cursor getBookmarkForUrl(ContentResolver cr, String url) {
Cursor c = cr.query(bookmarksUriWithLimit(1),
new String[] { Bookmarks._ID,
Bookmarks.URL,
Bookmarks.TITLE,
+ Bookmarks.TYPE,
+ Bookmarks.PARENT,
+ Bookmarks.GUID,
Bookmarks.KEYWORD },
Bookmarks.URL + " = ?",
new String[] { url },
null);
if (c != null && c.getCount() == 0) {
c.close();
c = null;
}
return c;
}
@Override
+ public Cursor getBookmarkById(ContentResolver cr, long id) {
+ final Cursor c = cr.query(mBookmarksUriWithProfile,
+ new String[] { Bookmarks._ID,
+ Bookmarks.URL,
+ Bookmarks.TITLE,
+ Bookmarks.TYPE,
+ Bookmarks.PARENT,
+ Bookmarks.GUID,
+ Bookmarks.KEYWORD },
+ Bookmarks._ID + " = ?",
+ new String[] { String.valueOf(id) },
+ null);
+
+ if (c != null && c.getCount() == 0) {
+ c.close();
+ return null;
+ }
+
+ return c;
+ }
+
+ @Override
public Cursor getBookmarksForPartialUrl(ContentResolver cr, String partialUrl) {
Cursor c = cr.query(mBookmarksUriWithProfile,
new String[] { Bookmarks.GUID, Bookmarks._ID, Bookmarks.URL },
Bookmarks.URL + " LIKE '%" + partialUrl + "%'", // TODO: Escaping!
null,
null);
if (c != null && c.getCount() == 0) {
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/LocalBrowserDBTest.java
@@ -0,0 +1,345 @@
+/* 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.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.RemoteException;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.db.DelegatingTestContentProvider;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.repositories.android.BrowserContractHelpers;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.shadows.ShadowContentResolver;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(TestRunner.class)
+public class LocalBrowserDBTest {
+ private static final long INVALID_ID = -1;
+ private final String BOOKMARK_URL = "https://www.mozilla.org";
+ private final String BOOKMARK_TITLE = "mozilla";
+
+ private final String UPDATE_URL = "https://bugzilla.mozilla.org";
+ private final String UPDATE_TITLE = "bugzilla";
+
+ private final String FOLDER_NAME = "folder1";
+
+ private Context context;
+ private BrowserProvider provider;
+ private ContentProviderClient bookmarkClient;
+
+ @Before
+ public void setUp() throws Exception {
+ context = RuntimeEnvironment.application;
+ provider = new BrowserProvider();
+ provider.onCreate();
+ ShadowContentResolver.registerProvider(BrowserContract.AUTHORITY, new DelegatingTestContentProvider(provider));
+
+ ShadowContentResolver contentResolver = new ShadowContentResolver();
+ bookmarkClient = contentResolver.acquireContentProviderClient(BrowserContractHelpers.BOOKMARKS_CONTENT_URI);
+ }
+
+ @After
+ public void tearDown() {
+ bookmarkClient.release();
+ provider.shutdown();
+ }
+
+ @Test
+ public void testRemoveBookmarkWithURL() {
+ BrowserDB db = new LocalBrowserDB("default");
+ ContentResolver cr = context.getContentResolver();
+
+ db.addBookmark(cr, BOOKMARK_TITLE, BOOKMARK_URL);
+ Cursor cursor = db.getBookmarkForUrl(cr, BOOKMARK_URL);
+ assertNotNull(cursor);
+
+ long parentId = INVALID_ID;
+ try {
+ assertTrue(cursor.moveToFirst());
+
+ final String title = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Bookmarks.TITLE));
+ assertEquals(title, BOOKMARK_TITLE);
+
+ final String url = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Bookmarks.URL));
+ assertEquals(url, BOOKMARK_URL);
+
+ parentId = cursor.getLong(cursor.getColumnIndexOrThrow(BrowserContract.Bookmarks.PARENT));
+ } finally {
+ cursor.close();
+ }
+ assertNotEquals(parentId, INVALID_ID);
+
+ final long lastModifiedBeforeRemove = getModifiedDate(parentId);
+
+ // Remove bookmark record
+ db.removeBookmarksWithURL(cr, BOOKMARK_URL);
+
+ // Check the record has been removed
+ cursor = db.getBookmarkForUrl(cr, BOOKMARK_URL);
+ assertNull(cursor);
+
+ // Check parent's lastModified timestamp is updated
+ final long lastModifiedAfterRemove = getModifiedDate(parentId);
+ assertTrue(lastModifiedAfterRemove > lastModifiedBeforeRemove);
+ }
+
+ @Test
+ public void testRemoveBookmarkWithId() {
+ BrowserDB db = new LocalBrowserDB("default");
+ ContentResolver cr = context.getContentResolver();
+
+ db.addBookmark(cr, BOOKMARK_TITLE, BOOKMARK_URL);
+ Cursor cursor = db.getBookmarkForUrl(cr, BOOKMARK_URL);
+ assertNotNull(cursor);
+
+ long bookmarkId = INVALID_ID;
+ long parentId = INVALID_ID;
+ try {
+ assertTrue(cursor.moveToFirst());
+
+ bookmarkId = cursor.getLong(cursor.getColumnIndexOrThrow(BrowserContract.Bookmarks._ID));
+ parentId = cursor.getLong(cursor.getColumnIndexOrThrow(BrowserContract.Bookmarks.PARENT));
+ } finally {
+ cursor.close();
+ }
+ assertNotEquals(bookmarkId, INVALID_ID);
+ assertNotEquals(parentId, INVALID_ID);
+
+ final long lastModifiedBeforeRemove = getModifiedDate(parentId);
+
+ // Remove bookmark record
+ db.removeBookmarkWithId(cr, bookmarkId);
+
+ cursor = db.getBookmarkForUrl(cr, BOOKMARK_URL);
+ assertNull(cursor);
+
+ // Check parent's lastModified timestamp is updated
+ final long lastModifiedAfterRemove = getModifiedDate(parentId);
+ assertTrue(lastModifiedAfterRemove > lastModifiedBeforeRemove);
+ }
+
+ @Test
+ public void testUpdateBookmark() throws Exception {
+ BrowserDB db = new LocalBrowserDB("default");
+ ContentResolver cr = context.getContentResolver();
+
+ db.addBookmark(cr, BOOKMARK_TITLE, BOOKMARK_URL);
+ Cursor cursor = db.getBookmarkForUrl(cr, BOOKMARK_URL);
+ assertNotNull(cursor);
+
+ long bookmarkId = INVALID_ID;
+ long parentId = getBookmarkIdFromGuid(BrowserContract.Bookmarks.MOBILE_FOLDER_GUID);
+ try {
+ assertTrue(cursor.moveToFirst());
+
+ final String insertedUrl = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Bookmarks.URL));
+ assertEquals(insertedUrl, BOOKMARK_URL);
+
+ final String insertedTitle = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Bookmarks.TITLE));
+ assertEquals(insertedTitle, BOOKMARK_TITLE);
+
+ bookmarkId = cursor.getLong(cursor.getColumnIndexOrThrow(BrowserContract.Bookmarks._ID));
+ } finally {
+ cursor.close();
+ }
+ assertNotEquals(bookmarkId, INVALID_ID);
+
+ final long parentLastModifiedBeforeUpdate = getModifiedDate(parentId);
+
+ // Update bookmark record
+ db.updateBookmark(cr, bookmarkId, UPDATE_URL, UPDATE_TITLE, "");
+ cursor = db.getBookmarkById(cr, bookmarkId);
+ assertNotNull(cursor);
+ try {
+ assertTrue(cursor.moveToFirst());
+
+ final String updatedUrl = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Bookmarks.URL));
+ assertEquals(updatedUrl, UPDATE_URL);
+
+ final String updatedTitle = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Bookmarks.TITLE));
+ assertEquals(updatedTitle, UPDATE_TITLE);
+ } finally {
+ cursor.close();
+ }
+
+ // Check parent's lastModified timestamp isn't changed
+ final long parentLastModifiedAfterUpdate = getModifiedDate(parentId);
+ assertTrue(parentLastModifiedAfterUpdate == parentLastModifiedBeforeUpdate);
+ }
+
+ @Test
+ public void testUpdateBookmarkWithParentChange() throws Exception {
+ BrowserDB db = new LocalBrowserDB("default");
+ ContentResolver cr = context.getContentResolver();
+
+ db.addBookmark(cr, BOOKMARK_TITLE, BOOKMARK_URL);
+ Cursor cursor = db.getBookmarkForUrl(cr, BOOKMARK_URL);
+ assertNotNull(cursor);
+
+ long bookmarkId = INVALID_ID;
+ long originalParentId = getBookmarkIdFromGuid(BrowserContract.Bookmarks.MOBILE_FOLDER_GUID);
+ try {
+ assertTrue(cursor.moveToFirst());
+
+ final String insertedUrl = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Bookmarks.URL));
+ assertEquals(insertedUrl, BOOKMARK_URL);
+
+ final String insertedTitle = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Bookmarks.TITLE));
+ assertEquals(insertedTitle, BOOKMARK_TITLE);
+
+ bookmarkId = cursor.getLong(cursor.getColumnIndexOrThrow(BrowserContract.Bookmarks._ID));
+ } finally {
+ cursor.close();
+ }
+ assertNotEquals(bookmarkId, INVALID_ID);
+
+ // Create a folder
+ final Uri newFolderUri = db.addBookmarkFolder(cr, FOLDER_NAME, originalParentId);
+ // Get id from Uri
+ final long newParentId = Long.valueOf(newFolderUri.getLastPathSegment());
+
+ final long originalParentLastModifiedBeforeUpdate = getModifiedDate(originalParentId);
+ final long newParentLastModifiedBeforeUpdate = getModifiedDate(newParentId);
+
+ // Update bookmark record
+ db.updateBookmark(cr, bookmarkId, UPDATE_URL, UPDATE_TITLE, "", newParentId, originalParentId);
+ cursor = db.getBookmarkById(cr, bookmarkId);
+ assertNotNull(cursor);
+ try {
+ assertTrue(cursor.moveToFirst());
+
+ final String updatedUrl = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Bookmarks.URL));
+ assertEquals(updatedUrl, UPDATE_URL);
+
+ final String updatedTitle = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Bookmarks.TITLE));
+ assertEquals(updatedTitle, UPDATE_TITLE);
+
+ final long parentId = cursor.getLong(cursor.getColumnIndex(BrowserContract.Bookmarks.PARENT));
+ assertEquals(parentId, newParentId);
+ } finally {
+ cursor.close();
+ }
+
+ // Check parent's lastModified timestamp
+ final long originalParentLastModifiedAfterUpdate = getModifiedDate(originalParentId);
+ assertTrue(originalParentLastModifiedAfterUpdate > originalParentLastModifiedBeforeUpdate);
+
+ final long newParentLastModifiedAfterUpdate = getModifiedDate(newParentId);
+ assertTrue(newParentLastModifiedAfterUpdate > newParentLastModifiedBeforeUpdate);
+ }
+
+ @Test
+ public void testAddBookmarkFolder() throws Exception {
+ BrowserDB db = new LocalBrowserDB("default");
+ ContentResolver cr = context.getContentResolver();
+
+ // Add a bookmark folder record
+ final long rootFolderId = getBookmarkIdFromGuid(BrowserContract.Bookmarks.MOBILE_FOLDER_GUID);
+ final long lastModifiedBeforeAdd = getModifiedDate(rootFolderId);
+ final Uri folderUri = db.addBookmarkFolder(cr, FOLDER_NAME, rootFolderId);
+ assertNotNull(folderUri);
+
+ // Get id from Uri
+ long folderId = Long.valueOf(folderUri.getLastPathSegment());
+
+ final Cursor cursor = db.getBookmarkById(cr, folderId);
+ assertNotNull(cursor);
+ try {
+ assertTrue(cursor.moveToFirst());
+
+ final String name = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Bookmarks.TITLE));
+ assertEquals(name, FOLDER_NAME);
+
+ final long parent = cursor.getLong(cursor.getColumnIndexOrThrow(BrowserContract.Bookmarks.PARENT));
+ assertEquals(parent, rootFolderId);
+ } finally {
+ cursor.close();
+ }
+
+ // Check parent's lastModified timestamp is updated
+ final long lastModifiedAfterAdd = getModifiedDate(rootFolderId);
+ assertTrue(lastModifiedAfterAdd > lastModifiedBeforeAdd);
+ }
+
+ @Test
+ public void testAddBookmark() throws Exception {
+ BrowserDB db = new LocalBrowserDB("default");
+ ContentResolver cr = context.getContentResolver();
+
+ final long rootFolderId = getBookmarkIdFromGuid(BrowserContract.Bookmarks.MOBILE_FOLDER_GUID);
+ final long lastModifiedBeforeAdd = getModifiedDate(rootFolderId);
+
+ // Add a bookmark
+ db.addBookmark(cr, BOOKMARK_TITLE, BOOKMARK_URL);
+
+ final Cursor cursor = db.getBookmarkForUrl(cr, BOOKMARK_URL);
+ assertNotNull(cursor);
+ try {
+ assertTrue(cursor.moveToFirst());
+
+ final String name = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Bookmarks.TITLE));
+ assertEquals(name, BOOKMARK_TITLE);
+
+ final String url = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Bookmarks.URL));
+ assertEquals(url, BOOKMARK_URL);
+ } finally {
+ cursor.close();
+ }
+
+ // Check parent's lastModified timestamp is updated
+ final long lastModifiedAfterAdd = getModifiedDate(rootFolderId);
+ assertTrue(lastModifiedAfterAdd > lastModifiedBeforeAdd);
+ }
+
+ private long getBookmarkIdFromGuid(String guid) throws RemoteException {
+ Cursor cursor = bookmarkClient.query(BrowserContract.Bookmarks.CONTENT_URI,
+ new String[] { BrowserContract.Bookmarks._ID },
+ BrowserContract.Bookmarks.GUID + " = ?",
+ new String[] { guid },
+ null);
+ assertNotNull(cursor);
+
+ long id = INVALID_ID;
+ try {
+ assertTrue(cursor.moveToFirst());
+ id = cursor.getLong(cursor.getColumnIndexOrThrow(BrowserContract.Bookmarks._ID));
+ } finally {
+ cursor.close();
+ }
+ assertNotEquals(id, INVALID_ID);
+ return id;
+ }
+
+ private long getModifiedDate(long id) {
+ Cursor cursor = provider.query(BrowserContract.Bookmarks.CONTENT_URI,
+ new String[] { BrowserContract.Bookmarks.DATE_MODIFIED },
+ BrowserContract.Bookmarks._ID + " = ?",
+ new String[] { String.valueOf(id) },
+ null);
+ assertNotNull(cursor);
+
+ long modified = -1;
+ try {
+ assertTrue(cursor.moveToFirst());
+ modified = cursor.getLong(cursor.getColumnIndexOrThrow(BrowserContract.Bookmarks.DATE_MODIFIED));
+ } finally {
+ cursor.close();
+ }
+ assertNotEquals(modified, -1);
+ return modified;
+ }
+}