Bug 1300144 - Implement Activity Stream "context" bottomsheet menu r?sebastian
MozReview-Commit-ID: ARvuWk7H99m
--- 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>