Temporal edge navigation feature

- User can swipe from left and right edge to go
  back and forward in history of a tab

Change-Id: I941f91122510b004bdcb6a718eb6c05730694960
diff --git a/src/com/android/browser/EdgeSwipeController.java b/src/com/android/browser/EdgeSwipeController.java
new file mode 100644
index 0000000..87ad2e7
--- /dev/null
+++ b/src/com/android/browser/EdgeSwipeController.java
@@ -0,0 +1,543 @@
+/*
+ * Copyright (c) 2015, The Linux Foundation. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *     * Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above
+ *       copyright notice, this list of conditions and the following
+ *       disclaimer in the documentation and/or other materials provided
+ *       with the distribution.
+ *     * Neither the name of The Linux Foundation nor the names of its
+ *       contributors may be used to endorse or promote products derived
+ *       from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+ * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+package com.android.browser;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.ColorMatrix;
+import android.graphics.ColorMatrixColorFilter;
+import android.graphics.Paint;
+import android.os.CountDownTimer;
+import android.support.v4.widget.ViewDragHelper;
+import android.view.View;
+
+import org.codeaurora.swe.WebHistoryItem;
+import org.codeaurora.swe.util.Activator;
+import org.codeaurora.swe.util.Observable;
+
+public class EdgeSwipeController extends ViewDragHelper.Callback {
+    private ViewDragHelper mDragHelper;
+    private int mState = ViewDragHelper.STATE_IDLE;
+    private int mFromEdge = ViewDragHelper.EDGE_LEFT;
+    private boolean mbNavigated = false;
+    private int mOldX = 0;
+    private int mOldDx = 0;
+    private Observable mPageLoadTarget;
+    private Observable mPageLoadObservable;
+
+    private boolean mbCurrBMSynced = false;
+
+    private Tab mActiveTab;
+    private TitleBar mTitleBar;
+
+    private static final float mMinAlpha = 0.5f;
+    private static final int mMinProgress = 85;
+    private static final int mProgressWaitMS = 1000;
+    private static final int EDGE_SWIPE_INVALID_INDEX = -2;
+
+    private CountDownTimer mLoadTimer, mCommitTimer;
+
+    private int mCurrIndex = EDGE_SWIPE_INVALID_INDEX;
+    private int mPrevIndex;
+    private int mNextIndex;
+    private int mMaxIndex;
+
+    private EdgeSwipeModel mModel;
+    private EdgeSwipeView mView;
+
+    public EdgeSwipeController(View container,
+                               int stationaryViewId,
+                               int slidingViewId,
+                               int slidingViewShadowId,
+                               int opacityViewId,
+                               int liveViewId,
+                               int viewGroupId,
+                               BaseUi ui) {
+        DraggableFrameLayout viewGroup = (DraggableFrameLayout)
+                container.findViewById(viewGroupId);
+
+        mActiveTab = ui.getActiveTab();
+        mTitleBar = ui.getTitleBar();
+
+        mModel = new EdgeSwipeModel(mActiveTab, mTitleBar);
+        mView = new EdgeSwipeView(
+                container,
+                stationaryViewId,
+                slidingViewId,
+                slidingViewShadowId,
+                opacityViewId,
+                liveViewId,
+                viewGroupId,
+                mTitleBar);
+
+        mPageLoadTarget = mActiveTab.getTabHistoryUpdateObservable();
+        mPageLoadObservable = Activator.activate(
+                new Observable.Observer() {
+                    @Override
+                    public void onChange(Object... params) {
+                        if (mDragHelper == null ||
+                                mPageLoadTarget == null) {
+                            return;
+                        }
+
+                        synchronized (this) {
+                            int index = (int) params[0];
+                            if (mState == ViewDragHelper.STATE_IDLE && index == mCurrIndex) {
+                                monitorProgressAtHistoryUpdate(index);
+                            }
+                        }
+                    }
+                },
+                mPageLoadTarget
+        );
+
+        mDragHelper = ViewDragHelper.create(viewGroup, 0.5f, this);
+        mDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT | ViewDragHelper.EDGE_RIGHT);
+        viewGroup.setDragHelper(mDragHelper);
+    }
+
+    private void swipeSessionCleanup() {
+        mView.goLive();
+        mModel.cleanup();
+        mCurrIndex = EDGE_SWIPE_INVALID_INDEX;
+        mState = ViewDragHelper.STATE_IDLE;
+    }
+
+    private boolean setState(int curState, int newState) {
+        if (mState == curState) {
+            mState = newState;
+            return true;
+        }
+        return false;
+    }
+
+    public void cleanup() {
+        if (mPageLoadObservable != null) {
+            mPageLoadObservable.onOff(false);
+            synchronized (this) {
+                mDragHelper.cancel();
+                swipeSessionCleanup();
+            }
+        }
+    }
+
+    public void onConfigurationChanged() {
+        synchronized (this) {
+            swipeSessionCleanup();
+        }
+    }
+
+    private void showCurrBMInStationaryView() {
+        if (!mbCurrBMSynced) {
+            Bitmap currBM = mModel.readSnapshot(mCurrIndex);
+            if (currBM != null) {
+                mView.setStationaryViewBitmap(currBM);
+                mbCurrBMSynced = true;
+            }
+        }
+    }
+
+    private void showCurrBMInSlidingView() {
+        if (!mbCurrBMSynced) {
+            Bitmap currBM = mModel.readSnapshot(mCurrIndex);
+            mView.setSlidingViewBitmap(currBM);
+            if (currBM != null) {
+                mbCurrBMSynced = true;
+            }
+        }
+    }
+
+    private Bitmap getGrayscale(Bitmap bitmap)
+    {
+        if (bitmap == null)
+            return null;
+
+        int height = bitmap.getHeight();
+        int width = bitmap.getWidth();
+
+        Bitmap gray = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+        Canvas c = new Canvas(gray);
+        Paint paint = new Paint();
+        ColorMatrix cm = new ColorMatrix();
+
+        cm.setSaturation(0);
+
+        ColorMatrixColorFilter f = new ColorMatrixColorFilter(cm);
+
+        paint.setColorFilter(f);
+
+        c.drawBitmap(bitmap, 0, 0, paint);
+        return gray;
+    }
+
+    private void monitorProgressAtLoad(final int pageIndex) {
+        if (mLoadTimer != null) {
+            mLoadTimer.cancel();
+        }
+
+        mLoadTimer = new CountDownTimer(mProgressWaitMS * 5, mProgressWaitMS) {
+            boolean mGrayBM = false;
+
+            public void onTick(long msRemain) {
+                if (msRemain > mProgressWaitMS * 4) {
+                    return;
+                }
+                synchronized (this) {
+                    if (mTitleBar.getProgressView().getProgressPercent() >= mMinProgress) {
+                        if (mState == ViewDragHelper.STATE_IDLE && pageIndex == mCurrIndex) {
+                            swipeSessionCleanup();
+
+                        }
+                        cancel();
+                    } else {
+                        if (mGrayBM) {
+                            return;
+                        }
+                        mView.setStationaryViewBitmap(
+                                getGrayscale(getSnapshotOrFavicon(pageIndex)));
+                        mGrayBM = true;
+                    }
+                }
+            }
+
+            public void onFinish() {
+                mGrayBM = false;
+                synchronized (this) {
+                    if (mTitleBar.getProgressView().getProgressPercent() >= mMinProgress) {
+                        if (mState == ViewDragHelper.STATE_IDLE && pageIndex == mCurrIndex) {
+                            swipeSessionCleanup();
+                        }
+                        cancel();
+                    }
+                }
+            }
+        }.start();
+    }
+
+    private void monitorProgressAtHistoryUpdate(final int pageIndex) {
+        if (mCommitTimer != null) {
+            mCommitTimer.cancel();
+        }
+
+        if (mTitleBar.getProgressView().getProgressPercent() >= mMinProgress
+                && mActiveTab.getWebView().getLastCommittedHistoryIndex() == pageIndex) {
+            swipeSessionCleanup();
+            return;
+        }
+
+        mCommitTimer = new CountDownTimer(mProgressWaitMS * 5, mProgressWaitMS) {
+            public void onTick(long msRemain) {
+                synchronized (this) {
+                    if (mTitleBar.getProgressView().getProgressPercent() >= mMinProgress) {
+                        if (mState == ViewDragHelper.STATE_IDLE && pageIndex == mCurrIndex) {
+                            swipeSessionCleanup();
+
+                        }
+                        cancel();
+                    }
+                }
+            }
+
+            public void onFinish() {
+                synchronized (this) {
+                    if (mState == ViewDragHelper.STATE_IDLE && pageIndex == mCurrIndex) {
+                        swipeSessionCleanup();
+                    }
+                }
+            }
+        }.start();
+    }
+
+    private boolean isPortrait(Bitmap bitmap) {
+        return (bitmap.getHeight() < bitmap.getWidth());
+    }
+
+    private Bitmap getSnapshotOrFavicon(int index) {
+        Bitmap bm = mModel.readSnapshot(index);
+        if (bm == null || mView.isPortrait() != isPortrait(bm))  {
+            WebHistoryItem item = mActiveTab.getWebView()
+                    .copyBackForwardList().getItemAtIndex(index);
+            if (item != null) {
+                bm = item.getFavicon();
+            }
+        }
+        return bm;
+    }
+
+    public void onViewDragStateChanged(int state) {
+        synchronized (this) {
+            if (mState != ViewDragHelper.STATE_SETTLING || state != ViewDragHelper.STATE_IDLE) {
+                return;
+            }
+
+            mView.hideSlidingViews();
+
+            if (mView.isLive()) {
+                return;
+            }
+
+            if (mbNavigated) {
+                mView.setStationaryViewBitmap(getSnapshotOrFavicon(mCurrIndex));
+                mView.setStationaryViewAlpha(1.0f);
+            } else {
+                swipeSessionCleanup();
+            }
+
+            mView.invalidate();
+
+            setState(ViewDragHelper.STATE_SETTLING, ViewDragHelper.STATE_IDLE);
+        }
+    }
+
+    public void onViewReleased(View releasedChild, float xvel, float yvel) {
+        synchronized (this) {
+            if (!setState(ViewDragHelper.STATE_DRAGGING, ViewDragHelper.STATE_SETTLING)) {
+                mOldX = 0;
+                mOldDx = 0;
+                return;
+            }
+
+            mbNavigated = true;
+
+            boolean bCrossedEventHorizon = Math.abs(mOldX) > mView.getWidth() / 2;
+
+            if (mCurrIndex >= 0) {
+                if ((xvel > 0 || (xvel == 0 && mOldX > 0 && bCrossedEventHorizon))
+                        && mFromEdge == ViewDragHelper.EDGE_LEFT
+                        && mActiveTab.getWebView().canGoToHistoryIndex(mCurrIndex - 1)) {
+                    mCurrIndex -= 1;
+                    mActiveTab.getWebView().stopLoading();
+                    mActiveTab.getWebView().goToHistoryIndex(mCurrIndex);
+                    monitorProgressAtLoad(mCurrIndex);
+                    mDragHelper.settleCapturedViewAt(
+                            releasedChild.getMeasuredWidth(),
+                            releasedChild.getTop());
+                } else if ((xvel < 0 || (xvel == 0 && mOldX < 0 && bCrossedEventHorizon))
+                        && mFromEdge == ViewDragHelper.EDGE_RIGHT
+                        && mActiveTab.getWebView().canGoToHistoryIndex(mCurrIndex + 1)) {
+                    mCurrIndex += 1;
+                    mActiveTab.getWebView().stopLoading();
+                    mActiveTab.getWebView().goToHistoryIndex(mCurrIndex);
+                    monitorProgressAtLoad(mCurrIndex);
+                    mDragHelper.settleCapturedViewAt(
+                            -releasedChild.getMeasuredWidth(),
+                            releasedChild.getTop());
+                    mView.goDormant();
+                } else {
+                    mbNavigated = false;
+                    mDragHelper.settleCapturedViewAt(0, releasedChild.getTop());
+                }
+            }
+            mOldX = 0;
+            mOldDx = 0;
+
+            mView.invalidate();
+        }
+    }
+
+    public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
+        float alpha = ((float) Math.abs(left)) / mView.getMeasuredWidth();
+
+        synchronized (this) {
+            switch (mFromEdge) {
+                case ViewDragHelper.EDGE_LEFT:
+                    if (mView.isLive()) {
+                        return;
+                    }
+
+                    if (mState != ViewDragHelper.STATE_IDLE) {
+                        mView.moveShadowView(left);
+                    }
+
+                    showCurrBMInSlidingView();
+
+                    if (mPrevIndex >= 0) {
+                        if (!mView.stationaryViewHasImage()) {
+                            mView.setStationaryViewBitmap(getSnapshotOrFavicon(mPrevIndex));
+                        }
+
+                        if (mActiveTab.getWebView().canGoToHistoryIndex(mPrevIndex)) {
+                            mView.setStationaryViewAlpha(mMinAlpha + alpha * (1 - mMinAlpha));
+                        }
+                    }
+                    break;
+                case ViewDragHelper.EDGE_RIGHT:
+                    if (mState != ViewDragHelper.STATE_IDLE) {
+                        mView.moveShadowView(mView.getMeasuredWidth() + left);
+
+                        if (!mView.slidingViewHasImage() && mNextIndex < mMaxIndex) {
+                            mView.setSlidingViewBitmap(getSnapshotOrFavicon(mNextIndex));
+                        }
+
+                        showCurrBMInStationaryView();
+                        if (mbCurrBMSynced) {
+                            mView.goDormant();
+                        }
+                    }
+                    if (mNextIndex < mMaxIndex &&
+                            mActiveTab.getWebView().canGoToHistoryIndex(mNextIndex)) {
+                        mView.setStationaryViewAlpha(mMinAlpha + (1 - alpha) * (1 - mMinAlpha));
+                    }
+                    break;
+                default:
+                    break;
+            }
+        }
+    }
+
+    public void onEdgeDragStarted(int edgeFlags, int pointerId) {
+        synchronized (this) {
+            if (mActiveTab.isPrivateBrowsingEnabled()) {
+                mDragHelper.abort();
+                return;
+            }
+
+            if (mDragHelper.getViewDragState() != ViewDragHelper.STATE_IDLE ||
+                    !setState(ViewDragHelper.STATE_IDLE, ViewDragHelper.STATE_DRAGGING)) {
+                mDragHelper.abort();
+                return;
+            }
+
+            if ((edgeFlags & mFromEdge) != mFromEdge || mCurrIndex == EDGE_SWIPE_INVALID_INDEX) {
+                onEdgeTouched(edgeFlags, pointerId);
+            }
+
+            mbCurrBMSynced = false;
+
+            switch (mFromEdge) {
+                case ViewDragHelper.EDGE_LEFT:
+                    mView.showSlidingViews();
+                    mView.goDormant();
+                    mPrevIndex = mCurrIndex - 1;
+                    mView.setStationaryViewBitmap(getSnapshotOrFavicon(mPrevIndex));
+                    showCurrBMInSlidingView();
+                    break;
+                case ViewDragHelper.EDGE_RIGHT:
+                    mView.showSlidingViews();
+                    mNextIndex = mCurrIndex + 1;
+                    mView.setSlidingViewBitmap(getSnapshotOrFavicon(mNextIndex));
+                    showCurrBMInStationaryView();
+                    if (mbCurrBMSynced)
+                        mView.goDormant();
+                    break;
+                default:
+                    break;
+            }
+        }
+    }
+
+    public int getOrderedChildIndex(int index) {
+        return mView.slidingViewIndex();
+    }
+
+    public void onEdgeTouched (int edgeFlags, int pointerId) {
+        synchronized (this) {
+            if (mActiveTab.isPrivateBrowsingEnabled()) {
+                mDragHelper.abort();
+                return;
+            }
+
+            if (mState != ViewDragHelper.STATE_IDLE && mCurrIndex != EDGE_SWIPE_INVALID_INDEX) {
+                mDragHelper.abort();
+                return;
+            }
+
+            mView.init();
+
+            if (mCurrIndex == EDGE_SWIPE_INVALID_INDEX) {
+                mCurrIndex = mActiveTab.getWebView().getLastCommittedHistoryIndex();
+            }
+
+            mMaxIndex = mActiveTab.getWebView().copyBackForwardList().getSize() - 1;
+            mModel.updateSnapshot(mCurrIndex);
+
+            if (ViewDragHelper.EDGE_LEFT == (edgeFlags & ViewDragHelper.EDGE_LEFT)) {
+                mFromEdge = ViewDragHelper.EDGE_LEFT;
+                mView.slidingViewTouched(mFromEdge);
+                if (mCurrIndex > 0) {
+                    mModel.fetchSnapshot(mCurrIndex - 1);
+                }
+            } else if (ViewDragHelper.EDGE_RIGHT == (edgeFlags & ViewDragHelper.EDGE_RIGHT)) {
+                mFromEdge = ViewDragHelper.EDGE_RIGHT;
+                mView.slidingViewTouched(mFromEdge);
+                if (mCurrIndex < mMaxIndex) {
+                    mModel.fetchSnapshot(mCurrIndex + 1);
+                }
+            }
+        }
+    }
+
+    public int getViewHorizontalDragRange(View child) {
+        return child.getMeasuredWidth();
+    }
+
+    public boolean tryCaptureView(View child, int pointerId) {
+        return (mState == ViewDragHelper.STATE_DRAGGING && mView.allowCapture(child));
+    }
+
+    public int clampViewPositionHorizontal(View child, int left, int dx) {
+        if (mOldX != 0 && Math.signum(dx) != Math.signum(mOldDx)) {
+            mOldDx = dx;
+            return mOldX;
+        }
+
+        switch (mFromEdge) {
+            case ViewDragHelper.EDGE_LEFT:
+                if (left < 0) {
+                    mOldDx = dx;
+                    return mOldX;
+                }
+                if (!mActiveTab.getWebView().canGoToHistoryIndex(mPrevIndex)) {
+                    if (Math.abs(left) >= child.getMeasuredWidth() / 3) {
+                        return child.getMeasuredWidth() / 3;
+                    }
+                }
+                break;
+            case ViewDragHelper.EDGE_RIGHT:
+                if (left > 0) {
+                    mOldDx = dx;
+                    return mOldX;
+                }
+                if (!mActiveTab.getWebView().canGoToHistoryIndex(mNextIndex)) {
+                    if (Math.abs(left) >= child.getMeasuredWidth() / 3) {
+                        return -child.getMeasuredWidth() / 3;
+                    }
+                }
+                break;
+            default:
+                break;
+        }
+
+        mOldX = left;
+        mOldDx = dx;
+        return left;
+    }
+}
+