Merge "Add camera affordance to navigation bar on phones" into klp-dev
diff --git a/core/java/com/android/internal/policy/IKeyguardService.aidl b/core/java/com/android/internal/policy/IKeyguardService.aidl
index d1f7fa3..dd2e006 100644
--- a/core/java/com/android/internal/policy/IKeyguardService.aidl
+++ b/core/java/com/android/internal/policy/IKeyguardService.aidl
@@ -15,6 +15,8 @@
  */
 package com.android.internal.policy;
 
+import android.view.MotionEvent;
+
 import com.android.internal.policy.IKeyguardShowCallback;
 import com.android.internal.policy.IKeyguardExitCallback;
 
@@ -39,4 +41,5 @@
     oneway void doKeyguardTimeout(in Bundle options);
     oneway void setCurrentUser(int userId);
     oneway void showAssistant();
+    oneway void dispatch(in MotionEvent event);
 }
diff --git a/packages/Keyguard/res/drawable-hdpi/kg_widget_bg_padded.9.png b/packages/Keyguard/res/drawable-hdpi/kg_widget_bg_padded.9.png
index dff1dfa..c697f44 100644
--- a/packages/Keyguard/res/drawable-hdpi/kg_widget_bg_padded.9.png
+++ b/packages/Keyguard/res/drawable-hdpi/kg_widget_bg_padded.9.png
Binary files differ
diff --git a/packages/Keyguard/res/drawable-mdpi/kg_widget_bg_padded.9.png b/packages/Keyguard/res/drawable-mdpi/kg_widget_bg_padded.9.png
index c313df1..562bc7e 100644
--- a/packages/Keyguard/res/drawable-mdpi/kg_widget_bg_padded.9.png
+++ b/packages/Keyguard/res/drawable-mdpi/kg_widget_bg_padded.9.png
Binary files differ
diff --git a/packages/Keyguard/res/drawable-xhdpi/kg_widget_bg_padded.9.png b/packages/Keyguard/res/drawable-xhdpi/kg_widget_bg_padded.9.png
index a84bfa3..b3edfd7 100644
--- a/packages/Keyguard/res/drawable-xhdpi/kg_widget_bg_padded.9.png
+++ b/packages/Keyguard/res/drawable-xhdpi/kg_widget_bg_padded.9.png
Binary files differ
diff --git a/packages/Keyguard/res/drawable-xxhdpi/kg_widget_bg_padded.9.png b/packages/Keyguard/res/drawable-xxhdpi/kg_widget_bg_padded.9.png
new file mode 100644
index 0000000..8d31fee
--- /dev/null
+++ b/packages/Keyguard/res/drawable-xxhdpi/kg_widget_bg_padded.9.png
Binary files differ
diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardHostView.java b/packages/Keyguard/src/com/android/keyguard/KeyguardHostView.java
index fbe3a9c..0787286 100644
--- a/packages/Keyguard/src/com/android/keyguard/KeyguardHostView.java
+++ b/packages/Keyguard/src/com/android/keyguard/KeyguardHostView.java
@@ -1650,4 +1650,9 @@
         mActivityLauncher.launchActivityWithAnimation(
                 intent, false, opts.toBundle(), null, null);
     }
+
+    public void dispatch(MotionEvent event) {
+        mAppWidgetContainer.handleExternalCameraEvent(event);
+    }
+
 }
diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardService.java b/packages/Keyguard/src/com/android/keyguard/KeyguardService.java
index a70e5bd..77006c5 100644
--- a/packages/Keyguard/src/com/android/keyguard/KeyguardService.java
+++ b/packages/Keyguard/src/com/android/keyguard/KeyguardService.java
@@ -28,6 +28,7 @@
 import android.os.Debug;
 import android.os.IBinder;
 import android.util.Log;
+import android.view.MotionEvent;
 
 import com.android.internal.policy.IKeyguardService;
 import com.android.internal.policy.IKeyguardExitCallback;
@@ -132,6 +133,10 @@
             checkPermission();
             mKeyguardViewMediator.showAssistant();
         }
+        public void dispatch(MotionEvent event) {
+            checkPermission();
+            mKeyguardViewMediator.dispatch(event);
+        }
     };
 
 }
diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardViewManager.java b/packages/Keyguard/src/com/android/keyguard/KeyguardViewManager.java
index 35bea26..4837458 100644
--- a/packages/Keyguard/src/com/android/keyguard/KeyguardViewManager.java
+++ b/packages/Keyguard/src/com/android/keyguard/KeyguardViewManager.java
@@ -38,6 +38,7 @@
 import android.util.SparseArray;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
+import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewManager;
@@ -425,4 +426,10 @@
             mKeyguardView.showAssistant();
         }
     }
+
+    public void dispatch(MotionEvent event) {
+        if (mKeyguardView != null) {
+            mKeyguardView.dispatch(event);
+        }
+    }
 }
diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardViewMediator.java b/packages/Keyguard/src/com/android/keyguard/KeyguardViewMediator.java
index e746f72..478096c 100644
--- a/packages/Keyguard/src/com/android/keyguard/KeyguardViewMediator.java
+++ b/packages/Keyguard/src/com/android/keyguard/KeyguardViewMediator.java
@@ -49,6 +49,7 @@
 import android.util.Log;
 import android.util.Slog;
 import android.view.KeyEvent;
