--- a/mobile/android/base/AndroidManifest.xml.in
+++ b/mobile/android/base/AndroidManifest.xml.in
@@ -323,21 +323,16 @@
android:label="@string/sync_configure_engines_title_tabs"
android:authorities="@ANDROID_PACKAGE_NAME@.db.tabs"
android:exported="false"/>
<provider android:name="org.mozilla.gecko.db.HomeProvider"
android:authorities="@ANDROID_PACKAGE_NAME@.db.home"
android:exported="false"/>
- <provider android:name="org.mozilla.gecko.db.ReadingListProvider"
- android:authorities="@ANDROID_PACKAGE_NAME@.db.readinglist"
- android:label="@string/reading_list_title"
- android:exported="false"/>
-
<provider android:name="org.mozilla.gecko.db.SearchHistoryProvider"
android:authorities="@ANDROID_PACKAGE_NAME@.db.searchhistory"
android:exported="false"/>
<service
android:exported="false"
android:name="org.mozilla.gecko.updater.UpdateService"
android:process="@MANGLED_ANDROID_PACKAGE_NAME@.UpdateService">
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
@@ -1710,17 +1710,16 @@ public class BrowserApp extends GeckoApp
overridePendingTransition(0, 0);
}
} else if ("Telemetry:Gather".equals(event)) {
final BrowserDB db = getProfile().getDB();
final ContentResolver cr = getContentResolver();
Telemetry.addToHistogram("PLACES_PAGES_COUNT", db.getCount(cr, "history"));
Telemetry.addToHistogram("FENNEC_BOOKMARKS_COUNT", db.getCount(cr, "bookmarks"));
- Telemetry.addToHistogram("FENNEC_READING_LIST_COUNT", db.getReadingListAccessor().getCount(cr));
Telemetry.addToHistogram("BROWSER_IS_USER_DEFAULT", (isDefaultBrowser(Intent.ACTION_VIEW) ? 1 : 0));
Telemetry.addToHistogram("FENNEC_TABQUEUE_ENABLED", (TabQueueHelper.isTabQueueEnabled(BrowserApp.this) ? 1 : 0));
Telemetry.addToHistogram("FENNEC_CUSTOM_HOMEPAGE", (TextUtils.isEmpty(getHomepage()) ? 0 : 1));
final SharedPreferences prefs = GeckoSharedPrefs.forProfile(getContext());
final boolean hasCustomHomepanels = prefs.contains(HomeConfigPrefsBackend.PREFS_CONFIG_KEY) || prefs.contains(HomeConfigPrefsBackend.PREFS_CONFIG_KEY_OLD);
Telemetry.addToHistogram("FENNEC_HOMEPANELS_CUSTOM", hasCustomHomepanels ? 1 : 0);
if (Versions.feature16Plus) {
--- a/mobile/android/base/java/org/mozilla/gecko/db/BrowserDB.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/BrowserDB.java
@@ -38,17 +38,16 @@ public interface BrowserDB {
public static enum FilterFlags {
EXCLUDE_PINNED_SITES
}
public abstract Searches getSearches();
public abstract TabsAccessor getTabsAccessor();
public abstract URLMetadata getURLMetadata();
- public abstract ReadingListAccessor getReadingListAccessor();
@RobocopTarget UrlAnnotations getUrlAnnotations();
/**
* Add default bookmarks to the database.
* Takes an offset; returns a new offset.
*/
public abstract int addDefaultBookmarks(Context context, ContentResolver cr, int offset);
--- a/mobile/android/base/java/org/mozilla/gecko/db/BrowserDatabaseHelper.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/BrowserDatabaseHelper.java
@@ -1044,17 +1044,20 @@ public final class BrowserDatabaseHelper
debug("Rewriting reading list table.");
createReadingListTable(db, "tmp_rl");
// Remove indexes. We don't need them now, and we'll be throwing away the table.
db.execSQL("DROP INDEX IF EXISTS reading_list_url");
db.execSQL("DROP INDEX IF EXISTS reading_list_guid");
db.execSQL("DROP INDEX IF EXISTS reading_list_content_status");
- final String thisDevice = ReadingListProvider.PLACEHOLDER_THIS_DEVICE;
+ // This used to be a part of the no longer existing ReadingListProvider, since we're deleting
+ // this table later in the second migration, and since sync for this table never existed,
+ // we don't care about the device name here.
+ final String thisDevice = "_fake_device_name_that_will_be_discarded_in_the_next_migration_";
db.execSQL("INSERT INTO tmp_rl (" +
// Here are the columns we can preserve.
ReadingListItems._ID + ", " +
ReadingListItems.URL + ", " +
ReadingListItems.TITLE + ", " +
ReadingListItems.RESOLVED_TITLE + ", " + // = TITLE (if CONTENT_STATUS = STATUS_FETCHED_ARTICLE)
ReadingListItems.RESOLVED_URL + ", " + // = URL (if CONTENT_STATUS = STATUS_FETCHED_ARTICLE)
ReadingListItems.EXCERPT + ", " +
--- a/mobile/android/base/java/org/mozilla/gecko/db/LocalBrowserDB.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/LocalBrowserDB.java
@@ -100,17 +100,16 @@ public class LocalBrowserDB implements B
private final Uri mFaviconsUriWithProfile;
private final Uri mThumbnailsUriWithProfile;
private final Uri mTopSitesUriWithProfile;
private final Uri mSearchHistoryUri;
private LocalSearches searches;
private LocalTabsAccessor tabsAccessor;
private LocalURLMetadata urlMetadata;
- private LocalReadingListAccessor readingListAccessor;
private LocalUrlAnnotations urlAnnotations;
private static final String[] DEFAULT_BOOKMARK_COLUMNS =
new String[] { Bookmarks._ID,
Bookmarks.GUID,
Bookmarks.URL,
Bookmarks.TITLE,
Bookmarks.TYPE,
@@ -135,17 +134,16 @@ public class LocalBrowserDB implements B
mHistoryUriWithProfile.buildUpon()
.appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true")
.appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true")
.build();
searches = new LocalSearches(mProfile);
tabsAccessor = new LocalTabsAccessor(mProfile);
urlMetadata = new LocalURLMetadata(mProfile);
- readingListAccessor = new LocalReadingListAccessor(mProfile);
urlAnnotations = new LocalUrlAnnotations(mProfile);
}
@Override
public Searches getSearches() {
return searches;
}
@@ -154,21 +152,16 @@ public class LocalBrowserDB implements B
return tabsAccessor;
}
@Override
public URLMetadata getURLMetadata() {
return urlMetadata;
}
- @Override
- public ReadingListAccessor getReadingListAccessor() {
- return readingListAccessor;
- }
-
@RobocopTarget
@Override
public UrlAnnotations getUrlAnnotations() {
return urlAnnotations;
}
/**
* Not thread safe. A helper to allocate new IDs for arbitrary strings.
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/db/LocalReadingListAccessor.java
+++ /dev/null
@@ -1,211 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * 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 android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.ContentValues;
-import android.content.Context;
-import android.database.ContentObserver;
-import android.database.Cursor;
-import android.net.Uri;
-import android.util.Log;
-
-import org.mozilla.gecko.annotation.RobocopTarget;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.GeckoEvent;
-import org.mozilla.gecko.reader.ReaderModeUtils;
-import org.mozilla.gecko.db.BrowserContract.ReadingListItems;
-
-
-@RobocopTarget
-public class LocalReadingListAccessor implements ReadingListAccessor {
- private static final String LOG_TAG = "GeckoReadingListAcc";
-
- private static final String NOT_DELETED = ReadingListItems.IS_DELETED + " = 0";
- private static final String NEITHER_DELETED_NOR_ARCHIVED = ReadingListItems.IS_ARCHIVED + " = 0 AND " + ReadingListItems.IS_DELETED + " = 0";
- private static final String ITEMS_TO_FETCH = ReadingListItems.CONTENT_STATUS + " = " + ReadingListItems.STATUS_UNFETCHED + " AND " + NEITHER_DELETED_NOR_ARCHIVED;
- private static final String SORT_ORDER_RECENT_FIRST = "COALESCE(" + ReadingListItems.SERVER_STORED_ON + ", " + ReadingListItems.ADDED_ON + ") DESC";
-
- private final Uri mReadingListUriWithProfile;
-
- public LocalReadingListAccessor(final String profile) {
- mReadingListUriWithProfile = DBUtils.appendProfile(profile, ReadingListItems.CONTENT_URI);
- }
-
- // Return a count of non-deleted items.
- @Override
- public int getCount(ContentResolver cr) {
- final String[] columns = new String[]{ReadingListItems._ID};
- final Cursor cursor = cr.query(mReadingListUriWithProfile, columns, NOT_DELETED, null, null);
-
- try {
- return cursor.getCount();
- } finally {
- cursor.close();
- }
- }
-
- @Override
- public Cursor getReadingList(ContentResolver cr) {
- // Return non-deleted, non-archived items, ordered by either server stored data or local added date,
- // descending.
- // This isn't ideal -- it depends on upload order! -- but the alternative is that a client with a
- // very skewed clock will force its items to the front or end of the list on other devices.
- return cr.query(mReadingListUriWithProfile,
- ReadingListItems.DEFAULT_PROJECTION,
- NEITHER_DELETED_NOR_ARCHIVED,
- null,
- SORT_ORDER_RECENT_FIRST);
- }
-
- @Override
- public Cursor getReadingListUnfetched(ContentResolver cr) {
- // Return unfetched, non-deleted, non-archived items, sorted by date added, newest first.
- // This allows us to fetch the top of the list first.
- return cr.query(mReadingListUriWithProfile,
- new String[] { ReadingListItems._ID, ReadingListItems.URL },
- ITEMS_TO_FETCH,
- null,
- SORT_ORDER_RECENT_FIRST);
- }
-
- @Override
- public boolean isReadingListItem(final ContentResolver cr, String uri) {
- uri = ReaderModeUtils.stripAboutReaderUrl(uri);
-
- final Cursor c = cr.query(mReadingListUriWithProfile,
- new String[] { ReadingListItems._ID },
- ReadingListItems.URL + " = ? OR " + ReadingListItems.RESOLVED_URL + " = ?",
- new String[] { uri, uri },
- null);
-
- if (c == null) {
- Log.e(LOG_TAG, "Null cursor in isReadingListItem");
- return false;
- }
-
- try {
- return c.moveToNext();
- } finally {
- c.close();
- }
- }
-
-
- @Override
- public long addReadingListItem(ContentResolver cr, ContentValues values) {
- // Check that required fields are present.
- for (String field: ReadingListItems.REQUIRED_FIELDS) {
- if (!values.containsKey(field)) {
- throw new IllegalArgumentException("Missing required field for reading list item: " + field);
- }
- }
-
- // URL is a required field so no key check needed.
- final String url = ReaderModeUtils.stripAboutReaderUrl(values.getAsString(ReadingListItems.URL));
- values.put(ReadingListItems.URL, url);
-
- // We're adding locally, so we can specify these.
- values.put(ReadingListItems.ADDED_ON, System.currentTimeMillis());
- values.put(ReadingListItems.ADDED_BY, ReadingListProvider.PLACEHOLDER_THIS_DEVICE);
-
- // We never un-delete (and we can't; we wipe as we go).
- // Re-add if necessary and allow the server to resolve conflicts.
- final long id = ContentUris.parseId(cr.insert(mReadingListUriWithProfile, values));
-
- GeckoAppShell.notifyObservers("Reader:Added", url);
-
- return id;
- }
-
- @Override
- public long addBasicReadingListItem(ContentResolver cr, String url, String title) {
- if (url == null) {
- throw new IllegalArgumentException("URL must not be null.");
- }
- final ContentValues values = new ContentValues();
- values.put(ReadingListItems.URL, url);
- if (title != null) {
- values.put(ReadingListItems.TITLE, title);
- } else {
- values.putNull(ReadingListItems.TITLE);
- }
-
- return addReadingListItem(cr, values);
- }
-
- @Override
- public void updateReadingListItem(ContentResolver cr, ContentValues values) {
- if (!values.containsKey(ReadingListItems._ID)) {
- throw new IllegalArgumentException("Cannot update reading list item without an ID");
- }
-
- if (values.containsKey(ReadingListItems.URL)) {
- values.put(ReadingListItems.URL, ReaderModeUtils.stripAboutReaderUrl(values.getAsString(ReadingListItems.URL)));
- }
-
- final int updated = cr.update(mReadingListUriWithProfile,
- values,
- ReadingListItems._ID + " = ? ",
- new String[] { values.getAsString(ReadingListItems._ID) });
-
- Log.d(LOG_TAG, "Updated " + updated + " reading list rows.");
- }
-
- @Override
- public void removeReadingListItemWithURL(final ContentResolver cr, String uri) {
- cr.delete(mReadingListUriWithProfile,
- ReadingListItems.URL + " = ? OR " + ReadingListItems.RESOLVED_URL + " = ?",
- new String[]{ uri, uri });
-
- GeckoAppShell.notifyObservers("Reader:Removed", uri);
- }
-
- @Override
- public void deleteItem(ContentResolver cr, long itemID) {
- // TODO: For completness, we should send a "Reader:Removed"
- // GeckoEvent, but we don't have the uri. Luckily, this is
- // only called in testing at the moment.
- cr.delete(ContentUris.appendId(mReadingListUriWithProfile.buildUpon(), itemID).build(),
- null, null);
- }
-
- @Override
- public void registerContentObserver(Context context, ContentObserver observer) {
- context.getContentResolver().registerContentObserver(mReadingListUriWithProfile, false, observer);
- }
-
- @Override
- public void markAsRead(ContentResolver cr, long itemID) {
- final ContentValues values = new ContentValues();
- values.put(ReadingListItems.MARKED_READ_BY, ReadingListProvider.PLACEHOLDER_THIS_DEVICE);
- values.put(ReadingListItems.MARKED_READ_ON, System.currentTimeMillis());
- values.put(ReadingListItems.IS_UNREAD, 0);
-
- // The ContentProvider will take care of updating the sync metadata.
- cr.update(mReadingListUriWithProfile, values, ReadingListItems._ID + " = " + itemID, null);
- }
-
- @Override
- public void markAsUnread(ContentResolver cr, long itemID) {
- final ContentValues values = new ContentValues();
- values.put(ReadingListItems.IS_UNREAD, 1);
-
- cr.update(mReadingListUriWithProfile, values, ReadingListItems._ID + " = " + itemID, null);
- }
-
- @Override
- public void updateContent(ContentResolver cr, long itemID, String resolvedTitle, String resolvedURL, String excerpt) {
- final ContentValues values = new ContentValues();
- values.put(ReadingListItems.CONTENT_STATUS, ReadingListItems.STATUS_FETCHED_ARTICLE);
- values.put(ReadingListItems.RESOLVED_URL, resolvedURL);
- values.put(ReadingListItems.RESOLVED_TITLE, resolvedTitle);
- values.put(ReadingListItems.EXCERPT, excerpt);
-
- // The ContentProvider will take care of updating the sync metadata.
- cr.update(mReadingListUriWithProfile, values, ReadingListItems._ID + " = " + itemID, null);
- }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/db/ReadingListAccessor.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * 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 android.content.ContentResolver;
-import android.content.ContentValues;
-import android.content.Context;
-import android.database.ContentObserver;
-import android.database.Cursor;
-import org.mozilla.gecko.annotation.RobocopTarget;
-
-@RobocopTarget
-public interface ReadingListAccessor {
- /**
- * Returns non-deleted, non-archived items.
- * Fennec doesn't currently offer a way to display archived items.
- *
- * Can return <code>null</code>.
- */
- Cursor getReadingList(ContentResolver cr);
-
- int getCount(ContentResolver cr);
-
- Cursor getReadingListUnfetched(ContentResolver cr);
-
- boolean isReadingListItem(ContentResolver cr, String uri);
-
- long addReadingListItem(ContentResolver cr, ContentValues values);
- long addBasicReadingListItem(ContentResolver cr, String url, String title);
-
- void updateReadingListItem(ContentResolver cr, ContentValues values);
-
- void removeReadingListItemWithURL(ContentResolver cr, String uri);
-
- void registerContentObserver(Context context, ContentObserver observer);
-
- void markAsRead(ContentResolver cr, long itemID);
- void markAsUnread(ContentResolver cr, long itemID);
- void updateContent(ContentResolver cr, long itemID, String resolvedTitle, String resolvedURL, String excerpt);
- void deleteItem(ContentResolver cr, long itemID);
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/db/ReadingListProvider.java
+++ /dev/null
@@ -1,414 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * 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 android.content.ContentUris;
-import android.content.ContentValues;
-import android.content.UriMatcher;
-import android.database.Cursor;
-import android.database.SQLException;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteQueryBuilder;
-import android.net.Uri;
-import android.text.TextUtils;
-import android.util.Log;
-import org.mozilla.gecko.db.DBUtils.UpdateOperation;
-
-import static org.mozilla.gecko.db.BrowserContract.ReadingListItems.*;
-
-public class ReadingListProvider extends SharedBrowserDatabaseProvider {
- private static final String LOGTAG = "GeckoRLProvider";
-
- static final String TABLE_READING_LIST = TABLE_NAME;
-
- static final int ITEMS = 101;
- static final int ITEMS_ID = 102;
- static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
-
- public static final String PLACEHOLDER_THIS_DEVICE = "$local";
-
- static {
- URI_MATCHER.addURI(BrowserContract.READING_LIST_AUTHORITY, "items", ITEMS);
- URI_MATCHER.addURI(BrowserContract.READING_LIST_AUTHORITY, "items/#", ITEMS_ID);
- }
-
- /**
- * Updates items that match the selection criteria. If no such items is found
- * one is inserted with the attributes passed in. Returns 0 if no item updated.
- *
- * Only use this method for callers, not internally -- it futzes with the provided
- * values to set syncing flags.
- *
- * @return Number of items updated or inserted
- */
- public int updateOrInsertItem(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
- if (!values.containsKey(CLIENT_LAST_MODIFIED)) {
- values.put(CLIENT_LAST_MODIFIED, System.currentTimeMillis());
- }
-
- if (isCallerSync(uri)) {
- int updated = updateItemsWithFlags(uri, values, null, selection, selectionArgs);
- if (updated > 0) {
- return updated;
- }
- return insertItem(uri, values) != -1 ? 1 : 0;
- }
-
- // Assume updated.
- final ContentValues flags = processChangeValues(values);
-
- int updated = updateItemsWithFlags(uri, values, flags, selection, selectionArgs);
- if (updated <= 0) {
- // Must be an insertion. Let's make sure we're NEW and discard any update flags.
- values.put(SYNC_STATUS, SYNC_STATUS_NEW);
- values.put(SYNC_CHANGE_FLAGS, SYNC_CHANGE_NONE);
- updated = insertItem(uri, values) != -1 ? 1 : 0;
- }
- return updated;
- }
-
- /**
- * This method does two things:
- * * Based on the values provided, it computes and returns an incremental status change
- * that can be applied to the database to track changes for syncing. This should be
- * applied with {@link UpdateOperation#BITWISE_OR}.
- * * It mutates the provided values to mark absolute field changes.
- *
- * @return null if no values were provided, or no change needs to be recorded.
- */
- private ContentValues processChangeValues(ContentValues values) {
- if (values == null || values.size() == 0) {
- return null;
- }
-
- final ContentValues out = new ContentValues();
- int flag = 0;
- if (values.containsKey(MARKED_READ_BY) ||
- values.containsKey(MARKED_READ_ON) ||
- values.containsKey(IS_UNREAD)) {
- flag |= SYNC_CHANGE_UNREAD_CHANGED;
- }
-
- if (values.containsKey(IS_FAVORITE)) {
- flag |= SYNC_CHANGE_FAVORITE_CHANGED;
- }
-
- if (values.containsKey(RESOLVED_URL) ||
- values.containsKey(RESOLVED_TITLE) ||
- values.containsKey(EXCERPT)) {
- flag |= SYNC_CHANGE_RESOLVED;
- }
-
- if (flag == 0) {
- return null;
- }
-
- out.put(SYNC_CHANGE_FLAGS, flag);
- return out;
- }
-
- /**
- * Updates items that match the selection criteria.
- *
- * @return Number of items updated or inserted
- */
- public int updateItemsWithFlags(Uri uri, ContentValues values, ContentValues flags, String selection, String[] selectionArgs) {
- trace("Updating ReadingListItems on URI: " + uri);
- final SQLiteDatabase db = getWritableDatabase(uri);
- if (!values.containsKey(CLIENT_LAST_MODIFIED)) {
- values.put(CLIENT_LAST_MODIFIED, System.currentTimeMillis());
- }
-
- if (flags == null) {
- // This code path is used by Sync. Bypass metadata changes.
- return db.update(TABLE_READING_LIST, values, selection, selectionArgs);
- }
-
- // Set synced items to MODIFIED; otherwise, leave the sync status untouched.
- final ContentValues setModified = new ContentValues();
- setModified.put(SYNC_STATUS, "CASE " + SYNC_STATUS +
- " WHEN " + SYNC_STATUS_SYNCED +
- " THEN " + SYNC_STATUS_MODIFIED +
- " ELSE " + SYNC_STATUS +
- " END");
-
- final ContentValues[] valuesAndFlags = {values, flags, setModified};
- final UpdateOperation[] ops = {UpdateOperation.ASSIGN, UpdateOperation.BITWISE_OR, UpdateOperation.EXPRESSION};
-
- return DBUtils.updateArrays(db, TABLE_READING_LIST, valuesAndFlags, ops, selection, selectionArgs);
- }
-
- /**
- * Inserts a new item into the DB. CLIENT_LAST_MODIFIED is generated if it is not specified.
- *
- * Non-Sync callers will have ADDED_ON and ADDED_BY set appropriately if they are missing;
- * the assumption is that this is a new item added on this device.
- *
- * @return ID of the newly inserted item
- */
- private long insertItem(Uri uri, ContentValues values) {
- if (!values.containsKey(CLIENT_LAST_MODIFIED)) {
- values.put(CLIENT_LAST_MODIFIED, System.currentTimeMillis());
- }
-
- // We trust the syncing code to specify SYNC_STATUS_SYNCED.
- if (!isCallerSync(uri)) {
- values.put(SYNC_STATUS, SYNC_STATUS_NEW);
- if (!values.containsKey(ADDED_ON)) {
- values.put(ADDED_ON, System.currentTimeMillis());
- }
- if (!values.containsKey(ADDED_BY)) {
- values.put(ADDED_BY, PLACEHOLDER_THIS_DEVICE);
- }
- }
-
- final String url = values.getAsString(URL);
- debug("Inserting item in database with URL: " + url);
- try {
- return getWritableDatabase(uri).insertOrThrow(TABLE_READING_LIST, null, values);
- } catch (SQLException e) {
- Log.e(LOGTAG, "Insert failed.", e);
- throw e;
- }
- }
-
- private static final ContentValues DELETED_VALUES;
- static {
- final ContentValues values = new ContentValues();
- values.put(IS_DELETED, 1);
-
- values.put(URL, ""); // Non-null column.
- values.putNull(RESOLVED_URL);
- values.putNull(RESOLVED_TITLE);
- values.putNull(TITLE);
- values.putNull(EXCERPT);
- values.putNull(ADDED_BY);
- values.putNull(MARKED_READ_BY);
-
- // Mark it as deleted for sync purposes.
- values.put(SYNC_STATUS, SYNC_STATUS_DELETED);
- values.put(SYNC_CHANGE_FLAGS, SYNC_CHANGE_NONE);
- DELETED_VALUES = values;
- }
-
- /**
- * Deletes items. Item is marked as 'deleted' so that sync can
- * detect the change.
- *
- * It's the caller's responsibility to handle both original and resolved URLs.
- * @return Number of deleted items
- */
- int deleteItems(final Uri uri, String selection, String[] selectionArgs) {
- debug("Deleting item entry for URI: " + uri);
- final SQLiteDatabase db = getWritableDatabase(uri);
-
- // TODO: also ensure that we delete affected items from the disk cache. Bug 1133158.
- if (isCallerSync(uri)) {
- debug("Directly deleting from reading list.");
- return db.delete(TABLE_READING_LIST, selection, selectionArgs);
- }
-
- // If we don't have a GUID for this item, then it hasn't made it
- // to the server. Just delete it.
- // If we do have a GUID, blank the row and mark it as deleted.
- int total = 0;
- final String whereNullGUID = DBUtils.concatenateWhere(selection, GUID + " IS NULL");
- final String whereNotNullGUID = DBUtils.concatenateWhere(selection, GUID + " IS NOT NULL");
-
- total += db.delete(TABLE_READING_LIST, whereNullGUID, selectionArgs);
- total += updateItemsWithFlags(uri, DELETED_VALUES, null, whereNotNullGUID, selectionArgs);
-
- return total;
- }
-
- int deleteItemByID(final Uri uri, long id) {
- debug("Deleting item entry for ID: " + id);
- final SQLiteDatabase db = getWritableDatabase(uri);
-
- // TODO: also ensure that we delete affected items from the disk cache. Bug 1133158.
- if (isCallerSync(uri)) {
- debug("Directly deleting from reading list.");
- final String selection = _ID + " = " + id;
- return db.delete(TABLE_READING_LIST, selection, null);
- }
-
- // If we don't have a GUID for this item, then it hasn't made it
- // to the server. Just delete it.
- final String whereNullGUID = _ID + " = " + id + " AND " + GUID + " IS NULL";
- final int raw = db.delete(TABLE_READING_LIST, whereNullGUID, null);
- if (raw > 0) {
- // _ID is unique, so this should only ever be 1, but it definitely means
- // we don't need to try the second part.
- return raw;
- }
-
- // If we do have a GUID, blank the row and mark it as deleted.
- final String whereNotNullGUID = _ID + " = " + id + " AND " + GUID + " IS NOT NULL";
- final ContentValues values = new ContentValues(DELETED_VALUES);
- values.put(CLIENT_LAST_MODIFIED, System.currentTimeMillis());
- return updateItemsWithFlags(uri, values, null, whereNotNullGUID, null);
- }
-
- @Override
- @SuppressWarnings("fallthrough")
- public int updateInTransaction(final Uri uri, ContentValues values, String selection, String[] selectionArgs) {
- trace("Calling update in transaction on URI: " + uri);
-
- int updated = 0;
- int match = URI_MATCHER.match(uri);
-
- switch (match) {
- case ITEMS_ID:
- debug("Update on ITEMS_ID: " + uri);
- selection = DBUtils.concatenateWhere(selection, TABLE_READING_LIST + "._id = ?");
- selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
- new String[] { Long.toString(ContentUris.parseId(uri)) });
-
- case ITEMS: {
- debug("Updating ITEMS: " + uri);
- if (shouldUpdateOrInsert(uri)) {
- // updateOrInsertItem handles change flags for us.
- updated = updateOrInsertItem(uri, values, selection, selectionArgs);
- } else {
- // Don't use flags if we're inserting from sync.
- ContentValues flags = isCallerSync(uri) ? null : processChangeValues(values);
- updated = updateItemsWithFlags(uri, values, flags, selection, selectionArgs);
- }
- break;
- }
-
- default:
- throw new UnsupportedOperationException("Unknown update URI " + uri);
- }
-
- debug("Updated " + updated + " rows for URI: " + uri);
- return updated;
- }
-
-
- @Override
- @SuppressWarnings("fallthrough")
- public int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) {
- trace("Calling delete in transaction on URI: " + uri);
-
- // This will never clean up any items that we're about to delete, so we
- // might as well run it first!
- cleanUpSomeDeletedRecords(uri, TABLE_READING_LIST);
-
- int numDeleted = 0;
- int match = URI_MATCHER.match(uri);
-
- switch (match) {
- case ITEMS_ID:
- debug("Deleting on ITEMS_ID: " + uri);
- numDeleted = deleteItemByID(uri, ContentUris.parseId(uri));
- break;
-
- case ITEMS:
- debug("Deleting ITEMS: " + uri);
- numDeleted = deleteItems(uri, selection, selectionArgs);
- break;
-
- default:
- throw new UnsupportedOperationException("Unknown update URI " + uri);
- }
-
- debug("Deleted " + numDeleted + " rows for URI: " + uri);
- return numDeleted;
- }
-
- @Override
- public Uri insertInTransaction(Uri uri, ContentValues values) {
- trace("Calling insert in transaction on URI: " + uri);
- long id = -1;
- int match = URI_MATCHER.match(uri);
-
- switch (match) {
- case ITEMS:
- trace("Insert on ITEMS: " + uri);
- id = insertItem(uri, values);
- break;
-
- default:
- // Log here because we typically insert in a batch, and that will muffle.
- Log.e(LOGTAG, "Unknown insert URI " + uri);
- throw new UnsupportedOperationException("Unknown insert URI " + uri);
- }
-
- debug("Inserted ID in database: " + id);
-
- if (id >= 0) {
- return ContentUris.withAppendedId(uri, id);
- }
-
- Log.e(LOGTAG, "Got to end of insertInTransaction without returning an id!");
- return null;
- }
-
- @Override
- public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
- String groupBy = null;
- SQLiteDatabase db = getReadableDatabase(uri);
- SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
- String limit = uri.getQueryParameter(BrowserContract.PARAM_LIMIT);
-
- final int match = URI_MATCHER.match(uri);
- switch (match) {
- case ITEMS_ID:
- trace("Query on ITEMS_ID: " + uri);
- selection = DBUtils.concatenateWhere(selection, _ID + " = ?");
- selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
- new String[] { Long.toString(ContentUris.parseId(uri)) });
-
- case ITEMS:
- trace("Query on ITEMS: " + uri);
- if (!shouldShowDeleted(uri)) {
- selection = DBUtils.concatenateWhere(IS_DELETED + " = 0", selection);
- }
- break;
-
- default:
- throw new UnsupportedOperationException("Unknown query URI " + uri);
- }
-
- if (TextUtils.isEmpty(sortOrder)) {
- sortOrder = DEFAULT_SORT_ORDER;
- }
-
- trace("Running built query.");
- qb.setTables(TABLE_READING_LIST);
- Cursor cursor = qb.query(db, projection, selection, selectionArgs, groupBy, null, sortOrder, limit);
- cursor.setNotificationUri(getContext().getContentResolver(), uri);
-
- return cursor;
- }
-
- @Override
- public String getType(Uri uri) {
- trace("Getting URI type: " + uri);
-
- final int match = URI_MATCHER.match(uri);
- switch (match) {
- case ITEMS:
- trace("URI is ITEMS: " + uri);
- return CONTENT_TYPE;
-
- case ITEMS_ID:
- trace("URI is ITEMS_ID: " + uri);
- return CONTENT_ITEM_TYPE;
- }
-
- debug("URI has unrecognized type: " + uri);
- return null;
- }
-
- @Override
- protected String getDeletedItemSelection(long earlierThan) {
- if (earlierThan == -1L) {
- return IS_DELETED + " = 1";
- }
- return IS_DELETED + " = 1 AND " + CLIENT_LAST_MODIFIED + " <= " + earlierThan;
- }
-}
--- a/mobile/android/base/java/org/mozilla/gecko/db/StubBrowserDB.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/StubBrowserDB.java
@@ -22,77 +22,16 @@ import org.mozilla.gecko.feeds.subscript
import android.content.ContentProviderOperation;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import android.graphics.drawable.BitmapDrawable;
-class StubReadingListAccessor implements ReadingListAccessor {
- @Override
- public Cursor getReadingList(ContentResolver cr) {
- return null;
- }
-
- @Override
- public int getCount(ContentResolver cr) {
- return 0;
- }
-
- @Override
- public Cursor getReadingListUnfetched(ContentResolver cr) {
- return null;
- }
-
- @Override
- public boolean isReadingListItem(ContentResolver cr, String uri) {
- return false;
- }
-
- @Override
- public long addReadingListItem(ContentResolver cr, ContentValues values) {
- return 0L;
- }
-
- @Override
- public long addBasicReadingListItem(ContentResolver cr, String url, String title) {
- return 0L;
- }
-
- @Override
- public void updateReadingListItem(ContentResolver cr, ContentValues values) {
- }
-
- @Override
- public void removeReadingListItemWithURL(ContentResolver cr, String uri) {
- }
-
- @Override
- public void registerContentObserver(Context context, ContentObserver observer) {
- }
-
- @Override
- public void markAsRead(ContentResolver cr, long itemID) {
- }
-
- @Override
- public void markAsUnread(ContentResolver cr, long itemID) {
- }
-
- @Override
- public void updateContent(ContentResolver cr, long itemID, String resolvedTitle, String resolvedURL, String excerpt) {
- }
-
- @Override
- public void deleteItem(ContentResolver cr, long itemID) {
-
- }
-}
-
class StubSearches implements Searches {
public StubSearches() {
}
public void insert(ContentResolver cr, String query) {
}
}
@@ -201,17 +140,16 @@ class StubUrlAnnotations implements UrlA
/*
* This base implementation just stubs all methods. For the
* real implementations, see LocalBrowserDB.java.
*/
public class StubBrowserDB implements BrowserDB {
private final StubSearches searches = new StubSearches();
private final StubTabsAccessor tabsAccessor = new StubTabsAccessor();
private final StubURLMetadata urlMetadata = new StubURLMetadata();
- private final StubReadingListAccessor readingListAccessor = new StubReadingListAccessor();
private final StubUrlAnnotations urlAnnotations = new StubUrlAnnotations();
private SuggestedSites suggestedSites = null;
@Override
public Searches getSearches() {
return searches;
}
@@ -221,21 +159,16 @@ public class StubBrowserDB implements Br
}
@Override
public URLMetadata getURLMetadata() {
return urlMetadata;
}
@Override
- public ReadingListAccessor getReadingListAccessor() {
- return readingListAccessor;
- }
-
- @Override
public UrlAnnotations getUrlAnnotations() {
return urlAnnotations;
}
protected static final Integer FAVICON_ID_NOT_FOUND = Integer.MIN_VALUE;
public StubBrowserDB(String profile) {
}
@@ -293,58 +226,37 @@ public class StubBrowserDB implements Br
public void clearHistory(ContentResolver cr, boolean clearSearchHistory) {
}
@RobocopTarget
public Cursor getBookmarksInFolder(ContentResolver cr, long folderId) {
return null;
}
- public Cursor getReadingList(ContentResolver cr) {
- return null;
- }
-
- public Cursor getReadingListUnfetched(ContentResolver cr) {
- return null;
- }
-
@RobocopTarget
public boolean isBookmark(ContentResolver cr, String uri) {
return false;
}
- public boolean isReadingListItem(ContentResolver cr, String uri) {
- return false;
- }
-
public String getUrlForKeyword(ContentResolver cr, String keyword) {
return null;
}
protected void bumpParents(ContentResolver cr, String param, String value) {
}
@RobocopTarget
public boolean addBookmark(ContentResolver cr, String title, String uri) {
return false;
}
@RobocopTarget
public void removeBookmarksWithURL(ContentResolver cr, String uri) {
}
- public void addReadingListItem(ContentResolver cr, ContentValues values) {
- }
-
- public void updateReadingListItem(ContentResolver cr, ContentValues values) {
- }
-
- public void removeReadingListItemWithURL(ContentResolver cr, String uri) {
- }
-
public void registerBookmarkObserver(ContentResolver cr, ContentObserver observer) {
}
@RobocopTarget
public void updateBookmark(ContentResolver cr, int id, String uri, String title, String keyword) {
}
public LoadFaviconResult getFaviconForUrl(ContentResolver cr, String faviconURL) {
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -215,27 +215,24 @@ gbjar.sources += ['java/org/mozilla/geck
'db/BaseTable.java',
'db/BrowserDatabaseHelper.java',
'db/BrowserDB.java',
'db/BrowserProvider.java',
'db/DBUtils.java',
'db/FormHistoryProvider.java',
'db/HomeProvider.java',
'db/LocalBrowserDB.java',
- 'db/LocalReadingListAccessor.java',
'db/LocalSearches.java',
'db/LocalTabsAccessor.java',
'db/LocalUrlAnnotations.java',
'db/LocalURLMetadata.java',
'db/LoginsProvider.java',
'db/PasswordsProvider.java',
'db/PerProfileDatabaseProvider.java',
'db/PerProfileDatabases.java',
- 'db/ReadingListAccessor.java',
- 'db/ReadingListProvider.java',
'db/RemoteClient.java',
'db/RemoteTab.java',
'db/Searches.java',
'db/SearchHistoryProvider.java',
'db/SharedBrowserDatabaseProvider.java',
'db/SQLiteBridgeContentProvider.java',
'db/StubBrowserDB.java',
'db/SuggestedSites.java',
--- a/mobile/android/tests/browser/robocop/robocop.ini
+++ b/mobile/android/tests/browser/robocop/robocop.ini
@@ -52,17 +52,16 @@ skip-if = true
[src/org/mozilla/gecko/tests/testPasswordProvider.java]
# [src/org/mozilla/gecko/tests/testPermissions.java] # see bug 757475
[src/org/mozilla/gecko/tests/testPictureLinkContextMenu.java]
[src/org/mozilla/gecko/tests/testPrefsObserver.java]
[src/org/mozilla/gecko/tests/testPrivateBrowsing.java]
[src/org/mozilla/gecko/tests/testPromptGridInput.java]
# bug 1001657
skip-if = android_version == "18"
-[src/org/mozilla/gecko/tests/testReadingListProvider.java]
[src/org/mozilla/gecko/tests/testSearchHistoryProvider.java]
[src/org/mozilla/gecko/tests/testSearchSuggestions.java]
# disabled on 4.3, bug 1145867
skip-if = android_version == "18"
[src/org/mozilla/gecko/tests/testSessionOOMSave.java]
# disabled on 4.3, bug 1144888
skip-if = android_version == "18"
[src/org/mozilla/gecko/tests/testSessionOOMRestore.java]
deleted file mode 100644
--- a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testReadingListProvider.java
+++ /dev/null
@@ -1,783 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * 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.tests;
-
-import java.util.HashSet;
-import java.util.Random;
-import java.util.concurrent.Callable;
-
-import android.app.Activity;
-import android.content.ContentResolver;
-import org.mozilla.gecko.BrowserApp;
-import org.mozilla.gecko.db.BrowserContract;
-import org.mozilla.gecko.db.BrowserContract.ReadingListItems;
-import org.mozilla.gecko.db.ReadingListAccessor;
-import org.mozilla.gecko.db.ReadingListProvider;
-
-import android.content.ContentProvider;
-import android.content.ContentUris;
-import android.content.ContentValues;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.net.Uri;
-
-import static org.mozilla.gecko.db.BrowserContract.ReadingListItems.*;
-import static org.mozilla.gecko.db.BrowserContract.ReadingListItems.ADDED_ON;
-import static org.mozilla.gecko.db.BrowserContract.ReadingListItems.CLIENT_LAST_MODIFIED;
-import static org.mozilla.gecko.db.BrowserContract.ReadingListItems.EXCERPT;
-import static org.mozilla.gecko.db.BrowserContract.ReadingListItems.GUID;
-import static org.mozilla.gecko.db.BrowserContract.ReadingListItems.IS_UNREAD;
-import static org.mozilla.gecko.db.BrowserContract.ReadingListItems.RESOLVED_TITLE;
-import static org.mozilla.gecko.db.BrowserContract.ReadingListItems.RESOLVED_URL;
-import static org.mozilla.gecko.db.BrowserContract.ReadingListItems.SERVER_LAST_MODIFIED;
-import static org.mozilla.gecko.db.BrowserContract.ReadingListItems.SERVER_STORED_ON;
-import static org.mozilla.gecko.db.BrowserContract.ReadingListItems.SYNC_CHANGE_FLAGS;
-import static org.mozilla.gecko.db.BrowserContract.ReadingListItems.SYNC_CHANGE_NONE;
-import static org.mozilla.gecko.db.BrowserContract.ReadingListItems.SYNC_CHANGE_RESOLVED;
-import static org.mozilla.gecko.db.BrowserContract.ReadingListItems.SYNC_CHANGE_UNREAD_CHANGED;
-import static org.mozilla.gecko.db.BrowserContract.ReadingListItems.SYNC_STATUS;
-import static org.mozilla.gecko.db.BrowserContract.ReadingListItems.SYNC_STATUS_MODIFIED;
-import static org.mozilla.gecko.db.BrowserContract.ReadingListItems.SYNC_STATUS_NEW;
-import static org.mozilla.gecko.db.BrowserContract.ReadingListItems.SYNC_STATUS_SYNCED;
-import static org.mozilla.gecko.db.BrowserContract.URLColumns.TITLE;
-import static org.mozilla.gecko.db.BrowserContract.URLColumns.URL;
-
-public class testReadingListProvider extends ContentProviderTest {
-
- private static final String DB_NAME = "browser.db";
-
- // List of tests to be run sorted by dependency.
- private final TestCase[] TESTS_TO_RUN = {
- new TestInsertItems(),
- new TestDeleteItems(),
- new TestUpdateItems(),
- new TestBatchOperations(),
- new TestBrowserProviderNotifications(),
- new TestStateSequencing(),
- };
-
- // Columns used to test for item equivalence.
- final String[] TEST_COLUMNS = { TITLE,
- URL,
- EXCERPT,
- ADDED_ON };
-
- // Indicates that insertions have been tested. ContentProvider.insert
- // has been proven to work.
- private boolean mContentProviderInsertTested = false;
-
- // Indicates that updates have been tested. ContentProvider.update
- // has been proven to work.
- private boolean mContentProviderUpdateTested = false;
-
- /**
- * Factory function that makes new ReadingListProvider instances.
- * <p>
- * We want a fresh provider each test, so this should be invoked in
- * <code>setUp</code> before each individual test.
- */
- private static final Callable<ContentProvider> sProviderFactory = new Callable<ContentProvider>() {
- @Override
- public ContentProvider call() {
- return new ReadingListProvider();
- }
- };
-
- @Override
- public void setUp() throws Exception {
- super.setUp(sProviderFactory, BrowserContract.READING_LIST_AUTHORITY, DB_NAME);
- for (TestCase test: TESTS_TO_RUN) {
- mTests.add(test);
- }
- }
-
- public void testReadingListProviderTests() throws Exception {
- for (Runnable test : mTests) {
- setTestName(test.getClass().getSimpleName());
- ensureEmptyDatabase();
- test.run();
- }
-
- // Ensure browser initialization is complete before completing test,
- // so that the minidumps directory is consistently created.
- blockForGeckoReady();
- }
-
- /**
- * Verify that we can insert a reading list item into the DB.
- */
- private class TestInsertItems extends TestCase {
- @Override
- public void test() throws Exception {
- ContentValues b = createFillerReadingListItem();
- long id = ContentUris.parseId(mProvider.insert(CONTENT_URI, b));
- Cursor c = getItemById(id);
-
- try {
- mAsserter.ok(c.moveToFirst(), "Inserted item found", "");
- assertRowEqualsContentValues(c, b);
-
- mAsserter.is(c.getInt(c.getColumnIndex(CONTENT_STATUS)),
- STATUS_UNFETCHED,
- "Inserted item has correct default content status");
- } finally {
- c.close();
- }
-
- testInsertWithNullCol(URL);
- mContentProviderInsertTested = true;
- }
-
- /**
- * Test that insertion fails when a required column
- * is null.
- */
- private void testInsertWithNullCol(String colName) {
- ContentValues b = createFillerReadingListItem();
- b.putNull(colName);
-
- try {
- ContentUris.parseId(mProvider.insert(CONTENT_URI, b));
- // If we get to here, the flawed insertion succeeded. Fail the test.
- mAsserter.ok(false, "Insertion did not succeed with " + colName + " == null", "");
- } catch (NullPointerException e) {
- // Indicates test was successful.
- }
- }
- }
-
- /**
- * Verify that we can remove a reading list item from the DB.
- */
- private class TestDeleteItems extends TestCase {
-
- @Override
- public void test() throws Exception {
- final long one = insertAnItemWithAssertion();
- final long two = insertAnItemWithAssertion();
-
- assignGUID(one);
-
- // Test that the item with a GUID is only marked as deleted and
- // not removed from the database.
- testNonSyncDelete(one, true);
-
- // The item without a GUID is just deleted immediately.
- testNonSyncDelete(two, false);
-
- // Test that the item with a GUID is removed from the database when deleted by Sync.
- testSyncDelete(one);
-
- final long three = insertAnItemWithAssertion();
-
- // Test that deleting works with only a URI.
- testDeleteWithItemURI(three);
- }
-
- private void assignGUID(final long id) {
- final ContentValues values = new ContentValues();
- values.put(GUID, "abcdefghi");
- mProvider.update(CONTENT_URI, values, _ID + " = " + id, null);
- }
-
- /**
- * Delete an item with PARAM_IS_SYNC unset and verify that item was only marked
- * as deleted and not actually removed from the database. Also verify that the item
- * marked as deleted doesn't show up in a query.
- *
- * Note that items are deleted immediately if they don't have a GUID.
- *
- * @param id of the item to be deleted
- * @param hasGUID if true, we expect the item to stick around and be marked.
- */
- private void testNonSyncDelete(long id, boolean hasGUID) {
- final int deleted = mProvider.delete(CONTENT_URI,
- _ID + " = " + id,
- null);
-
- mAsserter.is(deleted, 1, "Inserted item was deleted");
-
- // PARAM_SHOW_DELETED in the URI allows items marked as deleted to be
- // included in the query.
- Uri uri = appendUriParam(CONTENT_URI, BrowserContract.PARAM_SHOW_DELETED, "1");
- if (hasGUID) {
- assertItemExistsByID(uri, id, "Deleted item was only marked as deleted");
- } else {
- assertItemDoesNotExistByID(uri, id, "Deleted item had no GUID, so was really deleted.");
- }
-
- // Test that the 'deleted' item does not show up in a query when PARAM_SHOW_DELETED
- // is not specified in the URI.
- assertItemDoesNotExistByID(id, "Inserted item can't be found after deletion");
- }
-
- /**
- * Delete an item with PARAM_IS_SYNC=1 and verify that item
- * was actually removed from the database.
- *
- * @param id of the item to be deleted
- */
- private void testSyncDelete(long id) {
- final int deleted = mProvider.delete(appendUriParam(CONTENT_URI, BrowserContract.PARAM_IS_SYNC, "1"),
- _ID + " = " + id,
- null);
-
- mAsserter.is(deleted, 1, "Inserted item was deleted");
-
- Uri uri = appendUriParam(CONTENT_URI, BrowserContract.PARAM_SHOW_DELETED, "1");
- assertItemDoesNotExistByID(uri, id, "Inserted item is now actually deleted");
- }
-
- /**
- * Delete an item with its URI and verify that the item
- * was actually removed from the database.
- *
- * @param id of the item to be deleted
- */
- private void testDeleteWithItemURI(long id) {
- final int deleted = mProvider.delete(ContentUris.withAppendedId(CONTENT_URI, id), null, null);
- mAsserter.is(deleted, 1, "Inserted item was deleted using URI with id");
- }
- }
-
- /**
- * Verify that we can update reading list items.
- */
- private class TestUpdateItems extends TestCase {
-
- @Override
- public void test() throws Exception {
- // We should be able to insert into the DB.
- ensureCanInsert();
-
- ContentValues original = createFillerReadingListItem();
- long id = ContentUris.parseId(mProvider.insert(CONTENT_URI, original));
- int updated = 0;
- Long originalDateCreated = null;
- Long originalDateModified = null;
- ContentValues updates = new ContentValues();
- Cursor c = getItemById(id);
- try {
- mAsserter.ok(c.moveToFirst(), "Inserted item found", "");
-
- originalDateCreated = c.getLong(c.getColumnIndex(ADDED_ON));
- originalDateModified = c.getLong(c.getColumnIndex(CLIENT_LAST_MODIFIED));
-
- updates.put(TITLE, original.getAsString(TITLE) + "CHANGED");
- updates.put(URL, original.getAsString(URL) + "/more/stuff");
- updates.put(EXCERPT, original.getAsString(EXCERPT) + "CHANGED");
-
- updated = mProvider.update(CONTENT_URI, updates,
- _ID + " = ?",
- new String[] { String.valueOf(id) });
-
- mAsserter.is(updated, 1, "Inserted item was updated");
- } finally {
- c.close();
- }
-
- // Name change for clarity. These values will be compared with the
- // current cursor row.
- final ContentValues expectedValues = updates;
- c = getItemById(id);
- try {
- mAsserter.ok(c.moveToFirst(), "Updated item found", "");
- mAsserter.isnot(c.getLong(c.getColumnIndex(CLIENT_LAST_MODIFIED)),
- originalDateModified,
- "Date modified should have changed");
-
- // ADDED_ON shouldn't have changed.
- expectedValues.put(ADDED_ON, originalDateCreated);
- assertRowEqualsContentValues(c, expectedValues, /* compareDateModified */ false, TEST_COLUMNS);
- } finally {
- c.close();
- }
-
- // Test that updates on an item that doesn't exist does not modify any rows.
- testUpdateWithInvalidID();
-
- mContentProviderUpdateTested = true;
- }
-
- /**
- * Test that updates on an item that doesn't exist does
- * not modify any rows.
- */
- private void testUpdateWithInvalidID() {
- ensureEmptyDatabase();
- final ContentValues b = createFillerReadingListItem();
- final long id = ContentUris.parseId(mProvider.insert(CONTENT_URI, b));
- final long INVALID_ID = id + 1;
- final ContentValues updates = new ContentValues();
- updates.put(TITLE, b.getAsString(TITLE) + "CHANGED");
- final int updated = mProvider.update(CONTENT_URI, updates,
- _ID + " = ?",
- new String[] { String.valueOf(INVALID_ID) });
- mAsserter.is(updated, 0, "Should not be able to update item with an invalid ID");
- }
- }
-
- private class TestBatchOperations extends TestCase {
- private static final int ITEM_COUNT = 10;
-
- /**
- * Insert a bunch of items into the DB with the bulkInsert
- * method and verify that they are there.
- */
- private void testBulkInsert() {
- ensureEmptyDatabase();
- final ContentValues allVals[] = new ContentValues[ITEM_COUNT];
- final HashSet<String> urls = new HashSet<String>();
- for (int i = 0; i < ITEM_COUNT; i++) {
- final String url = "http://www.test.org/" + i;
- allVals[i] = new ContentValues();
- allVals[i].put(TITLE, "Test" + i);
- allVals[i].put(URL, url);
- allVals[i].put(EXCERPT, "EXCERPT" + i);
- urls.add(url);
- }
-
- int inserts = mProvider.bulkInsert(CONTENT_URI, allVals);
- mAsserter.is(inserts, ITEM_COUNT, "Excepted number of inserts matches");
-
- final Cursor c = mProvider.query(CONTENT_URI, null,
- null,
- null,
- null);
- try {
- while (c.moveToNext()) {
- final String url = c.getString(c.getColumnIndex(URL));
- mAsserter.ok(urls.contains(url), "Bulk inserted item with url == " + url + " was found in the DB", "");
- // We should only be seeing each item once. Remove from set to prevent dups.
- urls.remove(url);
- }
- } finally {
- c.close();
- }
- }
-
- @Override
- public void test() {
- testBulkInsert();
- }
- }
-
- /*
- * Verify that insert, update, delete, and bulkInsert operations
- * notify the ambient content resolver. Each operation calls the
- * content resolver notifyChange method synchronously, so it is
- * okay to test sequentially.
- */
- private class TestBrowserProviderNotifications extends TestCase {
-
- @Override
- public void test() {
- // We should be able to insert into the DB.
- ensureCanInsert();
- // We should be able to update the DB.
- ensureCanUpdate();
-
- final String CONTENT_URI = ReadingListItems.CONTENT_URI.toString();
-
- mResolver.notifyChangeList.clear();
-
- // Insert
- final ContentValues h = createFillerReadingListItem();
- long id = ContentUris.parseId(mProvider.insert(ReadingListItems.CONTENT_URI, h));
-
- mAsserter.isnot(id,
- -1L,
- "Inserted item has valid id");
-
- ensureOnlyChangeNotifiedStartsWith(CONTENT_URI, "insert");
-
- // Update
- mResolver.notifyChangeList.clear();
- h.put(TITLE, "http://newexample.com");
-
- long numUpdated = mProvider.update(ReadingListItems.CONTENT_URI, h,
- _ID + " = ?",
- new String[] { String.valueOf(id) });
-
- mAsserter.is(numUpdated,
- 1L,
- "Correct number of items are updated");
-
- ensureOnlyChangeNotifiedStartsWith(CONTENT_URI, "update");
-
- // Delete
- mResolver.notifyChangeList.clear();
- long numDeleted = mProvider.delete(ReadingListItems.CONTENT_URI, null, null);
-
- mAsserter.is(numDeleted,
- 1L,
- "Correct number of items are deleted");
-
- ensureOnlyChangeNotifiedStartsWith(CONTENT_URI, "delete");
-
- // Bulk insert
- mResolver.notifyChangeList.clear();
- final ContentValues[] hs = { createFillerReadingListItem(),
- createFillerReadingListItem(),
- createFillerReadingListItem() };
-
- long numBulkInserted = mProvider.bulkInsert(ReadingListItems.CONTENT_URI, hs);
-
- mAsserter.is(numBulkInserted,
- 3L,
- "Correct number of items are bulkInserted");
-
- ensureOnlyChangeNotifiedStartsWith(CONTENT_URI, "bulkInsert");
- }
-
- protected void ensureOnlyChangeNotifiedStartsWith(String expectedUri, String operation) {
- mAsserter.is(Long.valueOf(mResolver.notifyChangeList.size()),
- 1L,
- "Content observer was notified exactly once by " + operation);
-
- final Uri uri = mResolver.notifyChangeList.poll();
-
- mAsserter.isnot(uri,
- null,
- "Notification from " + operation + " was valid");
-
- mAsserter.ok(uri.toString().startsWith(expectedUri),
- "Content observer was notified exactly once by " + operation,
- "");
- }
- }
-
- private class TestStateSequencing extends TestCase {
- @Override
- protected void test() throws Exception {
- final ReadingListAccessor accessor = getTestProfile().getDB().getReadingListAccessor();
- final ContentResolver cr = getActivity().getContentResolver();
- final Uri syncURI = CONTENT_URI.buildUpon()
- .appendQueryParameter(BrowserContract.PARAM_IS_SYNC, "1")
- .appendQueryParameter(BrowserContract.PARAM_SHOW_DELETED, "1")
- .build();
-
- mAsserter.ok(accessor != null, "We have an accessor.", null);
-
- // Verify that the accessor thinks we're empty.
- mAsserter.ok(0 == accessor.getCount(cr), "We have no items.", null);
-
- // Insert an item via the accessor.
- final long addedItem = accessor.addBasicReadingListItem(cr, "http://example.org/", "Example A");
-
- mAsserter.ok(1 == accessor.getCount(cr), "We have one item.", null);
- final Cursor cursor = accessor.getReadingList(cr);
- try {
- mAsserter.ok(cursor.moveToNext(), "The cursor isn't empty.", null);
- mAsserter.ok(1 == cursor.getCount(), "The cursor agrees.", null);
- } finally {
- cursor.close();
- }
-
- // Verify that it has no GUID, that its state is NEW, etc.
- // This requires fetching more fields than the accessor uses.
- final Cursor all = getEverything(syncURI);
- try {
- mAsserter.ok(all.moveToNext(), "The cursor isn't empty.", null);
- mAsserter.ok(1 == all.getCount(), "The cursor agrees.", null);
-
- ContentValues expected = new ContentValues();
- expected.putNull(GUID);
- expected.put(SYNC_STATUS, SYNC_STATUS_NEW);
- expected.put(SYNC_CHANGE_FLAGS, SYNC_CHANGE_NONE);
- expected.put(URL, "http://example.org/");
- expected.put(TITLE, "Example A");
- expected.putNull(RESOLVED_URL);
- expected.putNull(RESOLVED_TITLE);
-
- final String[] testColumns = {GUID, SYNC_STATUS, SYNC_CHANGE_FLAGS, URL, TITLE, RESOLVED_URL, RESOLVED_TITLE, URL, TITLE};
- assertRowEqualsContentValues(all, expected, false, testColumns);
- } finally {
- all.close();
- }
-
- // Pretend that it was just synced.
- final long serverTime = System.currentTimeMillis();
- final ContentValues wasSynced = new ContentValues();
- wasSynced.put(GUID, "eeeeeeeeeeeeee");
- wasSynced.put(SYNC_STATUS, SYNC_STATUS_SYNCED);
- wasSynced.put(SYNC_CHANGE_FLAGS, SYNC_CHANGE_NONE);
- wasSynced.put(SERVER_STORED_ON, serverTime);
- wasSynced.put(SERVER_LAST_MODIFIED, serverTime);
-
- mAsserter.ok(1 == mProvider.update(syncURI, wasSynced, _ID + " = " + addedItem, null), "Updated one item.", null);
- final Cursor afterSync = getEverything(syncURI);
- try {
- mAsserter.ok(afterSync.moveToNext(), "The cursor isn't empty.", null);
- final String[] testColumns = {GUID, SYNC_STATUS, SYNC_CHANGE_FLAGS, SERVER_STORED_ON, SERVER_LAST_MODIFIED};
- assertRowEqualsContentValues(afterSync, wasSynced, false, testColumns);
- } finally {
- afterSync.close();
- }
-
- // Make changes to the record that exercise the various change flags, verifying that
- // the correct flags are set.
- final long beforeMarkedRead = System.currentTimeMillis();
- accessor.markAsRead(cr, addedItem);
- final ContentValues markedAsRead = new ContentValues();
- markedAsRead.put(GUID, "eeeeeeeeeeeeee");
- markedAsRead.put(SYNC_STATUS, SYNC_STATUS_MODIFIED);
- markedAsRead.put(SYNC_CHANGE_FLAGS, SYNC_CHANGE_UNREAD_CHANGED);
- markedAsRead.put(IS_UNREAD, 0);
-
- final Cursor afterMarkedRead = getEverything(syncURI);
- try {
- mAsserter.ok(afterMarkedRead.moveToNext(), "The cursor isn't empty.", null);
- final String[] testColumns = {GUID, SYNC_STATUS, SYNC_CHANGE_FLAGS, IS_UNREAD};
- assertRowEqualsContentValues(afterMarkedRead, markedAsRead, false, testColumns);
- assertModifiedInRange(afterMarkedRead, beforeMarkedRead);
- } finally {
- afterMarkedRead.close();
- }
-
- // Now our content is here!
- final long beforeContentUpdated = System.currentTimeMillis();
- accessor.updateContent(cr, addedItem, "New title", "http://www.example.com/article", "The excerpt is long.");
-
- // After this the content status should have changed, and we should be flagged to sync.
- final ContentValues contentUpdated = new ContentValues();
- contentUpdated.put(GUID, "eeeeeeeeeeeeee");
- contentUpdated.put(SYNC_STATUS, SYNC_STATUS_MODIFIED);
- contentUpdated.put(SYNC_CHANGE_FLAGS, SYNC_CHANGE_UNREAD_CHANGED | SYNC_CHANGE_RESOLVED);
- contentUpdated.put(IS_UNREAD, 0);
- contentUpdated.put(CONTENT_STATUS, STATUS_FETCHED_ARTICLE);
-
- final Cursor afterContentUpdated = getEverything(syncURI);
- try {
- mAsserter.ok(afterContentUpdated.moveToNext(), "The cursor isn't empty.", null);
- final String[] testColumns = {GUID, SYNC_STATUS, SYNC_CHANGE_FLAGS, IS_UNREAD, CONTENT_STATUS};
- assertRowEqualsContentValues(afterContentUpdated, contentUpdated, false, testColumns);
- assertModifiedInRange(afterContentUpdated, beforeContentUpdated);
- } finally {
- afterContentUpdated.close();
- }
-
- // Delete the record, and verify that its Sync state is DELETED.
- final long beforeDeletion = System.currentTimeMillis();
- accessor.deleteItem(cr, addedItem);
- final ContentValues itemDeleted = new ContentValues();
- itemDeleted.put(GUID, "eeeeeeeeeeeeee");
- itemDeleted.put(SYNC_STATUS, SYNC_STATUS_DELETED);
- itemDeleted.put(SYNC_CHANGE_FLAGS, SYNC_CHANGE_NONE);
- // TODO: CONTENT_STATUS on deletion?
-
- final Cursor afterDeletion = getEverything(syncURI);
- try {
- mAsserter.ok(afterDeletion.moveToNext(), "The cursor isn't empty.", null);
- final String[] testColumns = {GUID, SYNC_STATUS, SYNC_CHANGE_FLAGS};
- assertRowEqualsContentValues(afterDeletion, itemDeleted, false, testColumns);
- assertModifiedInRange(afterDeletion, beforeDeletion);
- } finally {
- afterDeletion.close();
- }
-
- // The accessor will no longer return the record.
- mAsserter.ok(0 == accessor.getCount(cr), "No items found.", null);
-
- // Add a new record as Sync -- it should start in state SYNCED.
- final ContentValues newRecord = new ContentValues();
- final long newServerTime = System.currentTimeMillis() - 50000;
- newRecord.put(GUID, "ffeeeeeeeeeeee");
- newRecord.put(SYNC_STATUS, SYNC_STATUS_SYNCED);
- newRecord.put(SYNC_CHANGE_FLAGS, SYNC_CHANGE_NONE);
- newRecord.put(SERVER_STORED_ON, newServerTime);
- newRecord.put(SERVER_LAST_MODIFIED, newServerTime);
- newRecord.put(CLIENT_LAST_MODIFIED, System.currentTimeMillis());
- newRecord.put(URL, "http://www.mozilla.org/");
- newRecord.put(TITLE, "Mozilla");
-
- final long newID = ContentUris.parseId(cr.insert(syncURI, newRecord));
- mAsserter.ok(newID > 0, "New ID is greater than 0.", null);
- mAsserter.ok(newID != addedItem, "New ID differs from last ID.", null);
-
- final Cursor afterNewInsert = getEverything(syncURI);
- try {
- mAsserter.ok(afterNewInsert.moveToNext(), "The cursor isn't empty.", null);
- mAsserter.ok(2 == afterNewInsert.getCount(), "The cursor has two rows.", null);
-
- // Default sort order means newest first.
- final String[] testColumns = {GUID, SYNC_STATUS, SYNC_CHANGE_FLAGS, SERVER_STORED_ON, SERVER_LAST_MODIFIED, CLIENT_LAST_MODIFIED, URL, TITLE};
- assertRowEqualsContentValues(afterNewInsert, newRecord, false, testColumns);
- } finally {
- afterNewInsert.close();
- }
-
- // Make a change to it. Verify that it's now changed with the right flags.
- final long beforeNewRead = System.currentTimeMillis();
- accessor.markAsRead(cr, newID);
- newRecord.put(SYNC_STATUS, SYNC_STATUS_MODIFIED);
- newRecord.put(SYNC_CHANGE_FLAGS, SYNC_CHANGE_UNREAD_CHANGED);
-
- final Cursor afterNewRead = getEverything(syncURI);
- try {
- mAsserter.ok(afterNewRead.moveToNext(), "The cursor isn't empty.", null);
- mAsserter.ok(2 == afterNewRead.getCount(), "The cursor has two rows.", null);
-
- // Default sort order means newest first.
- final String[] testColumns = {GUID, SYNC_STATUS, SYNC_CHANGE_FLAGS, SERVER_STORED_ON, SERVER_LAST_MODIFIED, URL, TITLE};
- assertRowEqualsContentValues(afterNewRead, newRecord, false, testColumns);
- assertModifiedInRange(afterNewRead, beforeNewRead);
- } finally {
- afterNewRead.close();
- }
- }
-
- private void assertModifiedInRange(Cursor cursor, long earliest) {
- final long dbModified = cursor.getLong(cursor.getColumnIndexOrThrow(CLIENT_LAST_MODIFIED));
- mAsserter.ok(dbModified >= earliest, "DB timestamp is at least as late as earliest.", null);
- mAsserter.ok(dbModified <= System.currentTimeMillis(), "DB timestamp is earlier than now.", null);
- }
- }
-
- /**
- * Removes all items from the DB.
- */
- private void ensureEmptyDatabase() {
- getWritableDatabase(CONTENT_URI).delete(TABLE_NAME, null, null);
- }
-
-
- private SQLiteDatabase getWritableDatabase(Uri uri) {
- Uri testUri = appendUriParam(uri, BrowserContract.PARAM_IS_TEST, "1");
- DelegatingTestContentProvider delegateProvider = (DelegatingTestContentProvider) mProvider;
- ReadingListProvider readingListProvider = (ReadingListProvider) delegateProvider.getTargetProvider();
- return readingListProvider.getWritableDatabaseForTesting(testUri);
- }
-
- /**
- * Checks that the values in the cursor's current row match those
- * in the ContentValues object.
- *
- * @param testColumns
- * @param cursorWithActual over the row to be checked
- * @param expectedValues to be checked
- */
- private void assertRowEqualsContentValues(Cursor cursorWithActual, ContentValues expectedValues, boolean compareDateModified, String[] testColumns) {
- for (String column: testColumns) {
- String expected = expectedValues.getAsString(column);
- String actual = cursorWithActual.getString(cursorWithActual.getColumnIndex(column));
- mAsserter.is(actual, expected, "Item has correct " + column);
- }
-
- if (compareDateModified) {
- String expected = expectedValues.getAsString(CLIENT_LAST_MODIFIED);
- String actual = cursorWithActual.getString(cursorWithActual.getColumnIndex(CLIENT_LAST_MODIFIED));
- mAsserter.is(actual, expected, "Item has correct " + CLIENT_LAST_MODIFIED);
- }
- }
-
- private void assertRowEqualsContentValues(Cursor cursorWithActual, ContentValues expectedValues) {
- assertRowEqualsContentValues(cursorWithActual, expectedValues, true, TEST_COLUMNS);
- }
-
- private ContentValues fillContentValues(String title, String url, String excerpt) {
- ContentValues values = new ContentValues();
-
- values.put(TITLE, title);
- values.put(URL, url);
- values.put(EXCERPT, excerpt);
- values.put(ADDED_ON, System.currentTimeMillis());
-
- return values;
- }
-
- private ContentValues createFillerReadingListItem() {
- Random rand = new Random();
- return fillContentValues("Example", "http://example.com/?num=" + rand.nextInt(), "foo bar");
- }
-
- private Cursor getEverything(Uri uri) {
- return mProvider.query(uri,
- null,
- null,
- null,
- null);
- }
-
- private Cursor getItemById(Uri uri, long id, String[] projection) {
- return mProvider.query(uri, projection,
- _ID + " = ?",
- new String[] { String.valueOf(id) },
- null);
- }
-
- private Cursor getItemById(long id) {
- return getItemById(CONTENT_URI, id, null);
- }
-
- private Cursor getItemById(Uri uri, long id) {
- return getItemById(uri, id, null);
- }
-
- /**
- * Verifies that ContentProvider insertions have been tested.
- */
- private void ensureCanInsert() {
- if (!mContentProviderInsertTested) {
- mAsserter.ok(false, "ContentProvider insertions have not been tested yet.", "");
- }
- }
-
- /**
- * Verifies that ContentProvider updates have been tested.
- */
- private void ensureCanUpdate() {
- if (!mContentProviderUpdateTested) {
- mAsserter.ok(false, "ContentProvider updates have not been tested yet.", "");
- }
- }
-
- private long insertAnItemWithAssertion() {
- // We should be able to insert into the DB.
- ensureCanInsert();
-
- ContentValues v = createFillerReadingListItem();
- long id = ContentUris.parseId(mProvider.insert(CONTENT_URI, v));
-
- assertItemExistsByID(id, "Inserted item found");
- return id;
- }
-
- private void assertItemExistsByID(Uri uri, long id, String msg) {
- Cursor c = getItemById(uri, id);
- try {
- mAsserter.ok(c.moveToFirst(), msg, "");
- } finally {
- c.close();
- }
- }
-
- private void assertItemExistsByID(long id, String msg) {
- Cursor c = getItemById(id);
- try {
- mAsserter.ok(c.moveToFirst(), msg, "");
- } finally {
- c.close();
- }
- }
-
- private void assertItemDoesNotExistByID(long id, String msg) {
- Cursor c = getItemById(id);
- try {
- mAsserter.ok(!c.moveToFirst(), msg, "");
- } finally {
- c.close();
- }
- }
-
- private void assertItemDoesNotExistByID(Uri uri, long id, String msg) {
- Cursor c = getItemById(uri, id);
- try {
- mAsserter.ok(!c.moveToFirst(), msg, "");
- } finally {
- c.close();
- }
- }
-}