Android: More native pages become navigable

This CL enables history navigation on more native pages when they are
shown in a tab. Native pages shown as an activity (on mobile) are not
put in history stack like before.

Navigation gets canceled if these native pages consume user gesture
(mostly long clicks on bookmark/download/history items).

- Bookmark
- Download
- History

Bug: 865567
Change-Id: I6830a314e4b42c6515b3a47b0aa0de9e2b53fde9
Reviewed-on: https://chromium-review.googlesource.com/c/1423477
Commit-Queue: Jinsuk Kim <jinsukkim@chromium.org>
Reviewed-by: Theresa <twellington@chromium.org>
Cr-Commit-Position: refs/heads/master@{#631525}
diff --git a/chrome/android/java/res/layout/selectable_list_layout.xml b/chrome/android/java/res/layout/selectable_list_layout.xml
index 1884854..b13b3517 100644
--- a/chrome/android/java/res/layout/selectable_list_layout.xml
+++ b/chrome/android/java/res/layout/selectable_list_layout.xml
@@ -12,7 +12,7 @@
         android:layout_height="@dimen/toolbar_height_no_shadow"
         android:background="@color/default_primary_color" />
 
-    <FrameLayout
+    <org.chromium.chrome.browser.gesturenav.HistoryNavigationLayout
         android:id="@+id/list_content"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
@@ -25,21 +25,27 @@
             android:clipToPadding="false"
             android:visibility="gone" />
 
-        <TextView
-            android:id="@+id/empty_view"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_gravity="center"
-            android:drawablePadding="3dp"
-            android:textAppearance="@style/TextAppearance.BlackDisabledText1"
-            android:visibility="gone" />
+        <FrameLayout
+            android:id="@+id/empty_view_wrapper"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:visibility="gone" >
+            <TextView
+                android:id="@+id/empty_view"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center"
+                android:drawablePadding="3dp"
+                android:visibility="gone"
+                android:textAppearance="@style/TextAppearance.BlackDisabledText1" />
+        </FrameLayout>
 
         <org.chromium.chrome.browser.widget.LoadingView
             android:id="@+id/loading_view"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_gravity="center" />
-    </FrameLayout>
+    </org.chromium.chrome.browser.gesturenav.HistoryNavigationLayout>
 
     <org.chromium.chrome.browser.widget.FadingShadowView
         android:id="@+id/shadow"
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/download/home/DownloadManagerCoordinatorImpl.java b/chrome/android/java/src/org/chromium/chrome/browser/download/home/DownloadManagerCoordinatorImpl.java
index 4ee209e..a71250c 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/download/home/DownloadManagerCoordinatorImpl.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/download/home/DownloadManagerCoordinatorImpl.java
@@ -7,7 +7,6 @@
 import android.app.Activity;
 import android.view.Gravity;
 import android.view.View;
-import android.view.ViewGroup;
 import android.widget.FrameLayout;
 
 import org.chromium.base.ApiCompatibilityUtils;
@@ -21,6 +20,7 @@
 import org.chromium.chrome.browser.download.home.snackbars.DeleteUndoCoordinator;
 import org.chromium.chrome.browser.download.home.toolbar.ToolbarCoordinator;
 import org.chromium.chrome.browser.download.items.OfflineContentAggregatorFactory;
+import org.chromium.chrome.browser.gesturenav.HistoryNavigationLayout;
 import org.chromium.chrome.browser.preferences.PreferencesLauncher;
 import org.chromium.chrome.browser.preferences.download.DownloadPreferences;
 import org.chromium.chrome.browser.profiles.Profile;
@@ -42,10 +42,11 @@
 
     private final ToolbarCoordinator mToolbarCoordinator;
     private final SelectionDelegate<ListItem> mSelectionDelegate;
+    private SelectionDelegate.SelectionObserver<ListItem> mNavigationCanceller;
 
     private final Activity mActivity;
 
-    private ViewGroup mMainView;
+    private HistoryNavigationLayout mMainView;
 
     private boolean mMuteFilterChanges;
 
@@ -71,7 +72,7 @@
      * TODO(crbug.com/880468) : Investigate if it is better to do in XML.
      */
     private void initializeView() {
-        mMainView = new FrameLayout(mActivity);
+        mMainView = new HistoryNavigationLayout(mActivity);
         mMainView.setBackgroundColor(ApiCompatibilityUtils.getColor(
                 mActivity.getResources(), R.color.modern_primary_color));
 
@@ -81,6 +82,10 @@
                 mActivity.getResources().getDimensionPixelOffset(R.dimen.toolbar_height_no_shadow),
                 0, 0);
         mMainView.addView(mListCoordinator.getView(), listParams);
+        mNavigationCanceller = (selectedItems) -> {
+            if (!selectedItems.isEmpty()) mMainView.release();
+        };
+        mSelectionDelegate.addObserver(mNavigationCanceller);
 
         FrameLayout.LayoutParams toolbarParams = new FrameLayout.LayoutParams(
                 FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT);
@@ -110,6 +115,7 @@
         mDeleteCoordinator.destroy();
         mListCoordinator.destroy();
         mToolbarCoordinator.destroy();
+        mSelectionDelegate.removeObserver(mNavigationCanceller);
     }
 
     @Override
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/gesturenav/HistoryNavigationLayout.java b/chrome/android/java/src/org/chromium/chrome/browser/gesturenav/HistoryNavigationLayout.java
index aad6529..35e9e5a 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/gesturenav/HistoryNavigationLayout.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/gesturenav/HistoryNavigationLayout.java
@@ -59,8 +59,6 @@
         if (context instanceof ChromeActivity) {
             mTabProvider = ((ChromeActivity) context).getActivityTabProvider();
             mDetector = new GestureDetector(getContext(), new SideNavGestureListener());
-        } else {
-            throw new IllegalStateException("This native page should be under ChromeActivity");
         }
     }
 
