Bug 1328937 - Introduce shared interface for activity stream models. r?grisha draft
authorSebastian Kaspari <s.kaspari@gmail.com>
Mon, 09 Jan 2017 14:39:34 +0100
changeset 458927 dedf5b00bb167e2649c370e84c4cc53aa5429c5b
parent 457652 ab8aacb3efc846a6ba15f2a26321533a03ccccad
child 541791 94cf8adb29281146e427103b199d9a7c6ec139aa
push id41110
push users.kaspari@gmail.com
push dateWed, 11 Jan 2017 10:02:07 +0000
reviewersgrisha
bugs1328937
milestone53.0a1
Bug 1328937 - Introduce shared interface for activity stream models. r?grisha MozReview-Commit-ID: EkfmQfCQNMM
mobile/android/base/java/org/mozilla/gecko/home/activitystream/menu/ActivityStreamContextMenu.java
mobile/android/base/java/org/mozilla/gecko/home/activitystream/menu/BottomSheetContextMenu.java
mobile/android/base/java/org/mozilla/gecko/home/activitystream/menu/PopupContextMenu.java
mobile/android/base/java/org/mozilla/gecko/home/activitystream/model/Highlight.java
mobile/android/base/java/org/mozilla/gecko/home/activitystream/model/Item.java
mobile/android/base/java/org/mozilla/gecko/home/activitystream/model/TopSite.java
mobile/android/base/java/org/mozilla/gecko/home/activitystream/stream/HighlightItem.java
mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesCard.java
mobile/android/base/moz.build
--- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/menu/ActivityStreamContextMenu.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/menu/ActivityStreamContextMenu.java
@@ -17,16 +17,17 @@ import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.IntentHelper;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.activitystream.ActivityStreamTelemetry;
 import org.mozilla.gecko.annotation.RobocopTarget;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.home.HomePager;
+import org.mozilla.gecko.home.activitystream.model.Item;
 import org.mozilla.gecko.reader.SavedReaderViewHelper;
 import org.mozilla.gecko.util.Clipboard;
 import org.mozilla.gecko.util.HardwareUtils;
 import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.util.UIAsyncTask;
 
 import java.util.EnumSet;
 
