Bug 1300144 - Implement Activity Stream "context" bottomsheet menu r?sebastian draft
authorAndrzej Hunt <ahunt@mozilla.com>
Mon, 24 Oct 2016 10:17:14 -0700
changeset 428926 0f2dd42d3b5fcbc31ca6cbe921791c736974631c
parent 427179 be5259cc18efcf0b920d748d7b9e123cc245a8ce
child 428927 acdf14659be153d3d1cc195106176016c3474dac
push id33423
push userahunt@mozilla.com
push dateMon, 24 Oct 2016 17:26:50 +0000
reviewerssebastian
bugs1300144
milestone52.0a1
Bug 1300144 - Implement Activity Stream "context" bottomsheet menu r?sebastian MozReview-Commit-ID: ARvuWk7H99m
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/locales/en-US/android_strings.dtd
mobile/android/base/moz.build
mobile/android/base/resources/drawable/as_contextmenu_divider.xml
mobile/android/base/resources/layout/activity_stream_contextmenu_layout.xml
mobile/android/base/resources/menu/activitystream_contextmenu.xml
mobile/android/base/resources/values/dimens.xml
mobile/android/base/resources/values/styles.xml
mobile/android/base/strings.xml.in
--- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/StreamItem.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/StreamItem.java
@@ -18,16 +18,17 @@ import android.view.TouchDelegate;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.ImageView;
 import android.widget.TextView;
 
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.home.HomePager;
+import org.mozilla.gecko.home.activitystream.menu.ActivityStreamContextMenu;
 import org.mozilla.gecko.home.activitystream.topsites.CirclePageIndicator;
 import org.mozilla.gecko.home.activitystream.topsites.TopSitesPagerAdapter;
 import org.mozilla.gecko.icons.IconCallback;
 import org.mozilla.gecko.icons.IconResponse;
 import org.mozilla.gecko.icons.Icons;
 import org.mozilla.gecko.util.DrawableUtil;
 import org.mozilla.gecko.widget.FaviconView;
 
@@ -68,16 +69,19 @@ public abstract class StreamItem extends
             layoutParams.height = tilesHeight + tilesMargin + textHeight;
             topSitesPager.setLayoutParams(layoutParams);
         }
     }
 
     public static class HighlightItem extends StreamItem implements IconCallback {
         public static final int LAYOUT_ID = R.layout.activity_stream_card_history_item;
 
+        String title;
+        String url;
+
         final FaviconView vIconView;
         final TextView vLabel;
         final TextView vTimeSince;
         final TextView vSourceView;
         final TextView vPageView;
 
         private Future<IconResponse> ongoingIconLoad;
         private int tilesMargin;
@@ -115,24 +119,34 @@ public abstract class StreamItem extends
                     final int heightDelta = (targetHitArea - delegateArea.height()) / 2;
                     delegateArea.bottom += heightDelta;
                     delegateArea.top -= heightDelta;
 
                     TouchDelegate touchDelegate = new TouchDelegate(delegateArea, menuButton);
                     itemView.setTouchDelegate(touchDelegate);
                 }
             });
+
+            menuButton.setOnClickListener(new View.OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    ActivityStreamContextMenu.show(v.getContext(), title, url, onUrlOpenListener, onUrlOpenInBackgroundListener, vIconView.getWidth(), vIconView.getHeight());
+                }
+            });
         }
 
         public void bind(Cursor cursor, int tilesWidth, int tilesHeight) {
+
             final long time = cursor.getLong(cursor.getColumnIndexOrThrow(BrowserContract.Highlights.DATE));
             final String ago = DateUtils.getRelativeTimeSpanString(time, System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS, 0).toString();
-            final String url = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Combined.URL));
 