@@ -161,9 +159,21 @@
     /**
      * Reset navigation UI in action.
      */
-    public void reset() {
-        cancelStopNavigatingRunnable();
-        if (mSideSlideLayout != null) mSideSlideLayout.reset();
+    private void reset() {
+        if (mSideSlideLayout != null) {
+            cancelStopNavigatingRunnable();
+            mSideSlideLayout.reset();
+        }
+    }
+
+    /**
+     * Cancel navigation UI with animation effect.
+     */
+    public void release() {
+        if (mSideSlideLayout != null) {
+            cancelStopNavigatingRunnable();
+            mSideSlideLayout.release(false);
+        }
     }
 
     private void cancelStopNavigatingRunnable() {
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/widget/selection/SelectableItemViewBase.java b/chrome/android/java/src/org/chromium/chrome/browser/widget/selection/SelectableItemViewBase.java
index 6dc9d03..795285b 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/widget/selection/SelectableItemViewBase.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/widget/selection/SelectableItemViewBase.java
@@ -6,9 +6,11 @@
 
 import android.content.Context;
 import android.util.AttributeSet;
+import android.view.MotionEvent;
 import android.view.View;
 import android.view.View.OnClickListener;
 import android.view.View.OnLongClickListener;
+import android.view.View.OnTouchListener;
 import android.widget.Checkable;
 import android.widget.FrameLayout;
 
@@ -26,8 +28,13 @@
  *
  * @param <E> The type of the item associated with this SelectableItemViewBase.
  */
-public abstract class SelectableItemViewBase<E> extends FrameLayout
-        implements Checkable, OnClickListener, OnLongClickListener, SelectionObserver<E> {
+public abstract class SelectableItemViewBase<E>
+        extends FrameLayout implements Checkable, OnClickListener, OnLongClickListener,
+                                       OnTouchListener, SelectionObserver<E> {
+    // Heuristic value used to rule out long clicks preceded by long horizontal move. A long click
+    // is ignored if finger was moved horizontally more than this threshold.
+    private static final float LONG_CLICK_SLIDE_THRESHOLD_PX = 100.f;
+
     private SelectionDelegate<E> mSelectionDelegate;
     private E mItem;
     private boolean mIsChecked;
@@ -35,6 +42,11 @@
     // Controls whether selection should happen during onLongClick.
     private boolean mSelectOnLongClick = true;
 
+    // X position of touch events to detect the amount of horizontal movement between touch down
+    // and the position where long click is triggered.
+    private float mAnchorX;
+    private float mCurrentX;
+
     /**
      * Constructor for inflating from XML.
      */
@@ -93,6 +105,7 @@
     protected void onFinishInflate() {
         super.onFinishInflate();
 
+        setOnTouchListener(this);
         setOnClickListener(this);
         setOnLongClickListener(this);
     }
@@ -111,6 +124,20 @@
         setChecked(false);
     }
 
+    // OnTouchListener implementation.
+    @Override
+    public final boolean onTouch(View view, MotionEvent event) {
+        int action = event.getActionMasked();
+        if (action == MotionEvent.ACTION_DOWN) {
+            // mCurrentX needs init here as well, since we might not get ACTION_MOVE
+            // for a simple click turning into a long click when selection mode is on.
+            mAnchorX = mCurrentX = event.getX();
+        } else if (action == MotionEvent.ACTION_MOVE) {
+            mCurrentX = event.getX();
+        }
+        return false;
+    }
+
     // OnClickListener implementation.
     @Override
     public final void onClick(View view) {
@@ -132,7 +159,7 @@
     @Override
     public boolean onLongClick(View view) {
         assert view == this;
-        handleSelection();
+        if (Math.abs(mCurrentX - mAnchorX) < LONG_CLICK_SLIDE_THRESHOLD_PX) handleSelection();
         return true;
     }
 
@@ -193,4 +220,4 @@
      * that case.
      */
     protected abstract void onClick();
-}
\ No newline at end of file
+}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/widget/selection/SelectableListLayout.java b/chrome/android/java/src/org/chromium/chrome/browser/widget/selection/SelectableListLayout.java
index d073c8f..b25adca 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/widget/selection/SelectableListLayout.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/widget/selection/SelectableListLayout.java
@@ -27,6 +27,7 @@
 
 import org.chromium.base.ApiCompatibilityUtils;
 import org.chromium.chrome.R;
+import org.chromium.chrome.browser.gesturenav.HistoryNavigationLayout;
 import org.chromium.chrome.browser.widget.FadingShadow;
 import org.chromium.chrome.browser.widget.FadingShadowView;
 import org.chromium.chrome.browser.widget.LoadingView;
@@ -54,6 +55,7 @@
     private RecyclerView.Adapter mAdapter;
     private ViewStub mToolbarStub;
     private TextView mEmptyView;
+    private View mEmptyViewWrapper;
     private LoadingView mLoadingView;
     private RecyclerView mRecyclerView;
     private ItemAnimator mItemAnimator;
@@ -70,11 +72,10 @@
         @Override
         public void onChanged() {
             super.onChanged();
+            updateEmptyViewVisibility();
             if (mAdapter.getItemCount() == 0) {
-                mEmptyView.setVisibility(View.VISIBLE);
                 mRecyclerView.setVisibility(View.GONE);
             } else {
-                mEmptyView.setVisibility(View.GONE);
                 mRecyclerView.setVisibility(View.VISIBLE);
             }
             // At inflation, the RecyclerView is set to gone, and the loading view is visible. As
@@ -108,6 +109,7 @@
         LayoutInflater.from(getContext()).inflate(R.layout.selectable_list_layout, this);
 
         mEmptyView = (TextView) findViewById(R.id.empty_view);
+        mEmptyViewWrapper = findViewById(R.id.empty_view_wrapper);
         mLoadingView = (LoadingView) findViewById(R.id.loading_view);
         mLoadingView.showLoadingUI();
 
@@ -244,6 +246,9 @@
         mEmptyView.setCompoundDrawablesWithIntrinsicBounds(null, emptyDrawable, null, null);
         mEmptyView.setText(mEmptyStringResId);
 
+        // Dummy listener to have the touch events dispatched to this view tree for navigation UI.
+        mEmptyViewWrapper.setOnTouchListener((v, event) -> true);
+
         return mEmptyView;
     }
 
@@ -290,6 +295,9 @@
     @Override
     public void onSelectionStateChange(List<E> selectedItems) {
         setToolbarShadowVisibility();
+        if (!selectedItems.isEmpty()) {
+            ((HistoryNavigationLayout) findViewById(R.id.list_content)).release();
+        }
     }
 
     /**
@@ -340,7 +348,9 @@
      * view implementation. We need to check it ourselves.
      */
     private void updateEmptyViewVisibility() {
-        mEmptyView.setVisibility(mAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE);
+        int visible = mAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE;
+        mEmptyView.setVisibility(visible);
+        mEmptyViewWrapper.setVisibility(visible);
     }
 
     @VisibleForTesting
@@ -368,4 +378,4 @@
 
         return false;
     }
-}
\ No newline at end of file
+}
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/download/DownloadActivityTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/download/DownloadActivityTest.java
index fb4f910..125cbdf 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/download/DownloadActivityTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/download/DownloadActivityTest.java
@@ -144,6 +144,7 @@
         features.put(ChromeFeatureList.DOWNLOADS_LOCATION_CHANGE, false);
         features.put(ChromeFeatureList.DOWNLOAD_HOME_SHOW_STORAGE_INFO, false);
         features.put(ChromeFeatureList.DOWNLOAD_HOME_V2, false);
+        features.put(ChromeFeatureList.OVERSCROLL_HISTORY_NAVIGATION, false);
         ChromeFeatureList.setTestFeatures(features);
 
         mStubbedProvider = new StubbedProvider();
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/download/home/DownloadActivityV2Test.java b/chrome/android/javatests/src/org/chromium/chrome/browser/download/home/DownloadActivityV2Test.java
index e8878c7..f544ea7 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/download/home/DownloadActivityV2Test.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/download/home/DownloadActivityV2Test.java
@@ -87,6 +87,7 @@
         features.put(ChromeFeatureList.DOWNLOAD_HOME_SHOW_STORAGE_INFO, true);
         features.put(ChromeFeatureList.DOWNLOAD_HOME_V2, true);
         features.put(ChromeFeatureList.OFFLINE_PAGES_PREFETCHING, true);
+        features.put(ChromeFeatureList.OVERSCROLL_HISTORY_NAVIGATION, false);
         ChromeFeatureList.setTestFeatures(features);
     }