+import android.view.MotionEvent;
 import android.view.WindowManager;
 import android.view.WindowManagerPolicy;
 
@@ -120,6 +121,7 @@
     private static final int SET_HIDDEN = 12;
     private static final int KEYGUARD_TIMEOUT = 13;
     private static final int SHOW_ASSISTANT = 14;
+    private static final int DISPATCH_EVENT = 15;
 
     /**
      * The default amount of time we stay awake (used for all key input)
@@ -1066,6 +1068,9 @@
                 case SHOW_ASSISTANT:
                     handleShowAssistant();
                     break;
+                case DISPATCH_EVENT:
+                    handleDispatchEvent((MotionEvent) msg.obj);
+                    break;
             }
         }
     };
@@ -1102,6 +1107,10 @@
         sendUserPresentBroadcast();
     }
 
+    protected void handleDispatchEvent(MotionEvent event) {
+        mKeyguardViewManager.dispatch(event);
+    }
+
     private void sendUserPresentBroadcast() {
         final UserHandle currentUser = new UserHandle(mLockPatternUtils.getCurrentUser());
         mContext.sendBroadcastAsUser(USER_PRESENT_INTENT, currentUser);
@@ -1327,4 +1336,9 @@
     public static MultiUserAvatarCache getAvatarCache() {
         return sMultiUserAvatarCache;
     }
+
+    public void dispatch(MotionEvent event) {
+        Message msg = mHandler.obtainMessage(DISPATCH_EVENT, event);
+        mHandler.sendMessage(msg);
+    }
 }
diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardViewStateManager.java b/packages/Keyguard/src/com/android/keyguard/KeyguardViewStateManager.java
index e85f6df..0539e82 100644
--- a/packages/Keyguard/src/com/android/keyguard/KeyguardViewStateManager.java
+++ b/packages/Keyguard/src/com/android/keyguard/KeyguardViewStateManager.java
@@ -23,6 +23,7 @@
         SlidingChallengeLayout.OnChallengeScrolledListener,
         ChallengeLayout.OnBouncerStateChangedListener {
 
+    private static final int WARP_FADE_DURATION = 250;
     private KeyguardWidgetPager mKeyguardWidgetPager;
     private ChallengeLayout mChallengeLayout;
     private KeyguardHostView mKeyguardHostView;
@@ -32,6 +33,7 @@
     private KeyguardSecurityView mKeyguardSecurityContainer;
     private static final int SCREEN_ON_HINT_DURATION = 1000;
     private static final int SCREEN_ON_RING_HINT_DELAY = 300;
+    private static final boolean SHOW_INITIAL_PAGE_HINTS = false;
     Handler mMainQueue = new Handler(Looper.myLooper());
 
     int mLastScrollState = SlidingChallengeLayout.SCROLL_STATE_IDLE;
@@ -167,6 +169,16 @@
         mCurrentPage = newPageIndex;
     }
 
+    public void onPageBeginWarp() {
+        // fadeOutSecurity(WARP_FADE_DURATION);
+        // mKeyguardWidgetPager.showNonWarpViews(WARP_FADE_DURATION, false);
+    }
+
+    public void onPageEndWarp() {
+        // fadeInSecurity(WARP_FADE_DURATION);
+        // mKeyguardWidgetPager.showNonWarpViews(WARP_FADE_DURATION, true);
+    }
+
     private int getChallengeTopRelativeToFrame(KeyguardWidgetFrame frame, int top) {
         mTmpPoint[0] = 0;
         mTmpPoint[1] = top;
@@ -296,7 +308,9 @@
                 mKeyguardSecurityContainer.showUsabilityHint();
             }
         } , SCREEN_ON_RING_HINT_DELAY);
-        mKeyguardWidgetPager.showInitialPageHints();
+        if (SHOW_INITIAL_PAGE_HINTS) {
+            mKeyguardWidgetPager.showInitialPageHints();
+        }
         if (mHideHintsRunnable != null) {
             mMainQueue.postDelayed(mHideHintsRunnable, SCREEN_ON_HINT_DURATION);
         }
diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardWidgetPager.java b/packages/Keyguard/src/com/android/keyguard/KeyguardWidgetPager.java
index c566457..8d2f21b 100644
--- a/packages/Keyguard/src/com/android/keyguard/KeyguardWidgetPager.java
+++ b/packages/Keyguard/src/com/android/keyguard/KeyguardWidgetPager.java
@@ -186,6 +186,16 @@
     }
 
     @Override
+    public void onPageBeginWarp() {
+        mViewStateManager.onPageBeginWarp();
+    }
+
+    @Override
+    public void onPageEndWarp() {
+        mViewStateManager.onPageEndWarp();
+    }
+
+    @Override
     public void sendAccessibilityEvent(int eventType) {
         if (eventType != AccessibilityEvent.TYPE_VIEW_SCROLLED || isPageMoving()) {
             super.sendAccessibilityEvent(eventType);
@@ -923,4 +933,27 @@
 
         return flags;
     }
+
+    public void handleExternalCameraEvent(MotionEvent event) {
+        beginCameraEvent();
+        int cameraPage = getPageCount() - 1;
+        boolean endWarp = false;
+        if (isCameraPage(cameraPage)) {
+            switch (event.getAction()) {
+                case MotionEvent.ACTION_DOWN:
+                    userActivity();
+                    startWarp(cameraPage);
+                    break;
+                case MotionEvent.ACTION_UP:
+                case MotionEvent.ACTION_CANCEL:
+                    endWarp = true;
+                    break;
+            }
+            dispatchTouchEvent(event);
+            // This has to happen after the event has been handled by the real widget pager
+            if (endWarp) endWarp();
+        }
+        endCameraEvent();
+    }
+
 }
diff --git a/packages/Keyguard/src/com/android/keyguard/PagedView.java b/packages/Keyguard/src/com/android/keyguard/PagedView.java
index 881d14d..cbf7946 100644
--- a/packages/Keyguard/src/com/android/keyguard/PagedView.java
+++ b/packages/Keyguard/src/com/android/keyguard/PagedView.java
@@ -62,6 +62,7 @@
 public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarchyChangeListener {
     private static final String TAG = "WidgetPagedView";
     private static final boolean DEBUG = false;
+    private static final boolean DEBUG_WARP = false;
     protected static final int INVALID_PAGE = -1;
 
     // the min drag distance for a fling to register, to prevent random page shifts
@@ -130,6 +131,7 @@
     protected final static int TOUCH_STATE_REORDERING = 4;
 
     protected final static float ALPHA_QUANTIZE_LEVEL = 0.0001f;
+    protected final static float TOUCH_SLOP_SCALE = 1.0f;
 
     protected int mTouchState = TOUCH_STATE_REST;
     protected boolean mForceScreenScrolled = false;
@@ -250,6 +252,10 @@
     // Bouncer
     private boolean mTopAlignPageWhenShrinkingForBouncer = false;
 
+    // Page warping
+    private int mPageSwapIndex = -1;
+    private boolean mIsCameraEvent;
+
     public interface PageSwitchListener {
         void onPageSwitching(View newPage, int newPageIndex);
         void onPageSwitched(View newPage, int newPageIndex);
@@ -469,15 +475,26 @@
     }
 
     protected void pageBeginMoving() {
+        if (DEBUG_WARP) Log.v(TAG, "pageBeginMoving(" + mIsPageMoving + ")");
         if (!mIsPageMoving) {
             mIsPageMoving = true;
+            if (mPageSwapIndex != -1) {
+                onPageBeginWarp();
+                swapPages(mPageSwapIndex, getPageCount() - 1);
+            }
             onPageBeginMoving();
         }
     }
 
     protected void pageEndMoving() {
+        if (DEBUG_WARP) Log.v(TAG, "pageEndMoving(" + mIsPageMoving + ")");
         if (mIsPageMoving) {
             mIsPageMoving = false;
+            if (mPageSwapIndex != -1) {
+                swapPages(mPageSwapIndex, getPageCount() - 1);
+                onPageEndWarp();
+                mPageSwapIndex = -1;
+            }
             onPageEndMoving();
         }
     }
@@ -737,6 +754,11 @@
             setHorizontalScrollBarEnabled(true);
             mFirstLayout = false;
         }
+        // If a page was swapped when we rebuilt the layout, swap it again now.
+        if (mPageSwapIndex  != -1) {
+            if (DEBUG_WARP) Log.v(TAG, "onLayout: swapping pages");
+            swapPages(mPageSwapIndex, getPageCount() - 1);
+        }
     }
 
     protected void screenScrolled(int screenCenter) {
@@ -1071,7 +1093,9 @@
                  * whether the user has moved far enough from his original down touch.
                  */
                 if (mActivePointerId != INVALID_POINTER) {
-                    determineScrollingStart(ev);
+                    if (mIsCameraEvent || determineScrollingStart(ev)) {
+                        startScrolling(ev);
+                    }
                     break;
                 }
                 // if mActivePointerId is INVALID_POINTER, then we must have missed an ACTION_DOWN