@@ -35,135 +36,126 @@ public abstract class ActivityStreamCont
         implements NavigationView.OnNavigationItemSelectedListener {
 
     public enum MenuMode {
         HIGHLIGHT,
         TOPSITE
     }
 
     private final Context context;
-
-    private final String title;
-    private final String url;
+    private final Item item;
 
     private final ActivityStreamTelemetry.Extras.Builder telemetryExtraBuilder;
 
-    // We might not know bookmarked/pinned states, so we allow for null values.
-    // If we aren't told what these are in the constructor, we look them up in postInit.
-    private @Nullable Boolean isBookmarked;
-    private @Nullable Boolean isPinned;
-
     private final HomePager.OnUrlOpenListener onUrlOpenListener;
     private final HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener;
 
     public abstract MenuItem getItemByID(int id);
 
     public abstract void show();
 
     public abstract void dismiss();
 
     private final MenuMode mode;
 
     /* package-private */ ActivityStreamContextMenu(final Context context,
                                                     final ActivityStreamTelemetry.Extras.Builder telemetryExtraBuilder,
                                                     final MenuMode mode,
-                                                    final String title, @NonNull final String url,
-                                                    @Nullable final Boolean isBookmarked, @Nullable final Boolean isPinned,
+                                                    final Item item,
                                                     HomePager.OnUrlOpenListener onUrlOpenListener,
                                                     HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener) {
         this.context = context;
+        this.item = item;
         this.telemetryExtraBuilder = telemetryExtraBuilder;
 
         this.mode = mode;
 
-        this.title = title;
-        this.url = url;
-        this.isBookmarked = isBookmarked;
-        this.isPinned = isPinned;
         this.onUrlOpenListener = onUrlOpenListener;
         this.onUrlOpenInBackgroundListener = onUrlOpenInBackgroundListener;
     }
 
     /**
      * Must be called before the menu is shown.
      * <p/>
      * Your implementation must be ready to return items from getItemByID() before postInit() is
      * called, i.e. you should probably inflate your menu items before this call.
      */
     /* package-local */ void postInit() {
         final MenuItem bookmarkItem = getItemByID(R.id.bookmark);
-        if (Boolean.TRUE.equals(this.isBookmarked)) {
+        if (Boolean.TRUE.equals(item.isBookmarked())) {
             bookmarkItem.setTitle(R.string.bookmark_remove);
         }
 
         final MenuItem pinItem = getItemByID(R.id.pin);
-        if (Boolean.TRUE.equals(this.isPinned)) {
+        if (Boolean.TRUE.equals(item.isPinned())) {
             pinItem.setTitle(R.string.contextmenu_top_sites_unpin);
         }
 
         // Disable "dismiss" for topsites until we have decided on its behaviour for topsites
         // (currently "dismiss" adds the URL to a highlights-specific blocklist, which the topsites
         // query has no knowledge of).
         if (mode == MenuMode.TOPSITE) {
             final MenuItem dismissItem = getItemByID(R.id.dismiss);
             dismissItem.setVisible(false);
         }
 
-        if (isBookmarked == null) {
+        if (item.isBookmarked() == null) {
             // Disable the bookmark item until we know its bookmark state
             bookmarkItem.setEnabled(false);
 
             (new UIAsyncTask.WithoutParams<Boolean>(ThreadUtils.getBackgroundHandler()) {
                 @Override
                 protected Boolean doInBackground() {
-                    return BrowserDB.from(context).isBookmark(context.getContentResolver(), url);
+                    return BrowserDB.from(context).isBookmark(context.getContentResolver(), item.getUrl());
                 }
 
                 @Override
                 protected void onPostExecute(Boolean hasBookmark) {
                     if (hasBookmark) {
                         bookmarkItem.setTitle(R.string.bookmark_remove);
                     }
 
-                    isBookmarked = hasBookmark;
+                    item.updateBookmarked(hasBookmark);
                     bookmarkItem.setEnabled(true);
                 }
             }).execute();
         }
 
-        if (isPinned == null) {
+        if (item.isPinned() == null) {
             // Disable the pin item until we know its pinned state
             pinItem.setEnabled(false);
 
             (new UIAsyncTask.WithoutParams<Boolean>(ThreadUtils.getBackgroundHandler()) {
                 @Override
                 protected Boolean doInBackground() {
-                    return BrowserDB.from(context).isPinnedForAS(context.getContentResolver(), url);
+                    return BrowserDB.from(context).isPinnedForAS(context.getContentResolver(), item.getUrl());
                 }
 
                 @Override
                 protected void onPostExecute(Boolean hasPin) {
                     if (hasPin) {
                         pinItem.setTitle(R.string.contextmenu_top_sites_unpin);
                     }
 
-                    isPinned = hasPin;
+                    item.updatePinned(hasPin);
                     pinItem.setEnabled(true);
                 }
             }).execute();
         }
 
         // Only show the "remove from history" item if a page actually has history
         final MenuItem deleteHistoryItem = getItemByID(R.id.delete);
         deleteHistoryItem.setVisible(false);
 
         (new UIAsyncTask.WithoutParams<Boolean>(ThreadUtils.getBackgroundHandler()) {
             @Override
             protected Boolean doInBackground() {
-                final Cursor cursor = BrowserDB.from(context).getHistoryForURL(context.getContentResolver(), url);
+                final Item item = ActivityStreamContextMenu.this.item;
+
+                final Cursor cursor = BrowserDB.from(context).getHistoryForURL(context.getContentResolver(), item.getUrl());
                 // It's tempting to throw here, but crashing because of a (hopefully) inconsequential
                 // oddity is somewhat questionable.
                 if (cursor == null) {
                     return false;
                 }
                 try {
                     return cursor.getCount() == 1;
                 } finally {
@@ -175,43 +167,43 @@ public abstract class ActivityStreamCont
             protected void onPostExecute(Boolean hasHistory) {
                 deleteHistoryItem.setVisible(hasHistory);
             }
         }).execute();
     }
 
 
     @Override
-    public boolean onNavigationItemSelected(MenuItem item) {
-        final int menuItemId = item.getItemId();
+    public boolean onNavigationItemSelected(MenuItem menuItem) {
+        final int menuItemId = menuItem.getItemId();
 
         // Sets extra telemetry which doesn't require additional state information.
         // Pin and bookmark items are handled separately below, since they do require state
         // information to handle correctly.
         telemetryExtraBuilder.fromMenuItemId(menuItemId);
 
-        switch (item.getItemId()) {
+        switch (menuItem.getItemId()) {
             case R.id.share:
                 // NB: Generic menu item action event will be sent at the end of this function.
                 // We have a seemingly duplicate telemetry event here because we want to emit
                 // a concrete event in case it is used by other queries to estimate feature usage.
                 Telemetry.sendUIEvent(TelemetryContract.Event.SHARE, TelemetryContract.Method.LIST, "as_contextmenu");
 
-                IntentHelper.openUriExternal(url, "text/plain", "", "", Intent.ACTION_SEND, title, false);
+                IntentHelper.openUriExternal(item.getUrl(), "text/plain", "", "", Intent.ACTION_SEND, item.getTitle(), false);
                 break;
 
             case R.id.bookmark:
                 final TelemetryContract.Event telemetryEvent;
                 final String telemetryExtra;
                 SavedReaderViewHelper rch = SavedReaderViewHelper.getSavedReaderViewHelper(context);
-                final boolean isReaderViewPage = rch.isURLCached(url);
+                final boolean isReaderViewPage = rch.isURLCached(item.getUrl());
 
                 // While isBookmarked is nullable, behaviour of postInit - disabling 'bookmark' item
                 // until we know value of isBookmarked - guarantees that it will be set when we get here.
-                if (isBookmarked) {
+                if (item.isBookmarked()) {
                     telemetryEvent = TelemetryContract.Event.UNSAVE;
 
                     if (isReaderViewPage) {
                         telemetryExtra = "as_bookmark_reader";
                     } else {
                         telemetryExtra = "as_bookmark";
                     }
                     telemetryExtraBuilder.set(ActivityStreamTelemetry.Contract.ITEM, ActivityStreamTelemetry.Contract.ITEM_REMOVE_BOOKMARK);
@@ -225,125 +217,119 @@ public abstract class ActivityStreamCont
                 // a concrete event in case it is used by other queries to estimate feature usage.
                 Telemetry.sendUIEvent(telemetryEvent, TelemetryContract.Method.CONTEXT_MENU, telemetryExtra);
 
                 ThreadUtils.postToBackgroundThread(new Runnable() {
                     @Override
                     public void run() {
                         final BrowserDB db = BrowserDB.from(context);
 
-                        if (isBookmarked) {
-                            db.removeBookmarksWithURL(context.getContentResolver(), url);
+                        if (item.isBookmarked()) {
+                            db.removeBookmarksWithURL(context.getContentResolver(), item.getUrl());
 
                         } else {
                             // We only store raw URLs in history (and bookmarks), hence we won't ever show about:reader
                             // URLs in AS topsites or highlights. Therefore we don't need to do any special about:reader handling here.
-                            db.addBookmark(context.getContentResolver(), title, url);
+                            db.addBookmark(context.getContentResolver(), item.getTitle(), item.getUrl());
                         }
                     }
                 });
                 break;
 
             case R.id.pin:
                 // While isPinned is nullable, behaviour of postInit - disabling 'pin' item
                 // until we know value of isPinned - guarantees that it will be set when we get here.
-                if (isPinned) {
+                if (item.isPinned()) {
                     telemetryExtraBuilder.set(ActivityStreamTelemetry.Contract.ITEM, ActivityStreamTelemetry.Contract.ITEM_UNPIN);
                 } else {
                     telemetryExtraBuilder.set(ActivityStreamTelemetry.Contract.ITEM, ActivityStreamTelemetry.Contract.ITEM_PIN);
                 }
 
                 ThreadUtils.postToBackgroundThread(new Runnable() {
                     @Override
                     public void run() {
                         final BrowserDB db = BrowserDB.from(context);
 
-                        if (isPinned) {
-                            db.unpinSiteForAS(context.getContentResolver(), url);
+                        if (item.isPinned()) {
+                            db.unpinSiteForAS(context.getContentResolver(), item.getUrl());
                         } else {
-                            db.pinSiteForAS(context.getContentResolver(), url, title);
+                            db.pinSiteForAS(context.getContentResolver(), item.getUrl(), item.getTitle());
                         }
                     }
                 });
                 break;
 
             case R.id.copy_url:
-                Clipboard.setText(url);
+                Clipboard.setText(item.getUrl());
                 break;
 
             case R.id.add_homescreen:
-                GeckoAppShell.createShortcut(title, url);
+                GeckoAppShell.createShortcut(item.getTitle(), item.getUrl());
                 break;
 
             case R.id.open_new_tab:
-                onUrlOpenInBackgroundListener.onUrlOpenInBackground(url, EnumSet.noneOf(HomePager.OnUrlOpenInBackgroundListener.Flags.class));
+                onUrlOpenInBackgroundListener.onUrlOpenInBackground(item.getUrl(), EnumSet.noneOf(HomePager.OnUrlOpenInBackgroundListener.Flags.class));
                 break;
 
             case R.id.open_new_private_tab:
-                onUrlOpenInBackgroundListener.onUrlOpenInBackground(url, EnumSet.of(HomePager.OnUrlOpenInBackgroundListener.Flags.PRIVATE));
+                onUrlOpenInBackgroundListener.onUrlOpenInBackground(item.getUrl(), EnumSet.of(HomePager.OnUrlOpenInBackgroundListener.Flags.PRIVATE));
                 break;
 
             case R.id.dismiss:
                 ThreadUtils.postToBackgroundThread(new Runnable() {
                     @Override
                     public void run() {
                         BrowserDB.from(context)
-                                .blockActivityStreamSite(context.getContentResolver(),
-                                        url);
+                                .blockActivityStreamSite(context.getContentResolver(), item.getUrl());
                     }
                 });
                 break;
 
             case R.id.delete:
                 ThreadUtils.postToBackgroundThread(new Runnable() {
                     @Override
                     public void run() {
                         BrowserDB.from(context)
-                                .removeHistoryEntry(context.getContentResolver(),
-                                        url);
+                                .removeHistoryEntry(context.getContentResolver(), item.getUrl());
                     }
                 });
                 break;
 
             default:
-                throw new IllegalArgumentException("Menu item with ID=" + item.getItemId() + " not handled");
+                throw new IllegalArgumentException("Menu item with ID=" + menuItem.getItemId() + " not handled");
         }
 
         Telemetry.sendUIEvent(
                 TelemetryContract.Event.ACTION,
                 TelemetryContract.Method.CONTEXT_MENU,
                 telemetryExtraBuilder.build()
         );
 
         dismiss();
         return true;
     }
 
 
     @RobocopTarget
     public static ActivityStreamContextMenu show(Context context,
                                                       View anchor, ActivityStreamTelemetry.Extras.Builder telemetryExtraBuilder,
-                                                      final MenuMode menuMode,
-                                                      final String title, @NonNull final String url,
-                                                      @Nullable final Boolean isBookmarked, @Nullable final Boolean isPinned,
+                                                      final MenuMode menuMode, final Item item,
                                                       HomePager.OnUrlOpenListener onUrlOpenListener,
                                                       HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener,
                                                       final int tilesWidth, final int tilesHeight) {
         final ActivityStreamContextMenu menu;
 
         if (!HardwareUtils.isTablet()) {
             menu = new BottomSheetContextMenu(context,
                     telemetryExtraBuilder, menuMode,
-                    title, url, isBookmarked, isPinned,
-                    onUrlOpenListener, onUrlOpenInBackgroundListener,
+                    item, onUrlOpenListener, onUrlOpenInBackgroundListener,
                     tilesWidth, tilesHeight);
         } else {
             menu = new PopupContextMenu(context,
                     anchor,
                     telemetryExtraBuilder, menuMode,
-                    title, url, isBookmarked, isPinned,
-                    onUrlOpenListener, onUrlOpenInBackgroundListener);
+                    item, onUrlOpenListener, onUrlOpenInBackgroundListener);
         }
 
         menu.show();
         return menu;
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/menu/BottomSheetContextMenu.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/menu/BottomSheetContextMenu.java
@@ -14,16 +14,17 @@ import android.view.MenuItem;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.TextView;
 
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.activitystream.ActivityStream;
 import org.mozilla.gecko.activitystream.ActivityStreamTelemetry;
 import org.mozilla.gecko.home.HomePager;
+import org.mozilla.gecko.home.activitystream.model.Item;
 import org.mozilla.gecko.icons.IconCallback;
 import org.mozilla.gecko.icons.IconResponse;
 import org.mozilla.gecko.icons.Icons;
 import org.mozilla.gecko.widget.FaviconView;
 
 import static org.mozilla.gecko.activitystream.ActivityStream.extractLabel;
 
 /* package-private */ class BottomSheetContextMenu
@@ -32,55 +33,51 @@ import static org.mozilla.gecko.activity
 
     private final BottomSheetDialog bottomSheetDialog;
 
     private final NavigationView navigationView;
 
     public BottomSheetContextMenu(final Context context,
                                   final ActivityStreamTelemetry.Extras.Builder telemetryExtraBuilder,
                                   final MenuMode mode,
-                                  final String title, @NonNull final String url,
-                                  @Nullable final Boolean isBookmarked, @Nullable final Boolean isPinned,
+                                  final Item item,
                                   HomePager.OnUrlOpenListener onUrlOpenListener,
                                   HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener,
                                   final int tilesWidth, final int tilesHeight) {
 
         super(context,
                 telemetryExtraBuilder,
                 mode,
-                title,
-                url,
-                isBookmarked,
-                isPinned,
+                item,
                 onUrlOpenListener,
                 onUrlOpenInBackgroundListener);
 
         final LayoutInflater inflater = LayoutInflater.from(context);
         final View content = inflater.inflate(R.layout.activity_stream_contextmenu_bottomsheet, null);
 
         bottomSheetDialog = new BottomSheetDialog(context);
         bottomSheetDialog.setContentView(content);
 
-        ((TextView) content.findViewById(R.id.title)).setText(title);
+        ((TextView) content.findViewById(R.id.title)).setText(item.getTitle());
 
-        extractLabel(context, url, false, new ActivityStream.LabelCallback() {
+        extractLabel(context, item.getUrl(), false, new ActivityStream.LabelCallback() {
                 public void onLabelExtracted(String label) {
                     ((TextView) content.findViewById(R.id.url)).setText(label);
                 }
         });
 
         // Copy layouted parameters from the Highlights / TopSites items to ensure consistency
         final FaviconView faviconView = (FaviconView) content.findViewById(R.id.icon);
         ViewGroup.LayoutParams layoutParams = faviconView.getLayoutParams();
         layoutParams.width = tilesWidth;
         layoutParams.height = tilesHeight;
         faviconView.setLayoutParams(layoutParams);
 
         Icons.with(context)
-                .pageUrl(url)
+                .pageUrl(item.getUrl())
                 .skipNetwork()
                 .build()
                 .execute(new IconCallback() {
                     @Override
                     public void onIconResponse(IconResponse response) {
                         faviconView.updateImage(response);
                     }
                 });
--- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/menu/PopupContextMenu.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/menu/PopupContextMenu.java
@@ -14,42 +14,37 @@ import android.view.LayoutInflater;
 import android.view.MenuItem;
 import android.view.View;
 import android.widget.PopupWindow;
 
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.activitystream.ActivityStream;
 import org.mozilla.gecko.activitystream.ActivityStreamTelemetry;
 import org.mozilla.gecko.home.HomePager;
+import org.mozilla.gecko.home.activitystream.model.Item;
 
 /* package-private */ class PopupContextMenu
         extends ActivityStreamContextMenu {
 
     private final PopupWindow popupWindow;
     private final NavigationView navigationView;
 
     private final View anchor;
 
     public PopupContextMenu(final Context context,
                             View anchor,
                             final ActivityStreamTelemetry.Extras.Builder telemetryExtraBuilder,
                             final MenuMode mode,
-                            final String title,
-                            @NonNull final String url,
-                            @Nullable final Boolean isBookmarked,
-                            @Nullable final Boolean isPinned,
+                            final Item item,
                             HomePager.OnUrlOpenListener onUrlOpenListener,
                             HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener) {
         super(context,
                 telemetryExtraBuilder,
                 mode,
-                title,
-                url,
-                isBookmarked,
-                isPinned,
+                item,
                 onUrlOpenListener,
                 onUrlOpenInBackgroundListener);
 
         this.anchor = anchor;
 
         final LayoutInflater inflater = LayoutInflater.from(context);
 
         View card = inflater.inflate(R.layout.activity_stream_contextmenu_popupmenu, null);
--- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/model/Highlight.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/model/Highlight.java
@@ -7,17 +7,17 @@ package org.mozilla.gecko.home.activitys
 
 import android.database.Cursor;
 import android.support.annotation.Nullable;
 import android.text.format.DateUtils;
 
 import org.mozilla.gecko.activitystream.Utils;
 import org.mozilla.gecko.db.BrowserContract;
 
-public class Highlight {
+public class Highlight implements Item {
     private final String title;
     private final String url;
     private final Utils.HighlightSource source;
     private final long time;
 
     private Metadata metadata;
 
     private @Nullable Boolean isPinned;
@@ -71,16 +71,26 @@ public class Highlight {
     public Boolean isBookmarked() {
         return isBookmarked;
     }
 
     public Boolean isPinned() {
         return isPinned;
     }
 
+    @Override
+    public void updateBookmarked(boolean bookmarked) {
+        this.isBookmarked = bookmarked;
+    }
+
+    @Override
+    public void updatePinned(boolean pinned) {
+        this.isPinned = pinned;
+    }
+
     public String getRelativeTimeSpan() {
         return DateUtils.getRelativeTimeSpanString(
                         time, System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS, 0).toString();
     }
 
     public Utils.HighlightSource getSource() {
         return source;
     }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/model/Item.java
@@ -0,0 +1,30 @@
+package org.mozilla.gecko.home.activitystream.model;
+
+import android.support.annotation.Nullable;
+
+/**
+ * Shared interface for activity stream item models.
+ */
+public interface Item {
+    String getTitle();
+
+    String getUrl();
+
+    /**
+     * @return True if the item is bookmarked, false otherwise. Might return 'null' if the bookmark
+     *         state is unknown and the database needs to be asked whether the URL is bookmarked.
+     */
+    @Nullable
+    Boolean isBookmarked();
+
+    /**
+     * @return True if the item is pinned, false otherwise. Will return 'null' if the pinned state
+     * is unknown or this item can't be pinned.
+     */
+    @Nullable
+    Boolean isPinned();
+
+    void updateBookmarked(boolean bookmarked);
+
+    void updatePinned(boolean pinned);
+}
--- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/model/TopSite.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/model/TopSite.java
@@ -5,21 +5,22 @@
 
 package org.mozilla.gecko.home.activitystream.model;
 
 import android.database.Cursor;
 import android.support.annotation.Nullable;
 
 import org.mozilla.gecko.db.BrowserContract;
 
-public class TopSite {
+public class TopSite implements Item {
     private final long id;
     private final String url;
     private final String title;
-    private @Nullable final Boolean isBookmarked;
+    private @Nullable Boolean isBookmarked;
+    private final @Nullable boolean isPinned;
     private @BrowserContract.TopSites.TopSiteType final int type;
 
     public static TopSite fromCursor(Cursor cursor) {
         // The Combined View only contains pages that have been visited at least once, i.e. any
         // page in the TopSites query will contain a HISTORY_ID. _ID however will be 0 for all rows.
         final long id = cursor.getLong(cursor.getColumnIndexOrThrow(BrowserContract.Combined.HISTORY_ID));
         final String url = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Combined.URL));
         final String title = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Combined.TITLE));
@@ -34,16 +35,17 @@ public class TopSite {
         return new TopSite(id, url, title, isBookmarked, type);
     }
 
     private TopSite(long id, String url, String title, @Nullable Boolean isBookmarked, int type) {
         this.id = id;
         this.url = url;
         this.title = title;
         this.isBookmarked = isBookmarked;
+        this.isPinned = type == BrowserContract.TopSites.TYPE_PINNED;
         this.type = type;
     }
 
     public long getId() {
         return id;
     }
 
     public String getUrl() {
@@ -59,12 +61,23 @@ public class TopSite {
         return isBookmarked;
     }
 
     @BrowserContract.TopSites.TopSiteType
     public int getType() {
         return type;
     }
 
-    public boolean isPinned() {
-        return type == BrowserContract.TopSites.TYPE_PINNED;
+    public Boolean isPinned() {
+        return isPinned;
+    }
+
+    @Override
+    public void updateBookmarked(boolean bookmarked) {
+        this.isBookmarked = bookmarked;
+    }
+
+    @Override
+    public void updatePinned(boolean pinned) {
+        throw new UnsupportedOperationException(
+                "Pinned state of a top site should be known at the time of querying the database already");
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/stream/HighlightItem.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/stream/HighlightItem.java
@@ -78,17 +78,17 @@ public class HighlightItem extends Strea
                 ActivityStreamTelemetry.Extras.Builder extras = ActivityStreamTelemetry.Extras.builder()
                         .set(ActivityStreamTelemetry.Contract.SOURCE_TYPE, ActivityStreamTelemetry.Contract.TYPE_HIGHLIGHTS)
                         .forHighlightSource(highlight.getSource());
 
                 ActivityStreamContextMenu.show(v.getContext(),
                         menuButton,
                         extras,
                         ActivityStreamContextMenu.MenuMode.HIGHLIGHT,
-                        highlight.getTitle(), highlight.getUrl(), highlight.isBookmarked(), highlight.isPinned(),
+                        highlight,
                         onUrlOpenListener, onUrlOpenInBackgroundListener,
                         vIconView.getWidth(), vIconView.getHeight());
 
                 Telemetry.sendUIEvent(
                         TelemetryContract.Event.SHOW,
                         TelemetryContract.Method.CONTEXT_MENU,
                         extras.build()
                 );
--- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesCard.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesCard.java
@@ -113,20 +113,17 @@ class TopSitesCard extends RecyclerView.
                     TelemetryContract.Method.LIST_ITEM,
                     extras.build()
             );
         } else if (clickedView == menuButton) {
             ActivityStreamContextMenu.show(clickedView.getContext(),
                     menuButton,
                     extras,
                     ActivityStreamContextMenu.MenuMode.TOPSITE,
-                    title.getText().toString(), topSite.getUrl(),
-
-                    topSite.isBookmarked(), topSite.isPinned(),
-
+                    topSite,
                     onUrlOpenListener, onUrlOpenInBackgroundListener,
                     faviconView.getWidth(), faviconView.getHeight());
 
             Telemetry.sendUIEvent(
                     TelemetryContract.Event.SHOW,
                     TelemetryContract.Method.CONTEXT_MENU,
                     extras.build()
             );
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -466,16 +466,17 @@ gbjar.sources += ['java/org/mozilla/geck
     'health/StubbedHealthRecorder.java',
     'home/activitystream/ActivityStream.java',
     'home/activitystream/ActivityStreamHomeFragment.java',
     'home/activitystream/ActivityStreamHomeScreen.java',
     'home/activitystream/menu/ActivityStreamContextMenu.java',
     'home/activitystream/menu/BottomSheetContextMenu.java',
     'home/activitystream/menu/PopupContextMenu.java',
     'home/activitystream/model/Highlight.java',
+    'home/activitystream/model/Item.java',
     'home/activitystream/model/Metadata.java',
     'home/activitystream/model/TopSite.java',
     'home/activitystream/stream/HighlightItem.java',
     'home/activitystream/stream/HighlightsTitle.java',
     'home/activitystream/stream/StreamItem.java',
     'home/activitystream/stream/TopPanel.java',
     'home/activitystream/stream/WelcomePanel.java',
     'home/activitystream/StreamRecyclerAdapter.java',