-            vLabel.setText(cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.History.TITLE)));
+            title = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.History.TITLE));
+            url = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Combined.URL));
+
+            vLabel.setText(title);
             vTimeSince.setText(ago);
 
             ViewGroup.LayoutParams layoutParams = vIconView.getLayoutParams();
             layoutParams.width = tilesWidth - tilesMargin;
             layoutParams.height = tilesHeight;
             vIconView.setLayoutParams(layoutParams);
 
             updateSource(cursor);
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/menu/ActivityStreamContextMenu.java
@@ -0,0 +1,232 @@
+/* -*- 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.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.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
+        implements NavigationView.OnNavigationItemSelectedListener {
+    final Context context;
+
+    final String title;
+    final String url;
+
+    final HomePager.OnUrlOpenListener onUrlOpenListener;
+    final HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener;
+
+    boolean isAlreadyBookmarked = false;
+
+    private ActivityStreamContextMenu(final Context context, final String title, @NonNull final String url,
+                                      HomePager.OnUrlOpenListener onUrlOpenListener,
+                                      HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener,
+                                      final int tilesWidth, final int tilesHeight) {
+        super(context);
+
+        this.context = context;
+
+        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);
+        final String label = extractLabel(url, false);
+        ((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);
+
+        // Disable the bookmark item until we know its bookmark state
+        final MenuItem bookmarkItem = navigationView.getMenu().findItem(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;
+            }
+
+            @Override
+            protected void onPostExecute(Void aVoid) {
+                if (isAlreadyBookmarked) {
+                    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);
+        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 &&
+                            cursor.getCount() == 1) {
+                        hasHistory = true;
+                    } else {
+                        hasHistory = false;
+                    }
+                } finally {
+                    cursor.close();
+                }
+                return null;
+            }
+
+            @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 String title, @NonNull  final String url,
+                            HomePager.OnUrlOpenListener onUrlOpenListener,
+                            HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener,
+                            final int tilesWidth, final int tilesHeight) {
+        final ActivityStreamContextMenu menu = new ActivityStreamContextMenu(context, 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;
+
+            case R.id.bookmark:
+                ThreadUtils.postToBackgroundThread(new Runnable() {
+                    @Override
+                    public void run() {
+                        final BrowserDB db = BrowserDB.from(context);
+
+                        if (isAlreadyBookmarked) {
+                            db.removeBookmarksWithURL(context.getContentResolver(), url);
+                        } else {
+                            db.addBookmark(context.getContentResolver(), title, url);
+                        }
+
+                    }
+                });
+                break;
+
+            case R.id.copy_url:
+                Clipboard.setText(url);
+                break;
+
+            case R.id.add_homescreen:
+                GeckoAppShell.createShortcut(title, url);
+                break;
+
+            case R.id.open_new_tab:
+                onUrlOpenInBackgroundListener.onUrlOpenInBackground(url, EnumSet.noneOf(HomePager.OnUrlOpenInBackgroundListener.Flags.class));
+                break;
+
+            case R.id.open_new_private_tab:
+                onUrlOpenInBackgroundListener.onUrlOpenInBackground(url, 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);
+                    }
+                });
+                break;
+
+            case R.id.delete:
+                ThreadUtils.postToBackgroundThread(new Runnable() {
+                    @Override
+                    public void run() {
+                        BrowserDB.from(context)
+                                .removeHistoryEntry(context.getContentResolver(),
+                                        url);
+                    }
+                });
+
+            default:
+                throw new IllegalArgumentException("Menu item with ID=" + item.getItemId() + " not handled");
+        }
+
+        dismiss();
+        return true;
+    }
+}
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -836,8 +836,13 @@ just addresses the organization to follo
 
 <!-- LOCALIZATION NOTE (activity_stream_highlight_label_bookmarked): This label is shown in the Activity
 Stream list for highlights sourced from th user's bookmarks. -->
 <!ENTITY activity_stream_highlight_label_bookmarked "Bookmarked">
 <!-- LOCALIZATION NOTE (activity_stream_highlight_label_visited): This label is shown in the Activity
 Stream list for highlights sourced from th user's bookmarks. -->
 <!ENTITY activity_stream_highlight_label_visited "Visited">
 
+<!-- LOCALIZATION NOTE (activity_stream_dismiss): This label is shown in the Activity Stream context menu,
+and allows hiding a URL/page from highlights or topsites. The page remains in history/bookmarks, but
+is simply hidden from the Activity Stream panel. -->
+<!ENTITY activity_stream_dismiss "Dismiss">
+<!ENTITY activity_stream_delete_history "Delete from History">
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -435,16 +435,17 @@ gbjar.sources += ['java/org/mozilla/geck
     'GlobalHistory.java',
     '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/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',
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/drawable/as_contextmenu_divider.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+       android:insetLeft="72dp">
+    <shape>
+        <size
+            android:height="1dp"/>
+        <solid android:color="@color/disabled_grey"/>
+    </shape>
+</inset>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/layout/activity_stream_contextmenu_layout.xml
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:gecko="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical">
+
+    <RelativeLayout
+        android:id="@+id/info_wrapper"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:padding="10dp">
+
+        <org.mozilla.gecko.widget.FaviconView
+            android:id="@+id/icon"
+            android:layout_width="@dimen/favicon_bg"
+            android:layout_height="@dimen/favicon_bg"
+            android:layout_gravity="center"
+            gecko:enableRoundCorners="false"
+            tools:background="@drawable/favicon_globe"/>
+
+        <TextView
+            android:id="@+id/url"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_toEndOf="@id/icon"
+            android:layout_toRightOf="@id/icon"
+            android:paddingLeft="@dimen/activity_stream_base_margin"
+            android:paddingStart="@dimen/activity_stream_base_margin"
+            android:textColor="@color/activity_stream_subtitle"
+            android:textSize="12sp"
+            tools:text="twitter"/>
+
+        <TextView
+            android:id="@+id/title"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_below="@id/url"
+            android:layout_toEndOf="@id/icon"
+            android:layout_toRightOf="@id/icon"
+            android:ellipsize="end"
+            android:maxLines="3"
+            android:paddingLeft="@dimen/activity_stream_base_margin"
+            android:paddingStart="@dimen/activity_stream_base_margin"
+            android:textColor="#ff000000"
+            android:textSize="14sp"
+            android:textStyle="bold"
+            tools:text="Descriptive title of a page that is veeeeeeery long - maybe even too long?"/>
+    </RelativeLayout>
+
+    <View
+        android:layout_width="match_parent"
+        android:layout_height="0.5dp"
+        android:layout_marginTop="4dp"
+        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
+            android:id="@+id/menu"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:theme="@style/ActivityStreamContextMenuStyle"
+            app:itemTextAppearance="@style/ActivityStreamContextMenuText"
+            app:menu="@menu/activitystream_contextmenu"/>
+
+    </android.support.v4.widget.NestedScrollView>
+
+
+</LinearLayout>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/menu/activitystream_contextmenu.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <!-- Group ID's are required, otherwise NavigationView won't show any dividers. The ID's are unused, but still required. -->
+    <group android:id="@+id/group0">
+        <item
+            android:id="@+id/bookmark"
+            android:icon="@drawable/as_bookmark"
+            android:title="@string/bookmark"/>
+        <item
+            android:id="@+id/share"
+            android:icon="@drawable/as_share"
+            android:title="@string/share"/>
+        <item
+            android:id="@+id/copy_url"
+            android:icon="@drawable/as_copy"
+            android:title="@string/contextmenu_copyurl"/>
+        <item
+            android:id="@+id/add_homescreen"
+            android:icon="@drawable/as_home"
+            android:title="@string/contextmenu_add_to_launcher"/>
+    </group>
+
+    <group android:id="@+id/group1">
+        <item
+            android:id="@+id/open_new_tab"
+            android:icon="@drawable/as_tab"
+            android:title="@string/contextmenu_open_new_tab"/>
+        <item
+            android:id="@+id/open_new_private_tab"
+            android:icon="@drawable/as_private"
+            android:title="@string/contextmenu_open_private_tab"/>
+    </group>
+
+
+    <group android:id="@+id/group2">
+        <item
+            android:id="@+id/dismiss"
+            android:icon="@drawable/as_dimiss"
+            android:title="@string/activity_stream_dismiss"/>
+
+        <item
+            android:id="@+id/delete"
+            android:icon="@drawable/as_bin"
+            android:visible="false"
+            android:title="@string/activity_stream_delete_history"/>
+    </group>
+</menu>
\ No newline at end of file
--- a/mobile/android/base/resources/values/dimens.xml
+++ b/mobile/android/base/resources/values/dimens.xml
@@ -216,9 +216,14 @@
     <item name="firstrun_tab_strip_content_start" type="dimen">15dp</item>
 
     <item name="notification_media_cover" type="dimen">128dp</item>
 
     <item name="activity_stream_base_margin" type="dimen">10dp</item>
     <item name="activity_stream_desired_tile_width" type="dimen">90dp</item>
     <item name="activity_stream_desired_tile_height" type="dimen">70dp</item>
     <item name="activity_stream_top_sites_text_height" type="dimen">30dp</item>
+
+    <item name="activity_stream_contextmenu_peek_height" type="dimen">380dp</item>
+    <!-- note: max_menu_height only affects the scrolling menu, but doesnt' take into consideration
+         the header above it. -->
+    <item name="activity_stream_contextmenu_max_menu_height" type="dimen">350dp</item>
 </resources>
--- a/mobile/android/base/resources/values/styles.xml
+++ b/mobile/android/base/resources/values/styles.xml
@@ -814,9 +814,19 @@
         <item name="android:paddingLeft">8dp</item>
         <item name="android:paddingRight">8dp</item>
 
         <!-- AppCompat sets Button text to all caps so we override that here. -->
         <item name="textAllCaps">false</item>
     </style>
 
     <style name="TabQueueActivity" parent="android:style/Theme.NoDisplay" />
+
+    <style name="ActivityStreamContextMenuText">
+        <item name="android:textSize">16sp</item>
+    </style>
+
+    <!-- We use this style to provide our own divider that has an inset on the left side -->
+    <style name="ActivityStreamContextMenuStyle">
+        <item name="android:listDivider">@drawable/as_contextmenu_divider</item>
+    </style>
+
 </resources>
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -629,9 +629,11 @@
   <string name="helper_triple_readerview_open_title">&helper_triple_readerview_open_title;</string>
   <string name="helper_triple_readerview_open_message">&helper_triple_readerview_open_message;</string>
   <string name="helper_triple_readerview_open_button">&helper_triple_readerview_open_button;</string>
 
   <string name="activity_stream_topsites">&activity_stream_topsites;</string>
   <string name="activity_stream_highlights">&activity_stream_highlights;</string>
   <string name="activity_stream_highlight_label_bookmarked">&activity_stream_highlight_label_bookmarked;</string>
   <string name="activity_stream_highlight_label_visited">&activity_stream_highlight_label_visited;</string>
+  <string name="activity_stream_dismiss">&activity_stream_dismiss;</string>
+  <string name="activity_stream_delete_history">&activity_stream_delete_history;</string>
 </resources>