Bug 1241810 - Use UrlAnnotations table as storage. r=mcomella
authorSebastian Kaspari <s.kaspari@gmail.com>
Fri, 26 Feb 2016 17:51:17 -0800
changeset 290386 5b263728ff7830134d42163fe702c02ae76358b4
parent 290385 3727b211b1e2c86c250925d15258e9ff4a9fca7b
child 290387 d0fd7d32ee5eb5caeb0340ff2ba2e2ab65a9d415
push id19656
push usergwagner@mozilla.com
push dateMon, 04 Apr 2016 13:43:23 +0000
treeherderb2g-inbound@e99061fde28a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmcomella
bugs1241810
milestone48.0a1
Bug 1241810 - Use UrlAnnotations table as storage. r=mcomella This patch removes the JSON flat file based storage and instead uses the new url_annotations table. Two mappings will be created in the database: * Key "feed": website URL -> feed URL This maps an URL to its feed URL. Multiple URLs can have the same feed URL. Later this mapping could be used to highlight URLs with feeds or subscriptions in the UI. * Key "feed_subscription": feed URL -> Object describing the feed This is the actual subscription and saves the state of a subscription linked to the feed URL. MozReview-Commit-ID: EFVxAwbhT5o
mobile/android/base/java/org/mozilla/gecko/db/BrowserContract.java
mobile/android/base/java/org/mozilla/gecko/db/BrowserProvider.java
mobile/android/base/java/org/mozilla/gecko/db/LocalUrlAnnotations.java
mobile/android/base/java/org/mozilla/gecko/db/StubBrowserDB.java
mobile/android/base/java/org/mozilla/gecko/db/UrlAnnotations.java
mobile/android/base/java/org/mozilla/gecko/feeds/FeedService.java
mobile/android/base/java/org/mozilla/gecko/feeds/action/BaseAction.java
mobile/android/base/java/org/mozilla/gecko/feeds/action/CheckAction.java
mobile/android/base/java/org/mozilla/gecko/feeds/action/EnrollAction.java
mobile/android/base/java/org/mozilla/gecko/feeds/action/SetupAction.java
mobile/android/base/java/org/mozilla/gecko/feeds/action/SubscribeAction.java
mobile/android/base/java/org/mozilla/gecko/feeds/action/WithdrawAction.java
mobile/android/base/java/org/mozilla/gecko/feeds/subscriptions/FeedSubscription.java
mobile/android/base/java/org/mozilla/gecko/feeds/subscriptions/SubscriptionStorage.java
mobile/android/base/moz.build
--- 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.text.TextUtils;
-import android.util.AtomicFile;
-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
@@ -285,17 +285,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',