@@ -1082,27 +1106,8 @@
             }
 
             case MotionEvent.ACTION_DOWN: {
-                final float x = ev.getX();
-                final float y = ev.getY();
-                // Remember location of down touch
-                mDownMotionX = x;
-                mDownMotionY = y;
-                mDownScrollX = getScrollX();
-                mLastMotionX = x;
-                mLastMotionY = y;
-                float[] p = mapPointFromViewToParent(this, x, y);
-                mParentDownMotionX = p[0];
-                mParentDownMotionY = p[1];
-                mLastMotionXRemainder = 0;
-                mTotalMotionX = 0;
-                mActivePointerId = ev.getPointerId(0);
-
-                // Determine if the down event is within the threshold to be an edge swipe
-                int leftEdgeBoundary = getViewportOffsetX() + mEdgeSwipeRegionSize;
-                int rightEdgeBoundary = getMeasuredWidth() - getViewportOffsetX() - mEdgeSwipeRegionSize;
-                if ((mDownMotionX <= leftEdgeBoundary || mDownMotionX >= rightEdgeBoundary)) {
-                    mDownEventOnEdge = true;
-                }
+                // Remember where the motion event started
+                saveDownState(ev);
 
                 /*
                  * If being flinged and user touches the screen, initiate drag;
@@ -1112,25 +1117,29 @@
                 final int xDist = Math.abs(mScroller.getFinalX() - mScroller.getCurrX());
                 final boolean finishedScrolling = (mScroller.isFinished() || xDist < mTouchSlop);
                 if (finishedScrolling) {
-                    mTouchState = TOUCH_STATE_REST;
+                    setTouchState(TOUCH_STATE_REST);
                     mScroller.abortAnimation();
                 } else {
-                    if (isTouchPointInViewportWithBuffer((int) mDownMotionX, (int) mDownMotionY)) {
-                        mTouchState = TOUCH_STATE_SCROLLING;
+                    if (mIsCameraEvent || isTouchPointInViewportWithBuffer(
+                            (int) mDownMotionX, (int) mDownMotionY)) {
+                        setTouchState(TOUCH_STATE_SCROLLING);
                     } else {
-                        mTouchState = TOUCH_STATE_REST;
+                        setTouchState(TOUCH_STATE_REST);
                     }
                 }
 
                 // check if this can be the beginning of a tap on the side of the pages
                 // to scroll the current page
                 if (!DISABLE_TOUCH_SIDE_PAGES) {
-                    if (mTouchState != TOUCH_STATE_PREV_PAGE && mTouchState != TOUCH_STATE_NEXT_PAGE) {
+                    if (mTouchState != TOUCH_STATE_PREV_PAGE
+                            && mTouchState != TOUCH_STATE_NEXT_PAGE) {
                         if (getChildCount() > 0) {
+                            float x = ev.getX();
+                            float y = ev.getY();
                             if (hitsPreviousPage(x, y)) {
-                                mTouchState = TOUCH_STATE_PREV_PAGE;
+                                setTouchState(TOUCH_STATE_PREV_PAGE);
                             } else if (hitsNextPage(x, y)) {
-                                mTouchState = TOUCH_STATE_NEXT_PAGE;
+                                setTouchState(TOUCH_STATE_NEXT_PAGE);
                             }
                         }
                     }
@@ -1160,48 +1169,85 @@
         return mTouchState != TOUCH_STATE_REST;
     }
 
-    protected void determineScrollingStart(MotionEvent ev) {
-        determineScrollingStart(ev, 1.0f);
+    private void setTouchState(int touchState) {
+        if (mTouchState != touchState) {
+            onTouchStateChanged(touchState);
+            mTouchState = touchState;
+        }
+    }
+
+    void onTouchStateChanged(int newTouchState) {
+        if (DEBUG) {
+            Log.v(TAG, "onTouchStateChanged(old="+ mTouchState + ", new=" + newTouchState + ")");
+        }
+    }
+
+    /**
+     * Save the state when we get {@link MotionEvent#ACTION_DOWN}
+     * @param ev
+     */
+    private void saveDownState(MotionEvent ev) {
+        // Remember where the motion event started
+        mDownMotionX = mLastMotionX = ev.getX();
+        mDownMotionY = mLastMotionY = ev.getY();
+        mDownScrollX = getScrollX();
+        float[] p = mapPointFromViewToParent(this, mLastMotionX, mLastMotionY);
+        mParentDownMotionX = p[0];
+        mParentDownMotionY = p[1];
+        mLastMotionXRemainder = 0;
+        mTotalMotionX = 0;
+        mActivePointerId = ev.getPointerId(0);
+
+        // Determine if the down event is within the threshold to be an edge swipe
+        int leftEdgeBoundary = getViewportOffsetX() + mEdgeSwipeRegionSize;
+        int rightEdgeBoundary = getMeasuredWidth() - getViewportOffsetX() - mEdgeSwipeRegionSize;
+        if ((mDownMotionX <= leftEdgeBoundary || mDownMotionX >= rightEdgeBoundary)) {
+            mDownEventOnEdge = true;
+        }
     }
 
     /*
      * Determines if we should change the touch state to start scrolling after the
      * user moves their touch point too far.
      */
-    protected void determineScrollingStart(MotionEvent ev, float touchSlopScale) {
+    protected boolean determineScrollingStart(MotionEvent ev) {
         // Disallow scrolling if we don't have a valid pointer index
         final int pointerIndex = ev.findPointerIndex(mActivePointerId);
-        if (pointerIndex == -1) return;
+        if (pointerIndex == -1) return false;
 
         // Disallow scrolling if we started the gesture from outside the viewport
         final float x = ev.getX(pointerIndex);
         final float y = ev.getY(pointerIndex);
-        if (!isTouchPointInViewportWithBuffer((int) x, (int) y)) return;
+        if (!isTouchPointInViewportWithBuffer((int) x, (int) y)) return false;
 
         // If we're only allowing edge swipes, we break out early if the down event wasn't
         // at the edge.
-        if (mOnlyAllowEdgeSwipes && !mDownEventOnEdge) return;
+        if (mOnlyAllowEdgeSwipes && !mDownEventOnEdge) return false;
 
         final int xDiff = (int) Math.abs(x - mLastMotionX);
         final int yDiff = (int) Math.abs(y - mLastMotionY);
 
-        final int touchSlop = Math.round(touchSlopScale * mTouchSlop);
+        final int touchSlop = Math.round(TOUCH_SLOP_SCALE * mTouchSlop);
         boolean xPaged = xDiff > mPagingTouchSlop;
         boolean xMoved = xDiff > touchSlop;
         boolean yMoved = yDiff > touchSlop;
 
-        if (xMoved || xPaged || yMoved) {
-            if (mUsePagingTouchSlop ? xPaged : xMoved) {
-                // Scroll if the user moved far enough along the X axis
-                mTouchState = TOUCH_STATE_SCROLLING;
-                mTotalMotionX += Math.abs(mLastMotionX - x);
-                mLastMotionX = x;
-                mLastMotionXRemainder = 0;
-                mTouchX = getViewportOffsetX() + getScrollX();
-                mSmoothingTime = System.nanoTime() / NANOTIME_DIV;
-                pageBeginMoving();
-            }
-        }
+        return (xMoved || xPaged || yMoved) && (mUsePagingTouchSlop ? xPaged : xMoved);
+    }
+
+    private void startScrolling(MotionEvent ev) {
+        // Ignore if we don't have a valid pointer index
+        final int pointerIndex = ev.findPointerIndex(mActivePointerId);
+        if (pointerIndex == -1) return;
+
+        final float x = ev.getX(pointerIndex);
+        setTouchState(TOUCH_STATE_SCROLLING);
+        mTotalMotionX += Math.abs(mLastMotionX - x);
+        mLastMotionX = x;
+        mLastMotionXRemainder = 0;
+        mTouchX = getViewportOffsetX() + getScrollX();
+        mSmoothingTime = System.nanoTime() / NANOTIME_DIV;
+        pageBeginMoving();
     }
 
     protected float getMaxScrollProgress() {
@@ -1322,22 +1368,7 @@
             }
 
             // Remember where the motion event started
-            mDownMotionX = mLastMotionX = ev.getX();
-            mDownMotionY = mLastMotionY = ev.getY();
-            mDownScrollX = getScrollX();
-            float[] p = mapPointFromViewToParent(this, mLastMotionX, mLastMotionY);
-            mParentDownMotionX = p[0];
-            mParentDownMotionY = p[1];
-            mLastMotionXRemainder = 0;
-            mTotalMotionX = 0;
-            mActivePointerId = ev.getPointerId(0);
-
-            // Determine if the down event is within the threshold to be an edge swipe
-            int leftEdgeBoundary = getViewportOffsetX() + mEdgeSwipeRegionSize;
-            int rightEdgeBoundary = getMeasuredWidth() - getViewportOffsetX() - mEdgeSwipeRegionSize;
-            if ((mDownMotionX <= leftEdgeBoundary || mDownMotionX >= rightEdgeBoundary)) {
-                mDownEventOnEdge = true;
-            }
+            saveDownState(ev);
 
             if (mTouchState == TOUCH_STATE_SCROLLING) {
                 pageBeginMoving();
@@ -1479,8 +1510,8 @@
                     removeCallbacks(mSidePageHoverRunnable);
                     mSidePageHoverIndex = -1;
                 }
-            } else {
-                determineScrollingStart(ev);
+            } else if (mIsCameraEvent || determineScrollingStart(ev)) {
+                startScrolling(ev);
             }
             break;
 
@@ -1604,7 +1635,7 @@
     private void resetTouchState() {
         releaseVelocityTracker();
         endReordering();
-        mTouchState = TOUCH_STATE_REST;
+        setTouchState(TOUCH_STATE_REST);
         mActivePointerId = INVALID_POINTER;
         mDownEventOnEdge = false;
     }
@@ -1821,8 +1852,17 @@
     protected void snapToPage(int whichPage, int delta, int duration) {
         snapToPage(whichPage, delta, duration, false);
     }
+
     protected void snapToPage(int whichPage, int delta, int duration, boolean immediate) {
-        mNextPage = whichPage;
+        if (mPageSwapIndex != -1 && whichPage == mPageSwapIndex) {
+            // jump to the last page
+            mNextPage = getPageCount() - 1;
+            if (DEBUG_WARP) Log.v(TAG, "snapToPage(" + whichPage + ") : reset mPageSwapIndex");
+            mPageSwapIndex = -1;
+        } else {
+            mNextPage = whichPage;
+        }
+
         notifyPageSwitching(whichPage);
         View focusedChild = getFocusedChild();
         if (focusedChild != null && whichPage != mCurrentPage &&
@@ -2102,7 +2142,7 @@
         }
 
         // Set the touch state to reordering (allows snapping to pages, dragging a child, etc.)
-        mTouchState = TOUCH_STATE_REORDERING;
+        setTouchState(TOUCH_STATE_REORDERING);
         mIsReordering = true;
 
         // Mark all the non-widget pages as invisible
@@ -2564,4 +2604,46 @@
     public boolean onHoverEvent(android.view.MotionEvent event) {
         return true;
     }
+
+    void beginCameraEvent() {
+        mIsCameraEvent = true;
+    }
+
+    void endCameraEvent() {
+        mIsCameraEvent = false;
+    }
+
+    /**
+     * Swaps the position of the views by setting the left and right edges appropriately.
+     */
+    void swapPages(int indexA, int indexB) {
+        View viewA = getPageAt(indexA);
+        View viewB = getPageAt(indexB);
+        if (viewA != viewB && viewA != null && viewB != null) {
+            int deltaX = viewA.getLeft() - viewB.getLeft();
+            viewA.offsetLeftAndRight(-deltaX);
+            viewB.offsetLeftAndRight(deltaX);
+        }
+    }
+
+    public void startWarp(int pageIndex) {
+        if (DEBUG_WARP) Log.v(TAG, "START WARP");
+        if (pageIndex != mCurrentPage + 1) {
+            mPageSwapIndex = mCurrentPage + 1;
+        }
+    }
+
+    public void endWarp() {
+        if (DEBUG_WARP) Log.v(TAG, "END WARP");
+        // mPageSwapIndex is reset in snapToPage() after the scroll animation completes
+    }
+
+    public void onPageBeginWarp() {
+
+    }
+
+    public void onPageEndWarp() {
+
+    }
+
 }
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index fa5c769..fb73d39 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -52,7 +52,7 @@
     <uses-permission android:name="android.permission.START_ANY_ACTIVITY" />
     <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
     <uses-permission android:name="android.permission.GET_TOP_ACTIVITY_INFO" />
-    
+
     <!-- WindowManager -->
     <uses-permission android:name="android.permission.INTERNAL_SYSTEM_WINDOW" />
     <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
@@ -68,6 +68,9 @@
     <!-- Alarm clocks -->
     <uses-permission android:name="com.android.alarm.permission.SET_ALARM" />
 
+    <!-- Keyguard -->
+    <uses-permission android:name="android.permission.CONTROL_KEYGUARD" />
+
     <application
         android:persistent="true"
         android:allowClearUserData="false"
diff --git a/packages/SystemUI/res/drawable-hdpi/ic_sysbar_camera.png b/packages/SystemUI/res/drawable-hdpi/ic_sysbar_camera.png
new file mode 100644
index 0000000..0bb590e
--- /dev/null
+++ b/packages/SystemUI/res/drawable-hdpi/ic_sysbar_camera.png
Binary files differ
diff --git a/packages/SystemUI/res/drawable-mdpi/ic_sysbar_camera.png b/packages/SystemUI/res/drawable-mdpi/ic_sysbar_camera.png
new file mode 100644
index 0000000..b767098
--- /dev/null
+++ b/packages/SystemUI/res/drawable-mdpi/ic_sysbar_camera.png
Binary files differ
diff --git a/packages/SystemUI/res/drawable-xhdpi/ic_sysbar_camera.png b/packages/SystemUI/res/drawable-xhdpi/ic_sysbar_camera.png
new file mode 100644
index 0000000..ea93f1a
--- /dev/null
+++ b/packages/SystemUI/res/drawable-xhdpi/ic_sysbar_camera.png
Binary files differ
diff --git a/packages/SystemUI/res/drawable-xxhdpi/ic_sysbar_camera.png b/packages/SystemUI/res/drawable-xxhdpi/ic_sysbar_camera.png
new file mode 100644
index 0000000..cda7d4b
--- /dev/null
+++ b/packages/SystemUI/res/drawable-xxhdpi/ic_sysbar_camera.png
Binary files differ
diff --git a/packages/SystemUI/res/layout/navigation_bar.xml b/packages/SystemUI/res/layout/navigation_bar.xml
index 5587f4e..11cbbc7 100644
--- a/packages/SystemUI/res/layout/navigation_bar.xml
+++ b/packages/SystemUI/res/layout/navigation_bar.xml
@@ -145,15 +145,30 @@
                 />
         </LinearLayout>
 
-        <com.android.systemui.statusbar.policy.KeyButtonView
-            android:layout_width="80dp"
-            android:id="@+id/search_light"
-            android:layout_height="match_parent"
-            android:layout_gravity="center_horizontal"
-            android:src="@drawable/search_light"
-            android:scaleType="center"
-            android:visibility="gone"
-            />
+        <FrameLayout
+            android:layout_width="match_parent"
+            android:layout_height="match_parent">
+
+            <com.android.systemui.statusbar.policy.KeyButtonView
+                android:layout_width="80dp"
+                android:id="@+id/search_light"
+                android:layout_height="match_parent"
+                android:layout_gravity="center"
+                android:src="@drawable/search_light"
+                android:scaleType="center"
+                android:visibility="gone"
+                />
+
+            <com.android.systemui.statusbar.policy.KeyButtonView
+                android:id="@+id/camera_button"
+                android:layout_height="match_parent"
+                android:layout_width="80dp"
+                android:layout_gravity="center_vertical|right"
+                android:src="@drawable/ic_sysbar_camera"
+                android:scaleType="center"
+                android:visibility="gone"
+                />
+        </FrameLayout>
 
         <com.android.systemui.statusbar.policy.DeadZone
             android:id="@+id/deadzone"
@@ -299,6 +314,8 @@
             android:visibility="gone"
             />
 
+        <!-- No camera button in landscape mode -->
+
         <com.android.systemui.statusbar.policy.DeadZone
             android:id="@+id/deadzone"
             android:layout_height="match_parent"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardTouchDelegate.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardTouchDelegate.java
new file mode 100644
index 0000000..a6e2347
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardTouchDelegate.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.phone;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.util.Log;
+import android.view.MotionEvent;
+
+import com.android.internal.policy.IKeyguardExitCallback;
+import com.android.internal.policy.IKeyguardShowCallback;
+import com.android.internal.policy.IKeyguardService;
+
+
+/**
+ * Facilitates event communication between navigation bar and keyguard.  Currently used to
+ * control WidgetPager in keyguard to expose the camera widget.
+ *
+ */
+public class KeyguardTouchDelegate {
+    // TODO: propagate changes to these to {@link KeyguardServiceDelegate}
+    static final String KEYGUARD_PACKAGE = "com.android.keyguard";
+    static final String KEYGUARD_CLASS = "com.android.keyguard.KeyguardService";
+
+    IKeyguardService mService;
+
+    protected static final boolean DEBUG = false;
+    protected static final String TAG = "KeyguardTouchDelegate";
+
+    private final ServiceConnection mKeyguardConnection = new ServiceConnection() {
+        @Override
+        public void onServiceConnected(ComponentName name, IBinder service) {
+            Log.v(TAG, "Connected to keyguard");
+            mService = IKeyguardService.Stub.asInterface(service);
+
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName name) {
+            Log.v(TAG, "Disconnected from keyguard");
+            mService = null;
+        }
+
+    };
+
+    public KeyguardTouchDelegate(Context context) {
+        Intent intent = new Intent();
+        intent.setClassName(KEYGUARD_PACKAGE, KEYGUARD_CLASS);
+        if (!context.bindServiceAsUser(intent, mKeyguardConnection,
+                Context.BIND_AUTO_CREATE, UserHandle.OWNER)) {
+            if (DEBUG) Log.v(TAG, "*** Keyguard: can't bind to " + KEYGUARD_CLASS);
+        } else {
+            if (DEBUG) Log.v(TAG, "*** Keyguard started");
+        }
+    }
+
+    public boolean isSecure() {
+        boolean secure = false;
+        if (mService != null) {
+            try {
+                secure = mService.isSecure();
+            } catch (RemoteException e) {
+                Log.e(TAG, "RemoteException calling keyguard.isSecure()!", e);
+            }
+        } else {
+            Log.w(TAG, "isSecure(): NO SERVICE!");
+        }
+        return secure;
+    }
+
+    public boolean dispatch(MotionEvent event) {
+        if (mService != null) {
+            try {
+                mService.dispatch(event);
+            } catch (RemoteException e) {
+                // What to do?
+                Log.e(TAG, "RemoteException sending event to keyguard!", e);
+                return false;
+            }
+            return true;
+        } else {
+            Log.w(TAG, "dispatch(event): NO SERVICE!");
+        }
+        return false;
+    }
+
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarTransitions.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarTransitions.java
index 040b750..04922fb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarTransitions.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarTransitions.java
@@ -67,6 +67,7 @@
         setKeyButtonViewQuiescentAlpha(mView.getHomeButton(), alpha, animate);
         setKeyButtonViewQuiescentAlpha(mView.getRecentsButton(), alpha, animate);
         setKeyButtonViewQuiescentAlpha(mView.getMenuButton(), alpha, animate);
+        setKeyButtonViewQuiescentAlpha(mView.getCameraButton(), alpha, animate);
 
         // apply to lights out
         applyLightsOut(mode == MODE_LIGHTS_OUT, animate, force);
@@ -140,4 +141,4 @@
             return false;
         }
     };
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
index 4849674..03f7fab 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
@@ -17,14 +17,20 @@
 package com.android.systemui.statusbar.phone;
 
 import android.animation.LayoutTransition;
