Bug 1312477 - Pre: refactor AS context menu to allow multiple view implementations r?sebastian draft
authorAndrzej Hunt <ahunt@mozilla.com>
Wed, 02 Nov 2016 20:57:05 +0100
changeset 434813 c1c5b7111fdbcb91453fc3926e1f4d74168e31ec
parent 434812 5289c919ea2ac1f84084cf2a8dfb7a29a6ae21ce
child 434814 36f95a615362d047c6ef9396f0886912f53cf7d8
push id34834
push userahunt@mozilla.com
push dateMon, 07 Nov 2016 12:22:00 +0000
reviewerssebastian
bugs1312477
milestone52.0a1
Bug 1312477 - Pre: refactor AS context menu to allow multiple view implementations r?sebastian This is in preparation for having a popup menu on tablets. MozReview-Commit-ID: 14thIuhRkgB
mobile/android/base/java/org/mozilla/gecko/home/activitystream/StreamItem.java
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/topsites/TopSitesCard.java
mobile/android/base/moz.build
mobile/android/base/resources/layout/activity_stream_contextmenu_bottomsheet.xml
mobile/android/base/resources/layout/activity_stream_contextmenu_layout.xml
mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testActivityStreamContextMenu.java
--- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/StreamItem.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/StreamItem.java
@@ -104,17 +104,19 @@ public abstract class StreamItem extends
             menuButton.setImageDrawable(
                     DrawableUtil.tintDrawable(menuButton.getContext(), R.drawable.menu, Color.LTGRAY));
 
             TouchTargetUtil.ensureTargetHitArea(menuButton, itemView);
 
             menuButton.setOnClickListener(new View.OnClickListener() {
                 @Override
                 public void onClick(View v) {
-                    ActivityStreamContextMenu.show(v.getContext(), ActivityStreamContextMenu.MenuMode.HIGHLIGHT,
+                    ActivityStreamContextMenu.show(v.getContext(),
+                            menuButton,
+                            ActivityStreamContextMenu.MenuMode.HIGHLIGHT,
                             title, url, onUrlOpenListener, onUrlOpenInBackgroundListener,
                             vIconView.getWidth(), vIconView.getHeight());
                 }
             });
         }
 
         public void bind(Cursor cursor, int tilesWidth, int tilesHeight) {
 
--- 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
@@ -3,122 +3,93 @@
  * 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.home.activitystream.menu;
 
 import android.content.Context;
 import android.content.Intent;
 import android.database.Cursor;
 import android.support.annotation.NonNull;
-import android.support.design.widget.BottomSheetBehavior;
-import android.support.design.widget.BottomSheetDialog;
 import android.support.design.widget.NavigationView;
-import android.view.LayoutInflater;
 import android.view.MenuItem;
 import android.view.View;
-import android.view.ViewGroup;
-import android.widget.TextView;
 
 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.ActivityStream;
+import org.mozilla.gecko.annotation.RobocopTarget;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.home.HomePager;
-import org.mozilla.gecko.icons.IconCallback;
-import org.mozilla.gecko.icons.IconResponse;
-import org.mozilla.gecko.icons.Icons;
 import org.mozilla.gecko.util.Clipboard;
 import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.util.UIAsyncTask;
-import org.mozilla.gecko.widget.FaviconView;
 
 import java.util.EnumSet;
 
-import static org.mozilla.gecko.activitystream.ActivityStream.extractLabel;
-
-public class ActivityStreamContextMenu
-    extends BottomSheetDialog
+@RobocopTarget
+public abstract class ActivityStreamContextMenu
         implements NavigationView.OnNavigationItemSelectedListener {
 
     public enum MenuMode {
         HIGHLIGHT,
         TOPSITE
     }
 
     final Context context;
 
     final String title;
     final String url;
 
     final HomePager.OnUrlOpenListener onUrlOpenListener;
     final HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener;
 
-    boolean isAlreadyBookmarked = false;
+    boolean isAlreadyBookmarked; // default false;
+
+    public abstract MenuItem getItemByID(int id);
+
+    public abstract void show();
+
+    public abstract void dismiss();
 
-    private ActivityStreamContextMenu(final Context context,
-                                      final MenuMode mode,
-                                      final String title, @NonNull final String url,
-                                      HomePager.OnUrlOpenListener onUrlOpenListener,
-                                      HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener,
-                                      final int tilesWidth, final int tilesHeight) {
-        super(context);
+    final MenuMode mode;
 
+    /* package-private */ ActivityStreamContextMenu(final Context context,
+                                                    final MenuMode mode,
+                                                    final String title, @NonNull final String url,
+                                                    HomePager.OnUrlOpenListener onUrlOpenListener,
+                                                    HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener) {
         this.context = context;
 
+        this.mode = mode;
+
         this.title = title;
         this.url = url;
         this.onUrlOpenListener = onUrlOpenListener;
         this.onUrlOpenInBackgroundListener = onUrlOpenInBackgroundListener;
-
-        final LayoutInflater inflater = LayoutInflater.from(context);
-
-        final View content = inflater.inflate(R.layout.activity_stream_contextmenu_layout, null);
-        setContentView(content);
-
-        ((TextView) findViewById(R.id.title)).setText(title);
-        extractLabel(context, url, false, new ActivityStream.LabelCallback() {
-                public void onLabelExtracted(String label) {
-                    ((TextView) findViewById(R.id.url)).setText(label);
-                }
-        });
-
-        // Copy layouted parameters from the Highlights / TopSites items to ensure consistency
-        final FaviconView faviconView = (FaviconView) findViewById(R.id.icon);
-        ViewGroup.LayoutParams layoutParams = faviconView.getLayoutParams();
-        layoutParams.width = tilesWidth;
-        layoutParams.height = tilesHeight;
-        faviconView.setLayoutParams(layoutParams);
-
-        Icons.with(context)
-                .pageUrl(url)
-                .skipNetwork()
-                .build()
-                .execute(new IconCallback() {
-                    @Override
-                    public void onIconResponse(IconResponse response) {
-                        faviconView.updateImage(response);
-                    }
-                });
-
-        NavigationView navigationView = (NavigationView) findViewById(R.id.menu);
-        navigationView.setNavigationItemSelectedListener(this);
-
+    }
+    
+    /**
+     * 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.
+     */
+    protected void postInit() {
         // 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 = navigationView.getMenu().findItem(R.id.dismiss);
+            final MenuItem dismissItem = getItemByID(R.id.dismiss);
             dismissItem.setVisible(false);
         }
 
         // Disable the bookmark item until we know its bookmark state
-        final MenuItem bookmarkItem = navigationView.getMenu().findItem(R.id.bookmark);
+        final MenuItem bookmarkItem = getItemByID(R.id.bookmark);
         bookmarkItem.setEnabled(false);
 
         (new UIAsyncTask.WithoutParams<Void>(ThreadUtils.getBackgroundHandler()) {
             @Override
             protected Void doInBackground() {
                 isAlreadyBookmarked = BrowserDB.from(context).isBookmark(context.getContentResolver(), url);
                 return null;
             }
@@ -129,20 +100,19 @@ public class ActivityStreamContextMenu
                     bookmarkItem.setTitle(R.string.bookmark_remove);
                 }
 
                 bookmarkItem.setEnabled(true);
             }
         }).execute();
 
         // Only show the "remove from history" item if a page actually has history
-        final MenuItem deleteHistoryItem = navigationView.getMenu().findItem(R.id.delete);
+        final MenuItem deleteHistoryItem = getItemByID(R.id.delete);
         deleteHistoryItem.setVisible(false);
 
-
         (new UIAsyncTask.WithoutParams<Void>(ThreadUtils.getBackgroundHandler()) {
             boolean hasHistory;
 
             @Override
             protected Void doInBackground() {
                 final Cursor cursor = BrowserDB.from(context).getHistoryForURL(context.getContentResolver(), url);
                 try {
                     if (cursor != null &&
@@ -159,36 +129,18 @@ public class ActivityStreamContextMenu
 
             @Override
             protected void onPostExecute(Void aVoid) {
                 if (hasHistory) {
                     deleteHistoryItem.setVisible(true);
                 }
             }
         }).execute();
-
-        BottomSheetBehavior<View> bsBehaviour = BottomSheetBehavior.from((View) content.getParent());
-        bsBehaviour.setPeekHeight(context.getResources().getDimensionPixelSize(R.dimen.activity_stream_contextmenu_peek_height));
     }
 
-    public static ActivityStreamContextMenu show(Context context,
-                            final MenuMode menuMode,
-                            final String title, @NonNull  final String url,
-                            HomePager.OnUrlOpenListener onUrlOpenListener,
-                            HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener,
-                            final int tilesWidth, final int tilesHeight) {
-        final ActivityStreamContextMenu menu = new ActivityStreamContextMenu(context,
-                menuMode,
-                title, url,
-                onUrlOpenListener, onUrlOpenInBackgroundListener,
-                tilesWidth, tilesHeight);
-        menu.show();
-
-        return menu;
-    }
 
     @Override
     public boolean onNavigationItemSelected(MenuItem item) {
         switch (item.getItemId()) {
             case R.id.share:
                 Telemetry.sendUIEvent(TelemetryContract.Event.SHARE, TelemetryContract.Method.LIST, "menu");
                 IntentHelper.openUriExternal(url, "text/plain", "", "", Intent.ACTION_SEND, title, false);
                 break;
@@ -248,9 +200,30 @@ public class ActivityStreamContextMenu
 
             default:
                 throw new IllegalArgumentException("Menu item with ID=" + item.getItemId() + " not handled");
         }
 
         dismiss();
         return true;
     }
+
+
+    @RobocopTarget
+    public static ActivityStreamContextMenu show(Context context,
+                                                      View anchor,
+                                                      final MenuMode menuMode,
+                                                      final String title, @NonNull final String url,
+                                                      HomePager.OnUrlOpenListener onUrlOpenListener,
+                                                      HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener,
+                                                      final int tilesWidth, final int tilesHeight) {
+        final ActivityStreamContextMenu menu;
+
+        menu = new BottomSheetContextMenu(context,
+                menuMode,
+                title, url,
+                onUrlOpenListener, onUrlOpenInBackgroundListener,
+                tilesWidth, tilesHeight);
+
+        menu.show();
+        return menu;
+    }
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/menu/BottomSheetContextMenu.java
@@ -0,0 +1,105 @@
+/* -*- 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.home.activitystream.menu;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.design.widget.BottomSheetBehavior;
+import android.support.design.widget.BottomSheetDialog;
+import android.support.design.widget.NavigationView;
+import android.view.LayoutInflater;
+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.home.HomePager;
+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
+        extends ActivityStreamContextMenu {
+
+
+    private final BottomSheetDialog bottomSheetDialog;
+
+    private final NavigationView navigationView;
+
+    public BottomSheetContextMenu(final Context context,
+                           final MenuMode mode,
+                           final String title, @NonNull final String url,
+                           HomePager.OnUrlOpenListener onUrlOpenListener,
+                           HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener,
+                           final int tilesWidth, final int tilesHeight) {
+
+        super(context,
+                mode,
+                title,
+                url,
+                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);
+
+        extractLabel(context, url, 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)
+                .skipNetwork()
+                .build()
+                .execute(new IconCallback() {
+                    @Override
+                    public void onIconResponse(IconResponse response) {
+                        faviconView.updateImage(response);
+                    }
+                });
+
+        navigationView = (NavigationView) content.findViewById(R.id.menu);
+        navigationView.setNavigationItemSelectedListener(this);
+
+        BottomSheetBehavior<View> bsBehaviour = BottomSheetBehavior.from((View) content.getParent());
+        bsBehaviour.setPeekHeight(context.getResources().getDimensionPixelSize(R.dimen.activity_stream_contextmenu_peek_height));
+
+        super.postInit();
+    }
+
+    @Override
+    public MenuItem getItemByID(int id) {
+        return navigationView.getMenu().findItem(id);
+    }
+
+    @Override
+    public void show() {
+        bottomSheetDialog.show();
+    }
+
+    public void dismiss() {
+        bottomSheetDialog.dismiss();
+    }
+
+}
--- 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
@@ -87,15 +87,16 @@ class TopSitesCard extends RecyclerView.
     }
 
     @Override
     public void onClick(View clickedView) {
         if (clickedView == itemView) {
             onUrlOpenListener.onUrlOpen(url, EnumSet.noneOf(HomePager.OnUrlOpenListener.Flags.class));
         } else if (clickedView == menuButton) {
             ActivityStreamContextMenu.show(clickedView.getContext(),
+                    menuButton,
                     ActivityStreamContextMenu.MenuMode.TOPSITE,
                     title.getText().toString(), url,
                     onUrlOpenListener, onUrlOpenInBackgroundListener,
                     faviconView.getWidth(), faviconView.getHeight());
         }
     }
 }
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -434,16 +434,17 @@ gbjar.sources += ['java/org/mozilla/geck
     'GuestSession.java',
     'health/HealthRecorder.java',
     'health/SessionInformation.java',
     '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/StreamItem.java',
     'home/activitystream/StreamRecyclerAdapter.java',
     'home/activitystream/topsites/CirclePageIndicator.java',
     'home/activitystream/topsites/TopSitesCard.java',
     'home/activitystream/topsites/TopSitesPage.java',
     'home/activitystream/topsites/TopSitesPageAdapter.java',
     'home/activitystream/topsites/TopSitesPagerAdapter.java',
     'home/BookmarkFolderView.java',
rename from mobile/android/base/resources/layout/activity_stream_contextmenu_layout.xml
rename to mobile/android/base/resources/layout/activity_stream_contextmenu_bottomsheet.xml
--- a/mobile/android/base/resources/layout/activity_stream_contextmenu_layout.xml
+++ b/mobile/android/base/resources/layout/activity_stream_contextmenu_bottomsheet.xml
@@ -58,19 +58,21 @@
         android:background="@color/disabled_grey"
         android:padding="4dp"/>
 
     <android.support.v4.widget.NestedScrollView
         android:layout_width="match_parent"
         android:layout_height="@dimen/activity_stream_contextmenu_max_menu_height">
 
         <android.support.design.widget.NavigationView
+            xmlns:android="http://schemas.android.com/apk/res/android"
+            xmlns:app="http://schemas.android.com/apk/res-auto"
             android:id="@+id/menu"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
+            app:itemTextAppearance="@style/ActivityStreamContextMenuText"
             android:theme="@style/ActivityStreamContextMenuStyle"
-            app:itemTextAppearance="@style/ActivityStreamContextMenuText"
             app:menu="@menu/activitystream_contextmenu"/>
 
     </android.support.v4.widget.NestedScrollView>
 
 
 </LinearLayout>
--- a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testActivityStreamContextMenu.java
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testActivityStreamContextMenu.java
@@ -43,52 +43,36 @@ public class testActivityStreamContextMe
         testMenuForUrl(testURL, false, true);
     }
 
     /**
      * Test that the menu shows the expected menu items for a given URL, and that these items have
      * the correct state.
      */
     private void testMenuForUrl(final String url, final boolean isBookmarked, final boolean isVisited) {
-        final ActivityStreamContextMenu menu = ActivityStreamContextMenu.show(getActivity(), ActivityStreamContextMenu.MenuMode.HIGHLIGHT, "foobar", url, null, null, 100, 100);
-
-        waitForContextMenu(menu);
+        final View anchor = new View(getActivity());
 
-        final View wrapper = menu.findViewById(R.id.info_wrapper);
-        mAsserter.is(wrapper.getVisibility(), View.VISIBLE, "menu should be visible");
-
-        NavigationView nv = (NavigationView) menu.findViewById(R.id.menu);
+        final ActivityStreamContextMenu menu = ActivityStreamContextMenu.show(getActivity(), anchor, ActivityStreamContextMenu.MenuMode.HIGHLIGHT, "foobar", url, null, null, 100, 100);
 
         final int expectedBookmarkString;
         if (isBookmarked) {
             expectedBookmarkString = R.string.bookmark_remove;
         } else {
             expectedBookmarkString = R.string.bookmark;
         }
 
-        final MenuItem bookmarkItem = nv.getMenu().findItem(R.id.bookmark);
+        final MenuItem bookmarkItem = menu.getItemByID(R.id.bookmark);
         assertMenuItemHasString(bookmarkItem, expectedBookmarkString);
 
-        final MenuItem deleteItem = nv.getMenu().findItem(R.id.delete);
+        final MenuItem deleteItem = menu.getItemByID(R.id.delete);
         assertMenuItemIsVisible(deleteItem, isVisited);
 
         menu.dismiss();
     }
 
-    private void waitForContextMenu(final ActivityStreamContextMenu menu) {
-        waitForCondition(new Condition() {
-            @Override
-            public boolean isSatisfied() {
-                final View v = menu.findViewById(R.id.info_wrapper);
-
-                return (v != null);
-            }
-        }, 5000);
-    }
-
     private void assertMenuItemIsVisible(final MenuItem item, final boolean shouldBeVisible) {
         waitForCondition(new Condition() {
             @Override
             public boolean isSatisfied() {
                 return (item.isVisible() == shouldBeVisible);
             }
         }, 5000);