--- a/mobile/android/base/java/org/mozilla/gecko/db/BrowserContract.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/BrowserContract.java
@@ -512,17 +512,33 @@ public class BrowserContract {
public static final String URL = "url";
public static final String KEY = "key";
public static final String VALUE = "value";
public static final String SYNC_STATUS = "sync_status";
public enum Key {
// We use a parameter, rather than name(), as defensive coding: we can't let the
// enum name change because we've already stored values into the DB.
- SCREENSHOT ("screenshot");
+ SCREENSHOT ("screenshot"),
+
+ /**
+ * This key maps URLs to its feeds.
+ *
+ * Key: feed
+ * Value: URL of feed
+ */
+ FEED("feed"),
+
+ /**
+ * This key maps URLs of feeds to an object describing the feed.
+ *
+ * Key: feed_subscription
+ * Value: JSON object describing feed
+ */
+ FEED_SUBSCRIPTION("feed_subscription");
private final String dbValue;
Key(final String dbValue) { this.dbValue = dbValue; }
public String getDbValue() { return dbValue; }
}
public enum SyncStatus {
--- a/mobile/android/base/java/org/mozilla/gecko/db/BrowserProvider.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/BrowserProvider.java
@@ -465,16 +465,21 @@ public class BrowserProvider extends Sha
// fall through
case THUMBNAILS: {
trace("Deleting thumbnails: " + uri);
beginWrite(db);
deleted = deleteThumbnails(uri, selection, selectionArgs);
break;
}
+ case URL_ANNOTATIONS:
+ trace("Delete on URL_ANNOTATIONS: " + uri);
+ deleteUrlAnnotation(uri, selection, selectionArgs);
+ break;
+
default: {
Table table = findTableFor(match);
if (table == null) {
throw new UnsupportedOperationException("Unknown delete URI " + uri);
}
trace("Deleting TABLE: " + uri);
beginWrite(db);
deleted = table.delete(db, uri, match, selection, selectionArgs);
@@ -647,16 +652,20 @@ public class BrowserProvider extends Sha
new String[] { url });
} else {
updated = updateExistingThumbnail(uri, values, Thumbnails.URL + " = ?",
new String[] { url });
}
break;
}
+ case URL_ANNOTATIONS:
+ updateUrlAnnotation(uri, values, selection, selectionArgs);
+ break;
+
default: {
Table table = findTableFor(match);
if (table == null) {
throw new UnsupportedOperationException("Unknown update URI " + uri);
}
trace("Update TABLE: " + uri);
beginWrite(db);
@@ -1272,17 +1281,17 @@ public class BrowserProvider extends Sha
values.put(Bookmarks.DATE_MODIFIED, System.currentTimeMillis());
}
trace("Querying bookmarks to update on URI: " + uri);
final SQLiteDatabase db = getWritableDatabase(uri);
// Compute matching IDs.
final Cursor cursor = db.query(TABLE_BOOKMARKS, bookmarksProjection,
- selection, selectionArgs, null, null, null);
+ selection, selectionArgs, null, null, null);
// Now that we're done reading, open a transaction.
final String inClause;
try {
inClause = DBUtils.computeSQLInClauseFromLongs(cursor, Bookmarks._ID);
} finally {
cursor.close();
}
@@ -1515,16 +1524,30 @@ public class BrowserProvider extends Sha
final String url = values.getAsString(UrlAnnotations.URL);
trace("Inserting url annotations for URL: " + url);
final SQLiteDatabase db = getWritableDatabase(uri);
beginWrite(db);
return db.insertOrThrow(TABLE_URL_ANNOTATIONS, null, values);
}
+ private void deleteUrlAnnotation(final Uri uri, final String selection, final String[] selectionArgs) {
+ trace("Deleting url annotation for URI: " + uri);
+
+ final SQLiteDatabase db = getWritableDatabase(uri);
+ db.delete(TABLE_URL_ANNOTATIONS, selection, selectionArgs);
+ }
+
+ private void updateUrlAnnotation(final Uri uri, final ContentValues values, final String selection, final String[] selectionArgs) {
+ trace("Updating url annotation for URI: " + uri);
+
+ final SQLiteDatabase db = getWritableDatabase(uri);
+ db.update(TABLE_URL_ANNOTATIONS, values, selection, selectionArgs);
+ }
+
private int updateOrInsertThumbnail(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
return updateThumbnail(uri, values, selection, selectionArgs,
true /* insert if needed */);
}
private int updateExistingThumbnail(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
--- a/mobile/android/base/java/org/mozilla/gecko/db/LocalUrlAnnotations.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/LocalUrlAnnotations.java
@@ -5,39 +5,187 @@
package org.mozilla.gecko.db;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import android.util.Log;
+
+import org.json.JSONException;
import org.mozilla.gecko.annotation.RobocopTarget;
import org.mozilla.gecko.db.BrowserContract.UrlAnnotations.Key;
+import org.mozilla.gecko.feeds.subscriptions.FeedSubscription;
public class LocalUrlAnnotations implements UrlAnnotations {
+ private static final String LOGTAG = "LocalUrlAnnotations";
+
private Uri urlAnnotationsTableWithProfile;
public LocalUrlAnnotations(final String profile) {
urlAnnotationsTableWithProfile = DBUtils.appendProfile(profile, BrowserContract.UrlAnnotations.CONTENT_URI);
}
+ /**
+ * Get all feed subscriptions.
+ */
+ @Override
+ public Cursor getFeedSubscriptions(ContentResolver cr) {
+ return queryByKey(cr,
+ Key.FEED_SUBSCRIPTION,
+ new String[] { BrowserContract.UrlAnnotations.URL, BrowserContract.UrlAnnotations.VALUE },
+ null);
+ }
+
+ /**
+ * Insert mapping from website URL to URL of the feed.
+ */
+ @Override
+ public void insertFeedUrl(ContentResolver cr, String originUrl, String feedUrl) {
+ insertAnnotation(cr, originUrl, Key.FEED, feedUrl);
+ }
+
+ /**
+ * Returns true if there's a mapping from the given website URL to a feed URL. False otherwise.
+ */
+ @Override
+ public boolean hasFeedUrlForWebsite(ContentResolver cr, String websiteUrl) {
+ return hasResultsForSelection(cr,
+ BrowserContract.UrlAnnotations.URL + " = ? AND " + BrowserContract.UrlAnnotations.KEY + " = ?",
+ new String[]{websiteUrl, Key.FEED.getDbValue()});
+ }
+
+ /**
+ * Returns true if there's a website URL with this feed URL. False otherwise.
+ */
+ @Override
+ public boolean hasWebsiteForFeedUrl(ContentResolver cr, String feedUrl) {
+ return hasResultsForSelection(cr,
+ BrowserContract.UrlAnnotations.VALUE + " = ? AND " + BrowserContract.UrlAnnotations.KEY + " = ?",
+ new String[]{feedUrl, Key.FEED.getDbValue()});
+ }
+
+ /**
+ * Delete the feed URL mapping for this website URL.
+ */
+ @Override
+ public void deleteFeedUrl(ContentResolver cr, String websiteUrl) {
+ deleteAnnotation(cr, websiteUrl, Key.FEED);
+ }
+
+ /**
+ * Get website URLs that are mapped to the given feed URL.
+ */
+ @Override
+ public Cursor getWebsitesWithFeedUrl(ContentResolver cr) {
+ return cr.query(urlAnnotationsTableWithProfile,
+ new String[] { BrowserContract.UrlAnnotations.URL },
+ BrowserContract.UrlAnnotations.KEY + " = ?",
+ new String[] { Key.FEED.getDbValue() },
+ null);
+ }
+
+ /**
+ * Returns true if there's a subscription for this feed URL. False otherwise.
+ */
+ @Override
+ public boolean hasFeedSubscription(ContentResolver cr, String feedUrl) {
+ return hasResultsForSelection(cr,
+ BrowserContract.UrlAnnotations.URL + " = ? AND " + BrowserContract.UrlAnnotations.KEY + " = ?",
+ new String[]{feedUrl, Key.FEED_SUBSCRIPTION.getDbValue()});
+ }
+
+ /**
+ * Insert the given feed subscription (Mapping from feed URL to the subscription object).
+ */
+ @Override
+ public void insertFeedSubscription(ContentResolver cr, FeedSubscription subscription) {
+ try {
+ insertAnnotation(cr, subscription.getFeedUrl(), Key.FEED_SUBSCRIPTION, subscription.toJSON().toString());
+ } catch (JSONException e) {
+ Log.w(LOGTAG, "Could not serialize subscription");
+ }
+ }
+
+ /**
+ * Update the feed subscription with new values.
+ */
+ @Override
+ public void updateFeedSubscription(ContentResolver cr, FeedSubscription subscription) {
+ try {
+ updateAnnotation(cr, subscription.getFeedUrl(), Key.FEED_SUBSCRIPTION, subscription.toJSON().toString());
+ } catch (JSONException e) {
+ Log.w(LOGTAG, "Could not serialize subscription");
+ }
+ }
+
+ /**
+ * Delete the subscription for the feed URL.
+ */
+ @Override
+ public void deleteFeedSubscription(ContentResolver cr, FeedSubscription subscription) {
+ deleteAnnotation(cr, subscription.getFeedUrl(), Key.FEED_SUBSCRIPTION);
+ }
+
+ private int deleteAnnotation(final ContentResolver cr, final String url, final Key key) {
+ return cr.delete(urlAnnotationsTableWithProfile,
+ BrowserContract.UrlAnnotations.KEY + " = ? AND " + BrowserContract.UrlAnnotations.URL + " = ?",
+ new String[] { key.getDbValue(), url });
+ }
+
+ private int updateAnnotation(final ContentResolver cr, final String url, final Key key, final String value) {
+ ContentValues values = new ContentValues();
+ values.put(BrowserContract.UrlAnnotations.VALUE, value);
+ values.put(BrowserContract.UrlAnnotations.DATE_MODIFIED, System.currentTimeMillis());
+
+ return cr.update(urlAnnotationsTableWithProfile,
+ values,
+ BrowserContract.UrlAnnotations.KEY + " = ? AND " + BrowserContract.UrlAnnotations.URL + " = ?",
+ new String[]{key.getDbValue(), url});
+ }
+
+ private void insertAnnotation(final ContentResolver cr, final String url, final Key key, final String value) {
+ insertAnnotation(cr, url, key.getDbValue(), value);
+ }
+
@RobocopTarget
@Override
public void insertAnnotation(final ContentResolver cr, final String url, final String key, final String value) {
final long creationTime = System.currentTimeMillis();
final ContentValues values = new ContentValues(5);
values.put(BrowserContract.UrlAnnotations.URL, url);
values.put(BrowserContract.UrlAnnotations.KEY, key);
values.put(BrowserContract.UrlAnnotations.VALUE, value);
values.put(BrowserContract.UrlAnnotations.DATE_CREATED, creationTime);
values.put(BrowserContract.UrlAnnotations.DATE_MODIFIED, creationTime);
cr.insert(urlAnnotationsTableWithProfile, values);
}
+ /**
+ * @return true if the table contains rows for the given selection.
+ */
+ private boolean hasResultsForSelection(ContentResolver cr, String selection, String[] selectionArgs) {
+ Cursor cursor = cr.query(urlAnnotationsTableWithProfile,
+ new String[] { BrowserContract.UrlAnnotations._ID },
+ selection,
+ selectionArgs,
+ null);
+ if (cursor == null) {
+ return false;
+ }
+
+ try {
+ return cursor.getCount() > 0;
+ } finally {
+ cursor.close();
+ }
+ }
+
private Cursor queryByKey(final ContentResolver cr, @NonNull final Key key, @Nullable final String[] projections,
@Nullable final String sortOrder) {
return cr.query(urlAnnotationsTableWithProfile,
projections,
BrowserContract.UrlAnnotations.KEY + " = ?", new String[] { key.getDbValue() },
sortOrder);
}
--- a/mobile/android/base/java/org/mozilla/gecko/db/StubBrowserDB.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/StubBrowserDB.java
@@ -12,16 +12,17 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.json.JSONObject;
import org.mozilla.gecko.annotation.RobocopTarget;
import org.mozilla.gecko.distribution.Distribution;
import org.mozilla.gecko.favicons.decoders.LoadFaviconResult;
import org.mozilla.gecko.Tab;
+import org.mozilla.gecko.feeds.subscriptions.FeedSubscription;
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;
@@ -154,16 +155,46 @@ class StubUrlAnnotations implements UrlA
@Override
public void insertAnnotation(ContentResolver cr, String url, String key, String value) {}
@Override
public Cursor getScreenshots(ContentResolver cr) { return null; }
@Override
public void insertScreenshot(ContentResolver cr, String pageUrl, final String screenshotLocation) {}
+
+ @Override
+ public Cursor getFeedSubscriptions(ContentResolver cr) { return null; }
+
+ @Override
+ public Cursor getWebsitesWithFeedUrl(ContentResolver cr) { return null; }
+
+ @Override
+ public void deleteFeedUrl(ContentResolver cr, String websiteUrl) {}
+
+ @Override
+ public boolean hasWebsiteForFeedUrl(ContentResolver cr, String feedUrl) { return false; }
+
+ @Override
+ public void deleteFeedSubscription(ContentResolver cr, FeedSubscription subscription) {}
+
+ @Override
+ public void updateFeedSubscription(ContentResolver cr, FeedSubscription subscription) {}
+
+ @Override
+ public boolean hasFeedSubscription(ContentResolver cr, String feedUrl) { return false; }
+
+ @Override
+ public void insertFeedSubscription(ContentResolver cr, FeedSubscription subscription) {}
+
+ @Override
+ public boolean hasFeedUrlForWebsite(ContentResolver cr, String websiteUrl) { return false; }
+
+ @Override
+ public void insertFeedUrl(ContentResolver cr, String originUrl, String feedUrl) {}
}
/*
* 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();
--- a/mobile/android/base/java/org/mozilla/gecko/db/UrlAnnotations.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/UrlAnnotations.java
@@ -2,15 +2,27 @@
* 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.database.Cursor;
import org.mozilla.gecko.annotation.RobocopTarget;
+import org.mozilla.gecko.feeds.subscriptions.FeedSubscription;
public interface UrlAnnotations {
@RobocopTarget void insertAnnotation(ContentResolver cr, String url, String key, String value);
Cursor getScreenshots(ContentResolver cr);
void insertScreenshot(ContentResolver cr, String pageUrl, String screenshotPath);
+
+ Cursor getFeedSubscriptions(ContentResolver cr);
+ Cursor getWebsitesWithFeedUrl(ContentResolver cr);
+ void deleteFeedUrl(ContentResolver cr, String websiteUrl);
+ boolean hasWebsiteForFeedUrl(ContentResolver cr, String feedUrl);
+ void deleteFeedSubscription(ContentResolver cr, FeedSubscription subscription);
+ void updateFeedSubscription(ContentResolver cr, FeedSubscription subscription);
+ boolean hasFeedSubscription(ContentResolver cr, String feedUrl);
+ void insertFeedSubscription(ContentResolver cr, FeedSubscription subscription);
+ boolean hasFeedUrlForWebsite(ContentResolver cr, String websiteUrl);
+ void insertFeedUrl(ContentResolver cr, String originUrl, String feedUrl);
}
--- a/mobile/android/base/java/org/mozilla/gecko/feeds/FeedService.java
+++ b/mobile/android/base/java/org/mozilla/gecko/feeds/FeedService.java
@@ -12,24 +12,25 @@ import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.support.annotation.Nullable;
import android.support.v4.net.ConnectivityManagerCompat;
import android.util.Log;
import com.keepsafe.switchboard.SwitchBoard;
import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.GeckoProfile;
import org.mozilla.gecko.GeckoSharedPrefs;
+import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.feeds.action.BaseAction;
import org.mozilla.gecko.feeds.action.CheckAction;
import org.mozilla.gecko.feeds.action.EnrollAction;
import org.mozilla.gecko.feeds.action.SetupAction;
import org.mozilla.gecko.feeds.action.SubscribeAction;
import org.mozilla.gecko.feeds.action.WithdrawAction;
-import org.mozilla.gecko.feeds.subscriptions.SubscriptionStorage;
import org.mozilla.gecko.preferences.GeckoPreferences;
import org.mozilla.gecko.util.Experiments;
/**
* Background service for subscribing to and checking website feeds to notify the user about updates.
*/
public class FeedService extends IntentService {
private static final String LOGTAG = "GeckoFeedService";
@@ -41,39 +42,44 @@ public class FeedService extends IntentS
public static final String ACTION_WITHDRAW = AppConstants.ANDROID_PACKAGE_NAME + ".FEEDS.WITHDRAW";
public static void setup(Context context) {
Intent intent = new Intent(context, FeedService.class);
intent.setAction(ACTION_SETUP);
context.startService(intent);
}
- public static void subscribe(Context context, String guid, String feedUrl) {
+ public static void subscribe(Context context, String feedUrl) {
Intent intent = new Intent(context, FeedService.class);
intent.setAction(ACTION_SUBSCRIBE);
- intent.putExtra(SubscribeAction.EXTRA_GUID, guid);
intent.putExtra(SubscribeAction.EXTRA_FEED_URL, feedUrl);
context.startService(intent);
}
- private SubscriptionStorage storage;
-
public FeedService() {
super(LOGTAG);
}
+ private BrowserDB browserDB;
+
@Override
public void onCreate() {
super.onCreate();
- storage = new SubscriptionStorage(getApplicationContext());
+ browserDB = GeckoProfile.get(this).getDB();
}
@Override
protected void onHandleIntent(Intent intent) {
+ if (intent == null) {
+ return;
+ }
+
+ Log.d(LOGTAG, "Service started with action: " + intent.getAction());
+
try {
if (!SwitchBoard.isInExperiment(this, Experiments.CONTENT_NOTIFICATIONS)) {
Log.d(LOGTAG, "Not in content notifications experiment. Skipping.");
return;
}
BaseAction action = createActionForIntent(intent);
if (action == null) {
@@ -88,49 +94,44 @@ public class FeedService extends IntentS
if (action.requiresNetwork() && !isConnectedToUnmeteredNetwork()) {
// For now just skip if we are not connected or the network is metered. We do not want
// to use precious mobile traffic.
Log.d(LOGTAG, "Not connected to a network or network is metered. Skipping.");
return;
}
- action.perform(intent);
-
- storage.persistChanges();
+ action.perform(browserDB, intent);
} finally {
FeedAlarmReceiver.completeWakefulIntent(intent);
}
}
@Nullable
private BaseAction createActionForIntent(Intent intent) {
- if (intent == null) {
- return null;
- }
+ final Context context = getApplicationContext();
switch (intent.getAction()) {
case ACTION_SETUP:
- return new SetupAction(this);
+ return new SetupAction(context);
case ACTION_SUBSCRIBE:
- return new SubscribeAction(storage);
+ return new SubscribeAction(context);
case ACTION_CHECK:
- return new CheckAction(this, storage);
+ return new CheckAction(context);
case ACTION_ENROLL:
- return new EnrollAction(this);
+ return new EnrollAction(context);
case ACTION_WITHDRAW:
- return new WithdrawAction(this, storage);
+ return new WithdrawAction(context);
default:
- Log.e(LOGTAG, "Unknown action: " + intent.getAction());
- return null;
+ throw new AssertionError("Unknown action: " + intent.getAction());
}
}
private boolean isConnectedToUnmeteredNetwork() {
ConnectivityManager manager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = manager.getActiveNetworkInfo();
if (networkInfo == null || !networkInfo.isConnected()) {
return false;
--- a/mobile/android/base/java/org/mozilla/gecko/feeds/action/BaseAction.java
+++ b/mobile/android/base/java/org/mozilla/gecko/feeds/action/BaseAction.java
@@ -2,25 +2,32 @@
* 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.feeds.action;
import android.content.Intent;
+import org.mozilla.gecko.db.BrowserDB;
+
/**
* Interface for actions run by FeedService.
*/
public interface BaseAction {
/**
* Perform this action.
- *
+ *
+ * @param browserDB database instance to perform the action.
* @param intent used to start the service.
*/
- void perform(Intent intent);
+ void perform(BrowserDB browserDB, Intent intent);
/**
* Does this action require an active network connection?
*/
boolean requiresNetwork();
+
+ /**
+ * Should this action only run if the preference is enabled?
+ */
boolean requiresPreferenceEnabled();
}
--- a/mobile/android/base/java/org/mozilla/gecko/feeds/action/CheckAction.java
+++ b/mobile/android/base/java/org/mozilla/gecko/feeds/action/CheckAction.java
@@ -3,79 +3,106 @@
* 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.feeds.action;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.ComponentName;
+import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
+import android.database.Cursor;
import android.net.Uri;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationManagerCompat;
import android.support.v4.content.ContextCompat;
import android.text.format.DateFormat;
import android.util.Log;
+import org.json.JSONException;
import org.mozilla.gecko.BrowserApp;
import org.mozilla.gecko.R;
+import org.mozilla.gecko.db.BrowserDB;
+import org.mozilla.gecko.db.UrlAnnotations;
import org.mozilla.gecko.feeds.FeedFetcher;
import org.mozilla.gecko.feeds.parser.Feed;
import org.mozilla.gecko.feeds.subscriptions.FeedSubscription;
-import org.mozilla.gecko.feeds.subscriptions.SubscriptionStorage;
import org.mozilla.gecko.util.StringUtils;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* CheckAction: Check if feeds we subscribed to have new content available.
*/
public class CheckAction implements BaseAction {
private static final String LOGTAG = "FeedCheckAction";
private Context context;
- private SubscriptionStorage storage;
- public CheckAction(Context context, SubscriptionStorage storage) {
+ public CheckAction(Context context) {
this.context = context;
- this.storage = storage;
}
@Override
- public void perform(Intent intent) {
- final List<FeedSubscription> subscriptions = storage.getSubscriptions();
-
- Log.d(LOGTAG, "Checking feeds for updates (" + subscriptions.size() + " feeds) ..");
+ public void perform(BrowserDB browserDB, Intent intent) {
+ final UrlAnnotations urlAnnotations = browserDB.getUrlAnnotations();
+ final ContentResolver resolver = context.getContentResolver();
+ final List<Feed> updatedFeeds = new ArrayList<>();
- List<Feed> updatedFeeds = new ArrayList<>();
+ Log.d(LOGTAG, "Checking feeds for updates..");
- for (FeedSubscription subscription : subscriptions) {
- Log.i(LOGTAG, "Checking feed: " + subscription.getFeedTitle());
+ Cursor cursor = urlAnnotations.getFeedSubscriptions(resolver);
+ if (cursor == null) {
+ return;
+ }
- FeedFetcher.FeedResponse response = fetchFeed(subscription);
- if (response == null) {
- continue;
- }
+ try {
+ while (cursor.moveToNext()) {
+ FeedSubscription subscription = FeedSubscription.fromCursor(cursor);
+
+ FeedFetcher.FeedResponse response = checkFeedForUpdates(subscription);
+ if (response != null) {
+ updatedFeeds.add(response.feed);
- if (subscription.isNewer(response)) {
- Log.d(LOGTAG, "* Feed has changed. New item: " + response.feed.getLastItem().getTitle());
-
- storage.updateSubscription(subscription, response);
-
- updatedFeeds.add(response.feed);
+ urlAnnotations.updateFeedSubscription(resolver, subscription);
+ }
}
+ } catch (JSONException e) {
+ Log.w(LOGTAG, "Could not deserialize subscription", e);
+ } finally {
+ cursor.close();
}
notify(updatedFeeds);
}
+ private FeedFetcher.FeedResponse checkFeedForUpdates(FeedSubscription subscription) {
+ Log.i(LOGTAG, "Checking feed: " + subscription.getFeedTitle());
+
+ FeedFetcher.FeedResponse response = fetchFeed(subscription);
+ if (response == null) {
+ return null;
+ }
+
+ if (subscription.isNewer(response)) {
+ Log.d(LOGTAG, "* Feed has changed. New item: " + response.feed.getLastItem().getTitle());
+
+ subscription.update(response);
+
+ return response;
+
+ }
+
+ return null;
+ }
+
private void notify(List<Feed> updatedFeeds) {
final int feedCount = updatedFeeds.size();
if (feedCount == 1) {
notifySingle(updatedFeeds.get(0));
} else if (feedCount > 1) {
notifyMultiple(updatedFeeds);
}
--- a/mobile/android/base/java/org/mozilla/gecko/feeds/action/EnrollAction.java
+++ b/mobile/android/base/java/org/mozilla/gecko/feeds/action/EnrollAction.java
@@ -1,24 +1,26 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* 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.feeds.action;
+import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.text.TextUtils;
import android.util.Log;
import org.mozilla.gecko.GeckoProfile;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.db.BrowserDB;
+import org.mozilla.gecko.db.UrlAnnotations;
import org.mozilla.gecko.feeds.FeedService;
import org.mozilla.gecko.feeds.knownsites.KnownSiteBlogger;
import org.mozilla.gecko.feeds.knownsites.KnownSite;
import org.mozilla.gecko.feeds.knownsites.KnownSiteMedium;
import org.mozilla.gecko.feeds.knownsites.KnownSiteWordpress;
/**
* EnrollAction: Search for bookmarks of known sites we can subscribe to.
@@ -34,54 +36,67 @@ public class EnrollAction implements Bas
private Context context;
public EnrollAction(Context context) {
this.context = context;
}
@Override
- public void perform(Intent intent) {
+ public void perform(BrowserDB db, Intent intent) {
Log.i(LOGTAG, "Searching for bookmarks to enroll in updates");
- BrowserDB db = GeckoProfile.get(context).getDB();
+ final ContentResolver contentResolver = context.getContentResolver();
for (KnownSite knownSite : knownSites) {
- searchFor(db, knownSite);
+ searchFor(db, contentResolver, knownSite);
}
}
@Override
public boolean requiresNetwork() {
return false;
}
@Override
public boolean requiresPreferenceEnabled() {
return true;
}
- private void searchFor(BrowserDB db, KnownSite knownSite) {
- Cursor cursor = db.getBookmarksForPartialUrl(context.getContentResolver(), "://" + knownSite.getURLSearchString() + "/");
+ private void searchFor(BrowserDB db, ContentResolver contentResolver, KnownSite knownSite) {
+ final UrlAnnotations urlAnnotations = db.getUrlAnnotations();
+
+ final Cursor cursor = db.getBookmarksForPartialUrl(contentResolver, knownSite.getURLSearchString());
if (cursor == null) {
- Log.d(LOGTAG, "Nothing found");
+ Log.d(LOGTAG, "Nothing found (" + knownSite.getClass().getSimpleName() + ")");
return;
}
try {
Log.d(LOGTAG, "Found " + cursor.getCount() + " websites");
while (cursor.moveToNext()) {
- final String guid = cursor.getString(cursor.getColumnIndex(BrowserContract.Bookmarks.GUID));
+
final String url = cursor.getString(cursor.getColumnIndex(BrowserContract.Bookmarks.URL));
- Log.d(LOGTAG, " (" + guid + ") " + url);
+ Log.d(LOGTAG, " URL: " + url);
String feedUrl = knownSite.getFeedFromURL(url);
- if (!TextUtils.isEmpty(feedUrl)) {
- FeedService.subscribe(context, guid, feedUrl);
+ if (TextUtils.isEmpty(feedUrl)) {
+
+
+ Log.w(LOGTAG, "Could not determine feed for URL: " + url);
+ }
+
+ if (!urlAnnotations.hasFeedUrlForWebsite(contentResolver, url)) {
+ urlAnnotations.insertFeedUrl(contentResolver, url, feedUrl);
+ }
+
+ if (!urlAnnotations.hasFeedSubscription(contentResolver, feedUrl)) {
+ FeedService.subscribe(context, feedUrl);
}
}
} finally {
cursor.close();
}
}
+
}
--- a/mobile/android/base/java/org/mozilla/gecko/feeds/action/SetupAction.java
+++ b/mobile/android/base/java/org/mozilla/gecko/feeds/action/SetupAction.java
@@ -7,33 +7,34 @@ package org.mozilla.gecko.feeds.action;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.SystemClock;
import android.util.Log;
+import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.feeds.FeedAlarmReceiver;
import org.mozilla.gecko.feeds.FeedService;
/**
* SetupAction: Set up alarms to run various actions every now and then.
*/
public class SetupAction implements BaseAction {
private static final String LOGTAG = "FeedSetupAction";
private Context context;
public SetupAction(Context context) {
this.context = context;
}
@Override
- public void perform(Intent intent) {
+ public void perform(BrowserDB browserDB, Intent intent) {
final AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
cancelPreviousAlarms(alarmManager);
scheduleAlarms(alarmManager);
}
@Override
public boolean requiresNetwork() {
--- a/mobile/android/base/java/org/mozilla/gecko/feeds/action/SubscribeAction.java
+++ b/mobile/android/base/java/org/mozilla/gecko/feeds/action/SubscribeAction.java
@@ -1,74 +1,73 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* 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.feeds.action;
+import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
+import org.mozilla.gecko.db.BrowserDB;
+import org.mozilla.gecko.db.UrlAnnotations;
import org.mozilla.gecko.feeds.FeedFetcher;
import org.mozilla.gecko.feeds.subscriptions.FeedSubscription;
-import org.mozilla.gecko.feeds.subscriptions.SubscriptionStorage;
-
-import java.util.UUID;
/**
* SubscribeAction: Try to fetch a feed and create a subscription if successful.
*/
public class SubscribeAction implements BaseAction {
private static final String LOGTAG = "FeedSubscribeAction";
- public static final String EXTRA_GUID = "guid";
public static final String EXTRA_FEED_URL = "feed_url";
- private SubscriptionStorage storage;
+ private Context context;
- public SubscribeAction(SubscriptionStorage storage) {
- this.storage = storage;
+ public SubscribeAction(Context context) {
+ this.context = context;
}
@Override
- public void perform(Intent intent) {
- final Bundle extras = intent.getExtras();
+ public void perform(BrowserDB browserDB, Intent intent) {
+ final UrlAnnotations urlAnnotations = browserDB.getUrlAnnotations();
- // TODO: Using a random UUID as fallback just so that I can subscribe for things that are not bookmarks (testing)
- final String guid = extras.getString(EXTRA_GUID, UUID.randomUUID().toString());
- final String feedUrl = intent.getStringExtra(EXTRA_FEED_URL);
+ final Bundle extras = intent.getExtras();
+ final String feedUrl = extras.getString(EXTRA_FEED_URL);
- if (storage.hasSubscriptionForBookmark(guid)) {
+ if (urlAnnotations.hasFeedSubscription(context.getContentResolver(), feedUrl)) {
Log.d(LOGTAG, "Already subscribed to " + feedUrl + ". Skipping.");
return;
}
Log.d(LOGTAG, "Subscribing to feed: " + feedUrl);
- subscribe(guid, feedUrl);
+ subscribe(urlAnnotations, feedUrl);
}
@Override
public boolean requiresNetwork() {
return true;
}
@Override
public boolean requiresPreferenceEnabled() {
return true;
}
- private void subscribe(String guid, String feedUrl) {
+ private void subscribe(UrlAnnotations urlAnnotations, String feedUrl) {
FeedFetcher.FeedResponse response = FeedFetcher.fetchAndParseFeed(feedUrl);
if (response == null) {
Log.w(LOGTAG, String.format("Could not fetch feed (%s). Not subscribing for now.", feedUrl));
return;
}
Log.d(LOGTAG, "Subscribing to feed: " + response.feed.getTitle());
- Log.d(LOGTAG, " GUID: " + guid);
Log.d(LOGTAG, " Last item: " + response.feed.getLastItem().getTitle());
- storage.addSubscription(FeedSubscription.create(guid, feedUrl, response));
+ final FeedSubscription subscription = FeedSubscription.create(feedUrl, response);
+
+ urlAnnotations.insertFeedSubscription(context.getContentResolver(), subscription);
}
}
--- a/mobile/android/base/java/org/mozilla/gecko/feeds/action/WithdrawAction.java
+++ b/mobile/android/base/java/org/mozilla/gecko/feeds/action/WithdrawAction.java
@@ -1,63 +1,103 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* 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.feeds.action;
+import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
+import android.database.Cursor;
import android.util.Log;
-import org.mozilla.gecko.GeckoProfile;
+import org.json.JSONException;
+import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.db.BrowserDB;
+import org.mozilla.gecko.db.UrlAnnotations;
import org.mozilla.gecko.feeds.subscriptions.FeedSubscription;
-import org.mozilla.gecko.feeds.subscriptions.SubscriptionStorage;
-
-import java.util.List;
/**
* WithdrawAction: Look for feeds to unsubscribe from.
*/
public class WithdrawAction implements BaseAction {
private static final String LOGTAG = "FeedWithdrawAction";
private Context context;
- private SubscriptionStorage storage;
- public WithdrawAction(Context context, SubscriptionStorage storage) {
+ public WithdrawAction(Context context) {
this.context = context;
- this.storage = storage;
}
@Override
- public void perform(Intent intent) {
- BrowserDB db = GeckoProfile.get(context).getDB();
+ public void perform(BrowserDB browserDB, Intent intent) {
+ Log.d(LOGTAG, "Searching for subscriptions to remove..");
+
+ final UrlAnnotations urlAnnotations = browserDB.getUrlAnnotations();
+ final ContentResolver resolver = context.getContentResolver();
+
+ removeFeedsOfUnknownUrls(browserDB, urlAnnotations, resolver);
+ removeSubscriptionsOfRemovedFeeds(urlAnnotations, resolver);
+ }
- List<FeedSubscription> subscriptions = storage.getSubscriptions();
+ /**
+ * Search for website URLs with a feed assigned. Remove entry if website URL is not known anymore:
+ * For now this means the website is not bookmarked.
+ */
+ private void removeFeedsOfUnknownUrls(BrowserDB browserDB, UrlAnnotations urlAnnotations, ContentResolver resolver) {
+ Cursor cursor = urlAnnotations.getWebsitesWithFeedUrl(resolver);
+ if (cursor == null) {
+ return;
+ }
+
+ try {
+ while (cursor.moveToNext()) {
+ final String url = cursor.getString(cursor.getColumnIndex(BrowserContract.UrlAnnotations.URL));
+
+ if (!browserDB.isBookmark(resolver, url)) {
+ Log.d(LOGTAG, "Removing feed for unknown URL: " + url);
- Log.d(LOGTAG, "Checking " + subscriptions.size() + " subscriptions");
+ urlAnnotations.deleteFeedUrl(resolver, url);
+ }
+ }
+ } finally {
+ cursor.close();
+ }
+ }
- for (FeedSubscription subscription : subscriptions) {
- if (!db.hasBookmarkWithGuid(context.getContentResolver(), subscription.getBookmarkGUID())) {
- unsubscribe(subscription);
+ /**
+ * Remove subscriptions of feed URLs that are not assigned to a website URL (anymore).
+ */
+ private void removeSubscriptionsOfRemovedFeeds(UrlAnnotations urlAnnotations, ContentResolver resolver) {
+ Cursor cursor = urlAnnotations.getFeedSubscriptions(resolver);
+ if (cursor == null) {
+ return;
+ }
+
+ try {
+ while (cursor.moveToNext()) {
+ final FeedSubscription subscription = FeedSubscription.fromCursor(cursor);
+
+ if (!urlAnnotations.hasWebsiteForFeedUrl(resolver, subscription.getFeedUrl())) {
+ Log.d(LOGTAG, "Removing subscription for feed: " + subscription.getFeedUrl());
+
+ urlAnnotations.deleteFeedSubscription(resolver, subscription);
+ }
}
+ } catch (JSONException e) {
+ Log.w(LOGTAG, "Could not deserialize subscription", e);
+ } finally {
+ cursor.close();
}
}
@Override
public boolean requiresNetwork() {
return false;
}
@Override
public boolean requiresPreferenceEnabled() {
return true;
}
-
- private void unsubscribe(FeedSubscription subscription) {
- Log.d(LOGTAG, "Unsubscribing from: (" + subscription.getBookmarkGUID() + ") " + subscription.getFeedUrl());
-
- storage.removeSubscription(subscription);
- }
}
--- a/mobile/android/base/java/org/mozilla/gecko/feeds/subscriptions/FeedSubscription.java
+++ b/mobile/android/base/java/org/mozilla/gecko/feeds/subscriptions/FeedSubscription.java
@@ -1,90 +1,81 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* 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.feeds.subscriptions;
+import android.database.Cursor;
import android.text.TextUtils;
import org.json.JSONException;
import org.json.JSONObject;
+import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.feeds.FeedFetcher;
import org.mozilla.gecko.feeds.parser.Item;
/**
* An object describing a subscription and containing some meta data about the last time we fetched
* the feed.
*/
public class FeedSubscription {
- private static final String JSON_KEY_FEED_URL = "feed_url";
private static final String JSON_KEY_FEED_TITLE = "feed_title";
- private static final String JSON_KEY_WEBSITE_URL = "website_url";
private static final String JSON_KEY_LAST_ITEM_TITLE = "last_item_title";
private static final String JSON_KEY_LAST_ITEM_URL = "last_item_url";
private static final String JSON_KEY_LAST_ITEM_TIMESTAMP = "last_item_timestamp";
private static final String JSON_KEY_ETAG = "etag";
private static final String JSON_KEY_LAST_MODIFIED = "last_modified";
- private static final String JSON_KEY_BOOKMARK_GUID = "bookmark_guid";
- private String bookmarkGuid; // Currently a subscription is linked to a bookmark
private String feedUrl;
private String feedTitle;
- private String websiteUrl;
private String lastItemTitle;
private String lastItemUrl;
private long lastItemTimestamp;
private String etag;
private String lastModified;
- public static FeedSubscription create(String bookmarkGuid, String url, FeedFetcher.FeedResponse response) {
+ public static FeedSubscription create(String feedUrl, FeedFetcher.FeedResponse response) {
FeedSubscription subscription = new FeedSubscription();
- subscription.bookmarkGuid = bookmarkGuid;
- subscription.feedUrl = url;
+ subscription.feedUrl = feedUrl;
subscription.update(response);
return subscription;
}
- public static FeedSubscription fromJSON(JSONObject object) throws JSONException {
- FeedSubscription subscription = new FeedSubscription();
+ public static FeedSubscription fromCursor(Cursor cursor) throws JSONException {
+ final FeedSubscription subscription = new FeedSubscription();
+ subscription.feedUrl = cursor.getString(cursor.getColumnIndex(BrowserContract.UrlAnnotations.URL));
- subscription.feedUrl = object.getString(JSON_KEY_FEED_URL);
- subscription.feedTitle = object.getString(JSON_KEY_FEED_TITLE);
- subscription.websiteUrl = object.getString(JSON_KEY_WEBSITE_URL);
- subscription.lastItemTitle = object.getString(JSON_KEY_LAST_ITEM_TITLE);
- subscription.lastItemUrl = object.getString(JSON_KEY_LAST_ITEM_URL);
- subscription.lastItemTimestamp = object.getLong(JSON_KEY_LAST_ITEM_TIMESTAMP);
- subscription.etag = object.optString(JSON_KEY_ETAG);
- subscription.lastModified = object.optString(JSON_KEY_LAST_MODIFIED);
- subscription.bookmarkGuid = object.getString(JSON_KEY_BOOKMARK_GUID);
+ final String value = cursor.getString(cursor.getColumnIndex(BrowserContract.UrlAnnotations.VALUE));
+ subscription.fromJSON(new JSONObject(value));
return subscription;
}
- /* package-private */ void update(FeedFetcher.FeedResponse response) {
- final String feedUrl = response.feed.getFeedURL();
- if (!TextUtils.isEmpty(feedUrl)) {
- // Prefer to use the URL we get from the feed for further requests
- this.feedUrl = feedUrl;
- }
+ private void fromJSON(JSONObject object) throws JSONException {
+ feedTitle = object.getString(JSON_KEY_FEED_TITLE);
+ lastItemTitle = object.getString(JSON_KEY_LAST_ITEM_TITLE);
+ lastItemUrl = object.getString(JSON_KEY_LAST_ITEM_URL);
+ lastItemTimestamp = object.getLong(JSON_KEY_LAST_ITEM_TIMESTAMP);
+ etag = object.optString(JSON_KEY_ETAG);
+ lastModified = object.optString(JSON_KEY_LAST_MODIFIED);
+ }
+ public void update(FeedFetcher.FeedResponse response) {
feedTitle = response.feed.getTitle();
- websiteUrl = response.feed.getWebsiteURL();
lastItemTitle = response.feed.getLastItem().getTitle();
lastItemUrl = response.feed.getLastItem().getURL();
lastItemTimestamp = response.feed.getLastItem().getTimestamp();
etag = response.etag;
lastModified = response.lastModified;
}
-
/**
* Guesstimate if this response is a newer representation of the feed.
*/
public boolean isNewer(FeedFetcher.FeedResponse response) {
final Item otherItem = response.feed.getLastItem();
if (lastItemTimestamp > otherItem.getTimestamp()) {
return true; // How to detect if this same item and it only has been updated?
@@ -106,56 +97,29 @@ public class FeedSubscription {
public String getFeedUrl() {
return feedUrl;
}
public String getFeedTitle() {
return feedTitle;
}
- public String getWebsiteUrl() {
- return websiteUrl;
- }
-
- public String getLastItemTitle() {
- return lastItemTitle;
- }
-
- public String getLastItemUrl() {
- return lastItemUrl;
- }
-
- public long getLastItemTimestamp() {
- return lastItemTimestamp;
- }
-
public String getETag() {
return etag;
}
public String getLastModified() {
return lastModified;
}
- public String getBookmarkGUID() {
- return bookmarkGuid;
- }
-
- public boolean isForTheSameBookmarkAs(FeedSubscription other) {
- return TextUtils.equals(bookmarkGuid, other.bookmarkGuid);
- }
-
public JSONObject toJSON() throws JSONException {
JSONObject object = new JSONObject();
- object.put(JSON_KEY_FEED_URL, feedUrl);
object.put(JSON_KEY_FEED_TITLE, feedTitle);
- object.put(JSON_KEY_WEBSITE_URL, websiteUrl);
object.put(JSON_KEY_LAST_ITEM_TITLE, lastItemTitle);
object.put(JSON_KEY_LAST_ITEM_URL, lastItemUrl);
object.put(JSON_KEY_LAST_ITEM_TIMESTAMP, lastItemTimestamp);
object.put(JSON_KEY_ETAG, etag);
object.put(JSON_KEY_LAST_MODIFIED, lastModified);
- object.put(JSON_KEY_BOOKMARK_GUID, bookmarkGuid);
return object;
}
}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/feeds/subscriptions/SubscriptionStorage.java
+++ /dev/null
@@ -1,220 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * 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.feeds.subscriptions;
-
-import android.content.Context;
-import android.support.v4.util.AtomicFile;
-import android.text.TextUtils;
-import android.util.Log;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.mozilla.gecko.feeds.FeedFetcher;
-
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.List;
-
-/**
- * Storage for feed subscriptions. This is just using a plain JSON file on disk.
- *
- * TODO: Store this data in the url metadata tablet instead (See bug 1250707)
- */
-public class SubscriptionStorage {
- private static final String LOGTAG = "FeedStorage";
- private static final String FILE_NAME = "feed_subscriptions";
-
- private static final String JSON_KEY_SUBSCRIPTIONS = "subscriptions";
-
- private final AtomicFile file; // Guarded by 'file'
-
- private List<FeedSubscription> subscriptions;
- private boolean hasLoadedSubscriptions;
- private boolean hasChanged;
-
- public SubscriptionStorage(Context context) {
- this(new AtomicFile(new File(context.getApplicationInfo().dataDir, FILE_NAME)));
-
- startLoadFromDisk();
- }
-
- // For injecting mocked AtomicFile objects during test
- protected SubscriptionStorage(AtomicFile file) {
- this.subscriptions = new ArrayList<>();
- this.file = file;
- }
-
- public synchronized void addSubscription(FeedSubscription subscription) {
- awaitLoadingSubscriptionsLocked();
-
- subscriptions.add(subscription);
- hasChanged = true;
- }
-
- public synchronized void removeSubscription(FeedSubscription subscription) {
- awaitLoadingSubscriptionsLocked();
-
- Iterator<FeedSubscription> iterator = subscriptions.iterator();
- while (iterator.hasNext()) {
- if (subscription.isForTheSameBookmarkAs(iterator.next())) {
- iterator.remove();
- hasChanged = true;
- return;
- }
- }
- }
-
- public synchronized List<FeedSubscription> getSubscriptions() {
- awaitLoadingSubscriptionsLocked();
-
- return new ArrayList<>(subscriptions);
- }
-
- public synchronized void updateSubscription(FeedSubscription subscription, FeedFetcher.FeedResponse response) {
- awaitLoadingSubscriptionsLocked();
-
- subscription.update(response);
-
- for (int i = 0; i < subscriptions.size(); i++) {
- if (subscriptions.get(i).isForTheSameBookmarkAs(subscription)) {
- subscriptions.set(i, subscription);
- hasChanged = true;
- return;
- }
- }
- }
-
- public synchronized boolean hasSubscriptionForBookmark(String guid) {
- awaitLoadingSubscriptionsLocked();
-
- for (int i = 0; i < subscriptions.size(); i++) {
- if (TextUtils.equals(guid, subscriptions.get(i).getBookmarkGUID())) {
- return true;
- }
- }
-
- return false;
- }
-
- private void awaitLoadingSubscriptionsLocked() {
- while (!hasLoadedSubscriptions) {
- try {
- Log.v(LOGTAG, "Waiting for subscriptions to be loaded");
-
- wait();
- } catch (InterruptedException e) {
- // Ignore
- }
- }
- }
-
- public void persistChanges() {
- new Thread(LOGTAG + "-Persist") {
- public void run() {
- writeToDisk();
- }
- }.start();
- }
-
- private void startLoadFromDisk() {
- new Thread(LOGTAG + "-Load") {
- public void run() {
- loadFromDisk();
- }
- }.start();
- }
-
- protected synchronized void loadFromDisk() {
- Log.d(LOGTAG, "Loading from disk");
-
- if (hasLoadedSubscriptions) {
- return;
- }
-
- List<FeedSubscription> subscriptions = new ArrayList<>();
-
- try {
- JSONObject data;
-
- synchronized (file) {
- data = new JSONObject(new String(file.readFully(), "UTF-8"));
- }
-
- JSONArray array = data.getJSONArray(JSON_KEY_SUBSCRIPTIONS);
- for (int i = 0; i < array.length(); i++) {
- subscriptions.add(FeedSubscription.fromJSON(array.getJSONObject(i)));
- }
- } catch (FileNotFoundException e) {
- Log.d(LOGTAG, "No subscriptions yet.");
- } catch (JSONException e) {
- Log.w(LOGTAG, "Unable to parse subscriptions JSON. Using empty list.", e);
- } catch (UnsupportedEncodingException e) {
- AssertionError error = new AssertionError("Should not happen: This device does not speak UTF-8");
- error.initCause(e);
- throw error;
- } catch (IOException e) {
- Log.d(LOGTAG, "Can't read subscriptions due to IOException", e);
- }
-
- onSubscriptionsLoaded(subscriptions);
-
- notifyAll();
-
- Log.d(LOGTAG, "Loaded " + subscriptions.size() + " elements");
- }
-
- protected void onSubscriptionsLoaded(List<FeedSubscription> subscriptions) {
- this.subscriptions = subscriptions;
- this.hasLoadedSubscriptions = true;
- }
-
- protected synchronized void writeToDisk() {
- if (!hasChanged) {
- Log.v(LOGTAG, "Not persisting: Subscriptions have not changed");
- return;
- }
-
- Log.d(LOGTAG, "Writing to disk");
-
- FileOutputStream outputStream = null;
-
- synchronized (file) {
- try {
- outputStream = file.startWrite();
-
- JSONArray array = new JSONArray();
- for (FeedSubscription subscription : this.subscriptions) {
- array.put(subscription.toJSON());
- }
-
- JSONObject catalog = new JSONObject();
- catalog.put(JSON_KEY_SUBSCRIPTIONS, array);
-
- outputStream.write(catalog.toString().getBytes("UTF-8"));
-
- file.finishWrite(outputStream);
-
- hasChanged = false;
- } catch (UnsupportedEncodingException e) {
- AssertionError error = new AssertionError("Should not happen: This device does not speak UTF-8");
- error.initCause(e);
- throw error;
- } catch (IOException | JSONException e) {
- Log.e(LOGTAG, "IOException during writing catalog", e);
-
- if (outputStream != null) {
- file.failWrite(outputStream);
- }
- }
- }
- }
-}
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -286,17 +286,16 @@ gbjar.sources += ['java/org/mozilla/geck
'feeds/knownsites/KnownSite.java',
'feeds/knownsites/KnownSiteBlogger.java',
'feeds/knownsites/KnownSiteMedium.java',
'feeds/knownsites/KnownSiteWordpress.java',
'feeds/parser/Feed.java',
'feeds/parser/Item.java',
'feeds/parser/SimpleFeedParser.java',
'feeds/subscriptions/FeedSubscription.java',
- 'feeds/subscriptions/SubscriptionStorage.java',
'FilePicker.java',
'FilePickerResultHandler.java',
'FindInPageBar.java',
'firstrun/DataPanel.java',
'firstrun/FirstrunAnimationContainer.java',
'firstrun/FirstrunPager.java',
'firstrun/FirstrunPagerConfig.java',
'firstrun/FirstrunPanel.java',