+import android.app.ActivityManagerNative;
 import android.app.StatusBarManager;
+import android.app.admin.DevicePolicyManager;
+import android.content.BroadcastReceiver;
 import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.res.Resources;
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.os.Handler;
 import android.os.Message;
+import android.os.RemoteException;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.Display;
@@ -77,6 +83,17 @@
     final static boolean WORKAROUND_INVALID_LAYOUT = true;
     final static int MSG_CHECK_INVALID_LAYOUT = 8686;
 
+    // used to disable the camera icon in navbar when disabled by DPM
+    private boolean mCameraDisabledByDpm;
+    KeyguardTouchDelegate mTouchDelegate;
+
+    private final OnTouchListener mCameraTouchListener = new OnTouchListener() {
+        @Override
+        public boolean onTouch(View v, MotionEvent event) {
+            return mTouchDelegate.dispatch(event);
+        }
+    };
+
     private class H extends Handler {
         public void handleMessage(Message m) {
             switch (m.what) {
@@ -115,6 +132,26 @@
         getIcons(res);
 
         mBarTransitions = new NavigationBarTransitions(this);
+
+        mTouchDelegate = new KeyguardTouchDelegate(mContext);
+
+        mCameraDisabledByDpm = isCameraDisabledByDpm();
+        watchForDevicePolicyChanges();
+    }
+
+    private void watchForDevicePolicyChanges() {
+        final IntentFilter filter = new IntentFilter();
+        filter.addAction(DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED);
+        mContext.registerReceiver(new BroadcastReceiver() {
+            public void onReceive(Context context, Intent intent) {
+                post(new Runnable() {
+                    @Override
+                    public void run() {
+                        mCameraDisabledByDpm = isCameraDisabledByDpm();
+                    }
+                });
+            }
+        }, filter);
     }
 
     public BarTransitions getBarTransitions() {
@@ -173,6 +210,11 @@
         return mCurrentView.findViewById(R.id.search_light);
     }
 
+    // shown when keyguard is visible and camera is available
+    public View getCameraButton() {
+        return mCurrentView.findViewById(R.id.camera_button);
+    }
+
     private void getIcons(Resources res) {
         mBackIcon = res.getDrawable(R.drawable.ic_sysbar_back);
         mBackLandIcon = res.getDrawable(R.drawable.ic_sysbar_back_land);
@@ -259,7 +301,31 @@
         getHomeButton()   .setVisibility(disableHome       ? View.INVISIBLE : View.VISIBLE);
         getRecentsButton().setVisibility(disableRecent     ? View.INVISIBLE : View.VISIBLE);
 
-        getSearchLight().setVisibility((disableHome && !disableSearch) ? View.VISIBLE : View.GONE);
+        final boolean shouldShowSearch = disableHome && !disableSearch;
+        getSearchLight().setVisibility(shouldShowSearch ? View.VISIBLE : View.GONE);
+        final View cameraButton = getCameraButton();
+        if (cameraButton != null) {
+            cameraButton.setVisibility(
+                    shouldShowSearch && !mCameraDisabledByDpm ? View.VISIBLE : View.GONE);
+        }
+    }
+
+    private boolean isCameraDisabledByDpm() {
+        final DevicePolicyManager dpm =
+                (DevicePolicyManager) mContext.getSystemService(Context.DEVICE_POLICY_SERVICE);
+        if (dpm != null) {
+            try {
+                final int userId = ActivityManagerNative.getDefault().getCurrentUser().id;
+                final int disabledFlags = dpm.getKeyguardDisabledFeatures(null, userId);
+                final  boolean disabledBecauseKeyguardSecure =
+                        (disabledFlags & DevicePolicyManager.KEYGUARD_DISABLE_SECURE_CAMERA) != 0
+                        && mTouchDelegate.isSecure();
+                return dpm.getCameraDisabled(null) || disabledBecauseKeyguardSecure;
+            } catch (RemoteException e) {
+                Log.e(TAG, "Can't get userId", e);
+            }
+        }
+        return false;
     }
 
     public void setSlippery(boolean newSlippery) {
@@ -302,6 +368,14 @@
                                                 : findViewById(R.id.rot270);
 
         mCurrentView = mRotatedViews[Surface.ROTATION_0];
+
+        // Add a touch handler for camera icon for all view orientations.
+        for (int i = 0; i < mRotatedViews.length; i++) {
+            View cameraButton = mRotatedViews[i].findViewById(R.id.camera_button);
+            if (cameraButton != null) {
+                cameraButton.setOnTouchListener(mCameraTouchListener);
+            }
+        }
     }
 
     public boolean isVertical() {
diff --git a/policy/src/com/android/internal/policy/impl/keyguard/KeyguardServiceDelegate.java b/policy/src/com/android/internal/policy/impl/keyguard/KeyguardServiceDelegate.java
index 874076a..56a282b 100644
--- a/policy/src/com/android/internal/policy/impl/keyguard/KeyguardServiceDelegate.java
+++ b/policy/src/com/android/internal/policy/impl/keyguard/KeyguardServiceDelegate.java
@@ -29,8 +29,10 @@
  * local or remote instances of keyguard.
  */
 public class KeyguardServiceDelegate {
-    private static final String KEYGUARD_PACKAGE = "com.android.keyguard";
-    private static final String KEYGUARD_CLASS = "com.android.keyguard.KeyguardService";
+    // TODO: propagate changes to these to {@link KeyguardTouchDelegate}
+    public static final String KEYGUARD_PACKAGE = "com.android.keyguard";
+    public static final String KEYGUARD_CLASS = "com.android.keyguard.KeyguardService";
+
     private static final String TAG = "KeyguardServiceDelegate";
     private static final boolean DEBUG = true;
     protected KeyguardServiceWrapper mKeyguardService;
diff --git a/policy/src/com/android/internal/policy/impl/keyguard/KeyguardServiceWrapper.java b/policy/src/com/android/internal/policy/impl/keyguard/KeyguardServiceWrapper.java
index 6b9c7df..b27584d 100644
--- a/policy/src/com/android/internal/policy/impl/keyguard/KeyguardServiceWrapper.java
+++ b/policy/src/com/android/internal/policy/impl/keyguard/KeyguardServiceWrapper.java
@@ -20,6 +20,7 @@
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.util.Slog;
+import android.view.MotionEvent;
 
 import com.android.internal.policy.IKeyguardShowCallback;
 import com.android.internal.policy.IKeyguardExitCallback;
@@ -187,6 +188,10 @@
         }
     }
 
+    public void dispatch(MotionEvent event) {
+        // Not used by PhoneWindowManager.  See code in {@link NavigationBarView}
+    }
+
     @Override
     public IBinder asBinder() {
         return mService.asBinder();