Add initial support for TAB navigation.

Bug: 3286652
Change-Id: I813a0318b3b8d9c9bc791ea6a2427be11c08de00
diff --git a/core/java/android/view/FocusFinder.java b/core/java/android/view/FocusFinder.java
index 8ad9a62..d9bf918 100644
--- a/core/java/android/view/FocusFinder.java
+++ b/core/java/android/view/FocusFinder.java
@@ -19,6 +19,8 @@
 import android.graphics.Rect;
 
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
 
 /**
  * The algorithm used for finding the next focusable view in a given direction
@@ -44,6 +46,7 @@
     Rect mFocusedRect = new Rect();
     Rect mOtherRect = new Rect();
     Rect mBestCandidateRect = new Rect();
+    SequentialFocusComparator mSequentialFocusComparator = new SequentialFocusComparator();
 
     // enforce thread local access
     private FocusFinder() {}
@@ -76,6 +79,7 @@
             switch (direction) {
                 case View.FOCUS_RIGHT:
                 case View.FOCUS_DOWN:
+                case View.FOCUS_FORWARD:
                     final int rootTop = root.getScrollY();
                     final int rootLeft = root.getScrollX();
                     mFocusedRect.set(rootLeft, rootTop, rootLeft, rootTop);
@@ -83,6 +87,7 @@
 
                 case View.FOCUS_LEFT:
                 case View.FOCUS_UP:
+                case View.FOCUS_BACKWARD:
                     final int rootBottom = root.getScrollY() + root.getHeight();
                     final int rootRight = root.getScrollX() + root.getWidth();
                     mFocusedRect.set(rootRight, rootBottom,
@@ -107,6 +112,48 @@
 
     private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) {
         ArrayList<View> focusables = root.getFocusables(direction);
+        if (focusables.isEmpty()) {
+            // The focus cannot change.
+            return null;
+        }
+
+        if (direction == View.FOCUS_FORWARD || direction == View.FOCUS_BACKWARD) {
+            if (focused != null && !focusables.contains(focused)) {
+                // Add the currently focused view to the list to have it sorted
+                // along with the other views.
+                focusables.add(focused);
+            }
+
+            try {
+                // Note: This sort is stable.
+                mSequentialFocusComparator.setRoot(root);
+                Collections.sort(focusables, mSequentialFocusComparator);
+            } finally {
+                mSequentialFocusComparator.recycle();
+            }
+
+            final int count = focusables.size();
+            switch (direction) {
+                case View.FOCUS_FORWARD:
+                    if (focused != null) {
+                        int position = focusables.lastIndexOf(focused);
+                        if (position >= 0 && position + 1 < count) {
+                            return focusables.get(position + 1);
+                        }
+                    }
+                    return focusables.get(0);
+
+                case View.FOCUS_BACKWARD:
+                    if (focused != null) {
+                        int position = focusables.indexOf(focused);
+                        if (position > 0) {
+                            return focusables.get(position - 1);
+                        }
+                    }
+                    return focusables.get(count - 1);
+            }
+            return null;
+        }
 
         // initialize the best candidate to something impossible
         // (so the first plausible view will become the best choice)
@@ -477,4 +524,59 @@
         throw new IllegalArgumentException("direction must be one of "
                 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
     }
+
+    /**
+     * Sorts views according to their visual layout and geometry for default tab order.
+     * This is used for sequential focus traversal.
+     */
+    private static final class SequentialFocusComparator implements Comparator<View> {
+        private final Rect mFirstRect = new Rect();
+        private final Rect mSecondRect = new Rect();
+        private ViewGroup mRoot;
+
+        public void recycle() {
+            mRoot = null;
+        }
+
+        public void setRoot(ViewGroup root) {
+            mRoot = root;
+        }
+
+        public int compare(View first, View second) {
+            if (first == second) {
+                return 0;
+            }
+
+            getRect(first, mFirstRect);
+            getRect(second, mSecondRect);
+
+            if (mFirstRect.top < mSecondRect.top) {
+                return -1;
+            } else if (mFirstRect.top > mSecondRect.top) {
+                return 1;
+            } else if (mFirstRect.left < mSecondRect.left) {
+                return -1;
+            } else if (mFirstRect.left > mSecondRect.left) {
+                return 1;
+            } else if (mFirstRect.bottom < mSecondRect.bottom) {
+                return -1;
+            } else if (mFirstRect.bottom > mSecondRect.bottom) {
+                return 1;
+            } else if (mFirstRect.right < mSecondRect.right) {
+                return -1;
+            } else if (mFirstRect.right > mSecondRect.right) {
+                return 1;
+            } else {
+                // The view are distinct but completely coincident so we consider
+                // them equal for our purposes.  Since the sort is stable, this
+                // means that the views will retain their layout order relative to one another.
+                return 0;
+            }
+        }
+
+        private void getRect(View view, Rect rect) {
+            view.getDrawingRect(rect);
+            mRoot.offsetDescendantRectToMyCoords(view, rect);
+        }
+    }
 }
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 75aebc9..71d07c6 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -64,6 +64,7 @@
 import android.view.inputmethod.InputMethodManager;
 import android.widget.ScrollBarDrawable;
 import com.android.internal.R;
+import com.android.internal.util.Predicate;
 import com.android.internal.view.menu.MenuBuilder;
 
 import java.lang.ref.WeakReference;
@@ -2066,6 +2067,12 @@
      */
     private int mNextFocusDownId = View.NO_ID;
 
+    /**
+     * When this view has focus and the next focus is {@link #FOCUS_FORWARD},
+     * the user may specify which view to go to next.
+     */
+    int mNextFocusForwardId = View.NO_ID;
+
     private CheckForLongPress mPendingCheckForLongPress;
     private CheckForTap mPendingCheckForTap = null;
     private PerformClick mPerformClick;
@@ -2424,6 +2431,9 @@
                 case R.styleable.View_nextFocusDown:
                     mNextFocusDownId = a.getResourceId(attr, View.NO_ID);
                     break;
+                case R.styleable.View_nextFocusForward:
+                    mNextFocusForwardId = a.getResourceId(attr, View.NO_ID);
+                    break;
                 case R.styleable.View_minWidth:
                     mMinWidth = a.getDimensionPixelSize(attr, 0);
                     break;
@@ -3112,9 +3122,9 @@
      * @param gainFocus True if the View has focus; false otherwise.
      * @param direction The direction focus has moved when requestFocus()
      *                  is called to give this view focus. Values are
-     *                  {@link #FOCUS_UP}, {@link #FOCUS_DOWN}, {@link #FOCUS_LEFT} or
-     *                  {@link #FOCUS_RIGHT}. It may not always apply, in which
-     *                  case use the default.
+     *                  {@link #FOCUS_UP}, {@link #FOCUS_DOWN}, {@link #FOCUS_LEFT},
+     *                  {@link #FOCUS_RIGHT}, {@link #FOCUS_FORWARD}, or {@link #FOCUS_BACKWARD}.
+     *                  It may not always apply, in which case use the default.
      * @param previouslyFocusedRect The rectangle, in this view's coordinate
      *        system, of the previously focused view.  If applicable, this will be
      *        passed in as finer grained information about where the focus is coming
@@ -3359,7 +3369,8 @@
     }
 
     /**
-     * @return The user specified next focus ID.
+     * Gets the id of the view to use when the next focus is {@link #FOCUS_LEFT}.
+     * @return The next focus ID, or {@link #NO_ID} if the framework should decide automatically.
      *
      * @attr ref android.R.styleable#View_nextFocusLeft
      */
@@ -3368,9 +3379,9 @@
     }
 
     /**
-     * Set the id of the view to use for the next focus
-     *
-     * @param nextFocusLeftId
+     * Sets the id of the view to use when the next focus is {@link #FOCUS_LEFT}.
+     * @param nextFocusLeftId The next focus ID, or {@link #NO_ID} if the framework should
+     * decide automatically.
      *
      * @attr ref android.R.styleable#View_nextFocusLeft
      */
@@ -3379,7 +3390,8 @@
     }
 
     /**
-     * @return The user specified next focus ID.
+     * Gets the id of the view to use when the next focus is {@link #FOCUS_RIGHT}.
+     * @return The next focus ID, or {@link #NO_ID} if the framework should decide automatically.
      *
      * @attr ref android.R.styleable#View_nextFocusRight
      */
@@ -3388,9 +3400,9 @@
     }
 
     /**
-     * Set the id of the view to use for the next focus
-     *
-     * @param nextFocusRightId
+     * Sets the id of the view to use when the next focus is {@link #FOCUS_RIGHT}.
+     * @param nextFocusRightId The next focus ID, or {@link #NO_ID} if the framework should
+     * decide automatically.
      *
      * @attr ref android.R.styleable#View_nextFocusRight
      */
@@ -3399,7 +3411,8 @@
     }
 
     /**
-     * @return The user specified next focus ID.
+     * Gets the id of the view to use when the next focus is {@link #FOCUS_UP}.
+     * @return The next focus ID, or {@link #NO_ID} if the framework should decide automatically.
      *
      * @attr ref android.R.styleable#View_nextFocusUp
      */
@@ -3408,9 +3421,9 @@
     }
 
     /**
-     * Set the id of the view to use for the next focus
-     *
-     * @param nextFocusUpId
+     * Sets the id of the view to use when the next focus is {@link #FOCUS_UP}.
+     * @param nextFocusUpId The next focus ID, or {@link #NO_ID} if the framework should
+     * decide automatically.
      *
      * @attr ref android.R.styleable#View_nextFocusUp
      */
@@ -3419,7 +3432,8 @@
     }
 
     /**
-     * @return The user specified next focus ID.
+     * Gets the id of the view to use when the next focus is {@link #FOCUS_DOWN}.
+     * @return The next focus ID, or {@link #NO_ID} if the framework should decide automatically.
      *
      * @attr ref android.R.styleable#View_nextFocusDown
      */
@@ -3428,9 +3442,9 @@
     }
 
     /**
-     * Set the id of the view to use for the next focus
-     *
-     * @param nextFocusDownId
+     * Sets the id of the view to use when the next focus is {@link #FOCUS_DOWN}.
+     * @param nextFocusDownId The next focus ID, or {@link #NO_ID} if the framework should
+     * decide automatically.
      *
      * @attr ref android.R.styleable#View_nextFocusDown
      */
@@ -3439,6 +3453,27 @@
     }
 
     /**
+     * Gets the id of the view to use when the next focus is {@link #FOCUS_FORWARD}.
+     * @return The next focus ID, or {@link #NO_ID} if the framework should decide automatically.
+     *
+     * @attr ref android.R.styleable#View_nextFocusForward
+     */
+    public int getNextFocusForwardId() {
+        return mNextFocusForwardId;
+    }
+
+    /**
+     * Sets the id of the view to use when the next focus is {@link #FOCUS_FORWARD}.
+     * @param nextFocusForwardId The next focus ID, or {@link #NO_ID} if the framework should
+     * decide automatically.
+     *
+     * @attr ref android.R.styleable#View_nextFocusForward
+     */
+    public void setNextFocusForwardId(int nextFocusForwardId) {
+        mNextFocusForwardId = nextFocusForwardId;
+    }
+
+    /**
      * Returns the visibility of this view and all of its ancestors
      *
      * @return True if this view and all of its ancestors are {@link #VISIBLE}
@@ -3949,10 +3984,10 @@
 
     /**
      * If a user manually specified the next view id for a particular direction,
-     * use the root to look up the view.  Once a view is found, it is cached
-     * for future lookups.
+     * use the root to look up the view.
      * @param root The root view of the hierarchy containing this view.
-     * @param direction One of FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, and FOCUS_RIGHT
+     * @param direction One of FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT, FOCUS_FORWARD,
+     * or FOCUS_BACKWARD.
      * @return The user specified next view, or null if there is none.
      */
     View findUserSetNextFocus(View root, int direction) {
@@ -3969,6 +4004,18 @@
             case FOCUS_DOWN:
                 if (mNextFocusDownId == View.NO_ID) return null;
                 return findViewShouldExist(root, mNextFocusDownId);
+            case FOCUS_FORWARD:
+                if (mNextFocusForwardId == View.NO_ID) return null;
+                return findViewShouldExist(root, mNextFocusForwardId);
+            case FOCUS_BACKWARD: {
+                final int id = mID;
+                return root.findViewByPredicate(new Predicate<View>() {
+                    @Override
+                    public boolean apply(View t) {
+                        return t.mNextFocusForwardId == id;
+                    }
+                });
+            }
         }
         return null;
     }
@@ -9359,6 +9406,18 @@
     }
 
     /**
+     * {@hide}
+     * @param predicate The predicate to evaluate.
+     * @return The first view that matches the predicate or null.
+     */
+    protected View findViewByPredicateTraversal(Predicate<View> predicate) {
+        if (predicate.apply(this)) {
+            return this;
+        }
+        return null;
+    }
+
+    /**
      * Look for a child view with the given id.  If this view has the given
      * id, return this view.
      *
@@ -9387,6 +9446,18 @@
     }
 
     /**
+     * {@hide}
+     * Look for a child view that matches the specified predicate.
+     * If this view matches the predicate, return this view.
+     *
+     * @param predicate The predicate to evaluate.
+     * @return The first view that matches the predicate or null.
+     */
+    public final View findViewByPredicate(Predicate<View> predicate) {
+        return findViewByPredicateTraversal(predicate);
+    }
+
+    /**
      * Sets the identifier for this view. The identifier does not have to be
      * unique in this view's hierarchy. The identifier should be a positive
      * number.
diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java
index a9aa8f9..7b3cc6c 100644
--- a/core/java/android/view/ViewGroup.java
+++ b/core/java/android/view/ViewGroup.java
@@ -18,6 +18,7 @@
 
 import android.animation.LayoutTransition;
 import com.android.internal.R;
+import com.android.internal.util.Predicate;
 
 import android.content.Context;
 import android.content.res.Configuration;
@@ -2516,6 +2517,33 @@
     }
 
     /**
+     * {@hide}
+     */
+    @Override
+    protected View findViewByPredicateTraversal(Predicate<View> predicate) {
+        if (predicate.apply(this)) {
+            return this;
+        }
+
+        final View[] where = mChildren;
+        final int len = mChildrenCount;
+
+        for (int i = 0; i < len; i++) {
+            View v = where[i];
+
+            if ((v.mPrivateFlags & IS_ROOT_NAMESPACE) == 0) {
+                v = v.findViewByPredicate(predicate);
+
+                if (v != null) {
+                    return v;
+                }
+            }
+        }
+
+        return null;
+    }
+
+    /**
      * Adds a child view. If no layout parameters are already set on the child, the
      * default parameters for this ViewGroup are set on the child.
      *
diff --git a/core/java/android/view/ViewRoot.java b/core/java/android/view/ViewRoot.java
index 95949b9..281dd27 100644
--- a/core/java/android/view/ViewRoot.java
+++ b/core/java/android/view/ViewRoot.java
@@ -2415,81 +2415,78 @@
     }
 
     /**
-     * @param keyCode The key code
-     * @return True if the key is directional.
+     * Returns true if the key is used for keyboard navigation.
+     * @param keyEvent The key event.
+     * @return True if the key is used for keyboard navigation.
      */
-    static boolean isDirectional(int keyCode) {
-        switch (keyCode) {
+    private static boolean isNavigationKey(KeyEvent keyEvent) {
+        switch (keyEvent.getKeyCode()) {
         case KeyEvent.KEYCODE_DPAD_LEFT:
         case KeyEvent.KEYCODE_DPAD_RIGHT:
         case KeyEvent.KEYCODE_DPAD_UP:
         case KeyEvent.KEYCODE_DPAD_DOWN:
+        case KeyEvent.KEYCODE_DPAD_CENTER:
+        case KeyEvent.KEYCODE_PAGE_UP:
+        case KeyEvent.KEYCODE_PAGE_DOWN:
+        case KeyEvent.KEYCODE_MOVE_HOME:
+        case KeyEvent.KEYCODE_MOVE_END:
+        case KeyEvent.KEYCODE_TAB:
+        case KeyEvent.KEYCODE_SPACE:
+        case KeyEvent.KEYCODE_ENTER:
             return true;
         }
         return false;
     }
 
     /**
-     * Returns true if this key is a keyboard key.
+     * Returns true if the key is used for typing.
      * @param keyEvent The key event.
-     * @return whether this key is a keyboard key.
+     * @return True if the key is used for typing.
      */
-    private static boolean isKeyboardKey(KeyEvent keyEvent) {
-      final int convertedKey = keyEvent.getUnicodeChar();
-        return convertedKey > 0;
+    private static boolean isTypingKey(KeyEvent keyEvent) {
+        return keyEvent.getUnicodeChar() > 0;
     }
 
-
-
     /**
-     * See if the key event means we should leave touch mode (and leave touch
-     * mode if so).
+     * See if the key event means we should leave touch mode (and leave touch mode if so).
      * @param event The key event.
      * @return Whether this key event should be consumed (meaning the act of
      *   leaving touch mode alone is considered the event).
      */
     private boolean checkForLeavingTouchModeAndConsume(KeyEvent event) {
-        final int action = event.getAction();
-        if (action != KeyEvent.ACTION_DOWN && action != KeyEvent.ACTION_MULTIPLE) {
-            return false;
-        }
-        if ((event.getFlags()&KeyEvent.FLAG_KEEP_TOUCH_MODE) != 0) {
-            return false;
-        }
-
-        // only relevant if we are in touch mode
+        // Only relevant in touch mode.
         if (!mAttachInfo.mInTouchMode) {
             return false;
         }
 
-        // if something like an edit text has focus and the user is typing,
-        // leave touch mode
-        //
-        // note: the condition of not being a keyboard key is kind of a hacky
-        // approximation of whether we think the focused view will want the
-        // key; if we knew for sure whether the focused view would consume
-        // the event, that would be better.
-        if (isKeyboardKey(event) && mView != null && mView.hasFocus()) {
-            mFocusedView = mView.findFocus();
-            if ((mFocusedView instanceof ViewGroup)
-                    && ((ViewGroup) mFocusedView).getDescendantFocusability() ==
-                    ViewGroup.FOCUS_AFTER_DESCENDANTS) {
-                // something has focus, but is holding it weakly as a container
-                return false;
-            }
-            if (ensureTouchMode(false)) {
-                throw new IllegalStateException("should not have changed focus "
-                        + "when leaving touch mode while a view has focus.");
-            }
+        // Only consider leaving touch mode on DOWN or MULTIPLE actions, never on UP.
+        final int action = event.getAction();
+        if (action != KeyEvent.ACTION_DOWN && action != KeyEvent.ACTION_MULTIPLE) {
             return false;
         }
 
-        if (isDirectional(event.getKeyCode())) {
-            // no view has focus, so we leave touch mode (and find something
-            // to give focus to).  the event is consumed if we were able to
-            // find something to give focus to.
+        // Don't leave touch mode if the IME told us not to.
+        if ((event.getFlags() & KeyEvent.FLAG_KEEP_TOUCH_MODE) != 0) {
+            return false;
+        }
+
+        // If the key can be used for keyboard navigation then leave touch mode
+        // and select a focused view if needed (in ensureTouchMode).
+        // When a new focused view is selected, we consume the navigation key because
+        // navigation doesn't make much sense unless a view already has focus so
+        // the key's purpose is to set focus.
+        if (isNavigationKey(event)) {
             return ensureTouchMode(false);
         }
+
+        // If the key can be used for typing then leave touch mode
+        // and select a focused view if needed (in ensureTouchMode).
+        // Always allow the view to process the typing key.
+        if (isTypingKey(event)) {
+            ensureTouchMode(false);
+            return false;
+        }
+
         return false;
     }
 
@@ -2640,16 +2637,31 @@
             int direction = 0;
             switch (event.getKeyCode()) {
             case KeyEvent.KEYCODE_DPAD_LEFT:
-                direction = View.FOCUS_LEFT;
+                if (event.hasNoModifiers()) {
+                    direction = View.FOCUS_LEFT;
+                }
                 break;
             case KeyEvent.KEYCODE_DPAD_RIGHT:
-                direction = View.FOCUS_RIGHT;
+                if (event.hasNoModifiers()) {
+                    direction = View.FOCUS_RIGHT;
+                }
                 break;
             case KeyEvent.KEYCODE_DPAD_UP:
-                direction = View.FOCUS_UP;
+                if (event.hasNoModifiers()) {
+                    direction = View.FOCUS_UP;
+                }
                 break;
             case KeyEvent.KEYCODE_DPAD_DOWN:
-                direction = View.FOCUS_DOWN;
+                if (event.hasNoModifiers()) {
+                    direction = View.FOCUS_DOWN;
+                }
+                break;
+            case KeyEvent.KEYCODE_TAB:
+                if (event.hasNoModifiers()) {
+                    direction = View.FOCUS_FORWARD;
+                } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
+                    direction = View.FOCUS_BACKWARD;
+                }
                 break;
             }
 
diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java
index 0deb0a0..d31acec 100644
--- a/core/java/android/widget/AbsListView.java
+++ b/core/java/android/widget/AbsListView.java
@@ -4382,6 +4382,18 @@
         requestLayout();
         invalidate();
     }
+    
+    /**
+     * If there is a selection returns true.
+     * Otherwise resurrects the selection and returns false.
+     */
+    boolean ensureSelectionOnMovementKey() {
+        if (mSelectedPosition < 0) {
+            resurrectSelection();
+            return false;
+        }
+        return true;
+    }
 
     /**
      * Makes the item at the supplied position selected.
@@ -4718,9 +4730,17 @@
             dX = dest.left + dest.width() / 2;
             dY = dest.bottom;
             break;
+        case View.FOCUS_FORWARD:
+        case View.FOCUS_BACKWARD:
+            sX = source.right + source.width() / 2;
+            sY = source.top + source.height() / 2;
+            dX = dest.left + dest.width() / 2;
+            dY = dest.top + dest.height() / 2;
+            break;
         default:
             throw new IllegalArgumentException("direction must be one of "
-                    + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
+                    + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT, "
+                    + "FOCUS_FORWARD, FOCUS_BACKWARD}.");
         }
         int deltaX = dX - sX;
         int deltaY = dY - sY;
diff --git a/core/java/android/widget/AutoCompleteTextView.java b/core/java/android/widget/AutoCompleteTextView.java
index e07befa..ee037cd 100644
--- a/core/java/android/widget/AutoCompleteTextView.java
+++ b/core/java/android/widget/AutoCompleteTextView.java
@@ -630,7 +630,10 @@
             // from the drop down as its content
             case KeyEvent.KEYCODE_ENTER:
             case KeyEvent.KEYCODE_DPAD_CENTER:
-                performCompletion();
+            case KeyEvent.KEYCODE_TAB:
+                if (event.hasNoModifiers()) {
+                    performCompletion();
+                }
                 return true;
             }
         }
@@ -646,7 +649,9 @@
         if (!isPopupShowing()) {
             switch(keyCode) {
             case KeyEvent.KEYCODE_DPAD_DOWN:
-                performValidation();
+                if (event.hasNoModifiers()) {
+                    performValidation();
+                }
             }
         }
 
diff --git a/core/java/android/widget/GridView.java b/core/java/android/widget/GridView.java
index df01da7..c7addcc 100644
--- a/core/java/android/widget/GridView.java
+++ b/core/java/android/widget/GridView.java
@@ -1483,61 +1483,95 @@
         int action = event.getAction();
 
         if (action != KeyEvent.ACTION_UP) {
-            if (mSelectedPosition < 0) {
-                switch (keyCode) {
-                    case KeyEvent.KEYCODE_DPAD_UP:
-                    case KeyEvent.KEYCODE_DPAD_DOWN:
-                    case KeyEvent.KEYCODE_DPAD_LEFT:
-                    case KeyEvent.KEYCODE_DPAD_RIGHT:
-                    case KeyEvent.KEYCODE_DPAD_CENTER:
-                    case KeyEvent.KEYCODE_SPACE:
-                    case KeyEvent.KEYCODE_ENTER:
-                        resurrectSelection();
-                        return true;
-                }
-            }
-
             switch (keyCode) {
                 case KeyEvent.KEYCODE_DPAD_LEFT:
-                    handled = arrowScroll(FOCUS_LEFT);
+                    if (event.hasNoModifiers()) {
+                        handled = ensureSelectionOnMovementKey() && arrowScroll(FOCUS_LEFT);
+                    }
                     break;
 
                 case KeyEvent.KEYCODE_DPAD_RIGHT:
-                    handled = arrowScroll(FOCUS_RIGHT);
+                    if (event.hasNoModifiers()) {
+                        handled = ensureSelectionOnMovementKey() && arrowScroll(FOCUS_RIGHT);
+                    }
                     break;
 
                 case KeyEvent.KEYCODE_DPAD_UP:
-                    if (!event.isAltPressed()) {
-                        handled = arrowScroll(FOCUS_UP);
-
-                    } else {
-                        handled = fullScroll(FOCUS_UP);
+                    if (event.hasNoModifiers()) {
+                        handled = ensureSelectionOnMovementKey() && arrowScroll(FOCUS_UP);
+                    } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) {
+                        handled = ensureSelectionOnMovementKey() && fullScroll(FOCUS_UP);
                     }
                     break;
 
                 case KeyEvent.KEYCODE_DPAD_DOWN:
-                    if (!event.isAltPressed()) {
-                        handled = arrowScroll(FOCUS_DOWN);
-                    } else {
-                        handled = fullScroll(FOCUS_DOWN);
+                    if (event.hasNoModifiers()) {
+                        handled = ensureSelectionOnMovementKey() && arrowScroll(FOCUS_DOWN);
+                    } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) {
+                        handled = ensureSelectionOnMovementKey() && fullScroll(FOCUS_DOWN);
                     }
                     break;
 
                 case KeyEvent.KEYCODE_DPAD_CENTER:
                 case KeyEvent.KEYCODE_ENTER: {
-                    if (getChildCount() > 0 && event.getRepeatCount() == 0) {
+                    if (event.hasNoModifiers()
+                            && event.getRepeatCount() == 0 && getChildCount() > 0) {
+                        ensureSelectionOnMovementKey();
                         keyPressed();
                     }
-
                     return true;
                 }
 
                 case KeyEvent.KEYCODE_SPACE:
                     if (mPopup == null || !mPopup.isShowing()) {
-                        if (!event.isShiftPressed()) {
-                            handled = pageScroll(FOCUS_DOWN);
-                        } else {
-                            handled = pageScroll(FOCUS_UP);
+                        if (event.hasNoModifiers()) {
+                            handled = ensureSelectionOnMovementKey() && pageScroll(FOCUS_DOWN);
+                        } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
+                            handled = ensureSelectionOnMovementKey() && pageScroll(FOCUS_UP);
+                        }
+                    }
+                    break;
+
+                case KeyEvent.KEYCODE_PAGE_UP:
+                    if (event.hasNoModifiers()) {
+                        handled = ensureSelectionOnMovementKey() && pageScroll(FOCUS_UP);
+                    } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) {
+                        handled = ensureSelectionOnMovementKey() && fullScroll(FOCUS_UP);
+                    }
+                    break;
+
+                case KeyEvent.KEYCODE_PAGE_DOWN:
+                    if (event.hasNoModifiers()) {
+                        handled = ensureSelectionOnMovementKey() && pageScroll(FOCUS_DOWN);
+                    } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) {
+                        handled = ensureSelectionOnMovementKey() && fullScroll(FOCUS_DOWN);
+                    }
+                    break;
+
+                case KeyEvent.KEYCODE_MOVE_HOME:
+                    if (event.hasNoModifiers()) {
+                        handled = ensureSelectionOnMovementKey() && fullScroll(FOCUS_UP);
+                    }
+                    break;
+
+                case KeyEvent.KEYCODE_MOVE_END:
+                    if (event.hasNoModifiers()) {
+                        handled = ensureSelectionOnMovementKey() && fullScroll(FOCUS_DOWN);
+                    }
+                    break;
+
+                case KeyEvent.KEYCODE_TAB:
+                    // XXX Sometimes it is useful to be able to TAB through the items in
+                    //     a GridView sequentially.  Unfortunately this can create an
+                    //     asymmetry in TAB navigation order unless the list selection
+                    //     always reverts to the top or bottom when receiving TAB focus from
+                    //     another widget.  Leaving this behavior disabled for now but
+                    //     perhaps it should be configurable (and more comprehensive).
+                    if (false) {
+                        if (event.hasNoModifiers()) {
+                            handled = ensureSelectionOnMovementKey() && sequenceScroll(FOCUS_FORWARD);
+                        } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
+                            handled = ensureSelectionOnMovementKey() && sequenceScroll(FOCUS_BACKWARD);
                         }
                     }
                     break;
@@ -1686,6 +1720,64 @@
         return moved;
     }
 
+    /**
+     * Goes to the next or previous item according to the order set by the
+     * adapter.
+     */
+    boolean sequenceScroll(int direction) {
+        int selectedPosition = mSelectedPosition;
+        int numColumns = mNumColumns;
+        int count = mItemCount;
+
+        int startOfRow;
+        int endOfRow;
+        if (!mStackFromBottom) {
+            startOfRow = (selectedPosition / numColumns) * numColumns;
+            endOfRow = Math.min(startOfRow + numColumns - 1, count - 1);
+        } else {
+            int invertedSelection = count - 1 - selectedPosition;
+            endOfRow = count - 1 - (invertedSelection / numColumns) * numColumns;
+            startOfRow = Math.max(0, endOfRow - numColumns + 1);
+        }
+
+        boolean moved = false;
+        boolean showScroll = false;
+        switch (direction) {
+            case FOCUS_FORWARD:
+                if (selectedPosition < count - 1) {
+                    // Move to the next item.
+                    mLayoutMode = LAYOUT_MOVE_SELECTION;
+                    setSelectionInt(selectedPosition + 1);
+                    moved = true;
+                    // Show the scrollbar only if changing rows.
+                    showScroll = selectedPosition == endOfRow;
+                }
+                break;
+
+            case FOCUS_BACKWARD:
+                if (selectedPosition > 0) {
+                    // Move to the previous item.
+                    mLayoutMode = LAYOUT_MOVE_SELECTION;
+                    setSelectionInt(selectedPosition - 1);
+                    moved = true;
+                    // Show the scrollbar only if changing rows.
+                    showScroll = selectedPosition == startOfRow;
+                }
+                break;
+        }
+
+        if (moved) {
+            playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction));
+            invokeOnItemScrollListener();
+        }
+
+        if (showScroll) {
+            awakenScrollBars();
+        }
+
+        return moved;
+    }
+
     @Override
     protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
         super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
@@ -1729,7 +1821,7 @@
      * change is coming from?
      * @param childIndex The index to check.
      * @param direction The direction, one of
-     *        {FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}
+     *        {FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT, FOCUS_FORWARD, FOCUS_BACKWARD}
      * @return Whether childIndex is a candidate.
      */
     private boolean isCandidateSelection(int childIndex, int direction) {
@@ -1761,9 +1853,16 @@
             case View.FOCUS_UP:
                 // coming from bottom, need to be in last row
                 return rowEnd == count - 1;
+            case View.FOCUS_FORWARD:
+                // coming from top-left, need to be first in top row
+                return childIndex == rowStart && rowStart == 0;
+            case View.FOCUS_BACKWARD:
+                // coming from bottom-right, need to be last in bottom row
+                return childIndex == rowEnd && rowEnd == count - 1;
             default:
                 throw new IllegalArgumentException("direction must be one of "
-                        + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
+                        + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT, "
+                        + "FOCUS_FORWARD, FOCUS_BACKWARD}.");
         }
     }
 
diff --git a/core/java/android/widget/ListView.java b/core/java/android/widget/ListView.java
index 6894c99..1f95159 100644
--- a/core/java/android/widget/ListView.java
+++ b/core/java/android/widget/ListView.java
@@ -17,6 +17,7 @@
 package android.widget;
 
 import com.android.internal.R;
+import com.android.internal.util.Predicate;
 import com.google.android.collect.Lists;
 
 import android.content.Context;
@@ -2077,51 +2078,47 @@
         int action = event.getAction();
 
         if (action != KeyEvent.ACTION_UP) {
-            if (mSelectedPosition < 0) {
-                switch (keyCode) {
-                case KeyEvent.KEYCODE_DPAD_UP:
-                case KeyEvent.KEYCODE_DPAD_DOWN:
-                case KeyEvent.KEYCODE_DPAD_CENTER:
-                case KeyEvent.KEYCODE_ENTER:
-                case KeyEvent.KEYCODE_SPACE:
-                    if (resurrectSelection()) {
-                        return true;
-                    }
-                }
-            }
             switch (keyCode) {
             case KeyEvent.KEYCODE_DPAD_UP:
-                if (!event.isAltPressed()) {
-                    while (count > 0) {
-                        handled = arrowScroll(FOCUS_UP);
-                        count--;
+                if (event.hasNoModifiers()) {
+                    if (ensureSelectionOnMovementKey()) {
+                        while (count-- > 0) {
+                            handled |= arrowScroll(FOCUS_UP);
+                        }
                     }
-                } else {
-                    handled = fullScroll(FOCUS_UP);
+                } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) {
+                    handled = ensureSelectionOnMovementKey() && fullScroll(FOCUS_UP);
                 }
                 break;
 
             case KeyEvent.KEYCODE_DPAD_DOWN:
-                if (!event.isAltPressed()) {
-                    while (count > 0) {
-                        handled = arrowScroll(FOCUS_DOWN);
-                        count--;
+                if (event.hasNoModifiers()) {
+                    if (ensureSelectionOnMovementKey()) {
+                        while (count-- > 0) {
+                            handled |= arrowScroll(FOCUS_DOWN);
+                        }
                     }
-                } else {
-                    handled = fullScroll(FOCUS_DOWN);
+                } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) {
+                    handled = ensureSelectionOnMovementKey() && fullScroll(FOCUS_DOWN);
                 }
                 break;
 
             case KeyEvent.KEYCODE_DPAD_LEFT:
-                handled = handleHorizontalFocusWithinListItem(View.FOCUS_LEFT);
+                if (event.hasNoModifiers()) {
+                    handled = handleHorizontalFocusWithinListItem(View.FOCUS_LEFT);
+                }
                 break;
+
             case KeyEvent.KEYCODE_DPAD_RIGHT:
-                handled = handleHorizontalFocusWithinListItem(View.FOCUS_RIGHT);
+                if (event.hasNoModifiers()) {
+                    handled = handleHorizontalFocusWithinListItem(View.FOCUS_RIGHT);
+                }
                 break;
 
             case KeyEvent.KEYCODE_DPAD_CENTER:
             case KeyEvent.KEYCODE_ENTER:
                 if (mItemCount > 0 && event.getRepeatCount() == 0) {
+                    ensureSelectionOnMovementKey();
                     keyPressed();
                 }
                 handled = true;
@@ -2129,14 +2126,58 @@
 
             case KeyEvent.KEYCODE_SPACE:
                 if (mPopup == null || !mPopup.isShowing()) {
-                    if (!event.isShiftPressed()) {
-                        pageScroll(FOCUS_DOWN);
-                    } else {
-                        pageScroll(FOCUS_UP);
+                    if (event.hasNoModifiers()) {
+                        handled = ensureSelectionOnMovementKey() && pageScroll(FOCUS_DOWN);
+                    } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
+                        handled = ensureSelectionOnMovementKey() && pageScroll(FOCUS_UP);
                     }
                     handled = true;
                 }
                 break;
+
+            case KeyEvent.KEYCODE_PAGE_UP:
+                if (event.hasNoModifiers()) {
+                    handled = ensureSelectionOnMovementKey() && pageScroll(FOCUS_UP);
+                } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) {
+                    handled = ensureSelectionOnMovementKey() && fullScroll(FOCUS_UP);
+                }
+                break;
+
+            case KeyEvent.KEYCODE_PAGE_DOWN:
+                if (event.hasNoModifiers()) {
+                    handled = ensureSelectionOnMovementKey() && pageScroll(FOCUS_DOWN);
+                } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) {
+                    handled = ensureSelectionOnMovementKey() && fullScroll(FOCUS_DOWN);
+                }
+                break;
+
+            case KeyEvent.KEYCODE_MOVE_HOME:
+                if (event.hasNoModifiers()) {
+                    handled = ensureSelectionOnMovementKey() && fullScroll(FOCUS_UP);
+                }
+                break;
+
+            case KeyEvent.KEYCODE_MOVE_END:
+                if (event.hasNoModifiers()) {
+                    handled = ensureSelectionOnMovementKey() && fullScroll(FOCUS_DOWN);
+                }
+                break;
+
+            case KeyEvent.KEYCODE_TAB:
+                // XXX Sometimes it is useful to be able to TAB through the items in
+                //     a ListView sequentially.  Unfortunately this can create an
+                //     asymmetry in TAB navigation order unless the list selection
+                //     always reverts to the top or bottom when receiving TAB focus from
+                //     another widget.  Leaving this behavior disabled for now but
+                //     perhaps it should be configurable (and more comprehensive).
+                if (false) {
+                    if (event.hasNoModifiers()) {
+                        handled = ensureSelectionOnMovementKey() && arrowScroll(FOCUS_DOWN);
+                    } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
+                        handled = ensureSelectionOnMovementKey() && arrowScroll(FOCUS_UP);
+                    }
+                }
+                break;
             }
         }
 
@@ -3401,7 +3442,7 @@
     }
 
     /* (non-Javadoc)
-     * @see android.view.View#findViewWithTag(String)
+     * @see android.view.View#findViewWithTag(Object)
      * First look in our children, then in any header and footer views that may be scrolled off.
      */
     @Override
@@ -3409,12 +3450,12 @@
         View v;
         v = super.findViewWithTagTraversal(tag);
         if (v == null) {
-            v = findViewTagInHeadersOrFooters(mHeaderViewInfos, tag);
+            v = findViewWithTagInHeadersOrFooters(mHeaderViewInfos, tag);
             if (v != null) {
                 return v;
             }
 
-            v = findViewTagInHeadersOrFooters(mFooterViewInfos, tag);
+            v = findViewWithTagInHeadersOrFooters(mFooterViewInfos, tag);
             if (v != null) {
                 return v;
             }
@@ -3426,7 +3467,7 @@
      *
      * Look in the passed in list of headers or footers for the view with the tag.
      */
-    View findViewTagInHeadersOrFooters(ArrayList<FixedViewInfo> where, Object tag) {
+    View findViewWithTagInHeadersOrFooters(ArrayList<FixedViewInfo> where, Object tag) {
         if (where != null) {
             int len = where.size();
             View v;
@@ -3446,6 +3487,55 @@
         return null;
     }
 
+    /**
+     * @hide
+     * @see android.view.View#findViewByPredicate(Predicate)
+     * First look in our children, then in any header and footer views that may be scrolled off.
+     */
+    @Override
+    protected View findViewByPredicateTraversal(Predicate<View> predicate) {
+        View v;
+        v = super.findViewByPredicateTraversal(predicate);
+        if (v == null) {
+            v = findViewByPredicateInHeadersOrFooters(mHeaderViewInfos, predicate);
+            if (v != null) {
+                return v;
+            }
+
+            v = findViewByPredicateInHeadersOrFooters(mFooterViewInfos, predicate);
+            if (v != null) {
+                return v;
+            }
+        }
+        return v;
+    }
+
+    /* (non-Javadoc)
+     *
+     * Look in the passed in list of headers or footers for the first view that matches
+     * the predicate.
+     */
+    View findViewByPredicateInHeadersOrFooters(ArrayList<FixedViewInfo> where,
+            Predicate<View> predicate) {
+        if (where != null) {
+            int len = where.size();
+            View v;
+
+            for (int i = 0; i < len; i++) {
+                v = where.get(i).view;
+
+                if (!v.isRootNamespace()) {
+                    v = v.findViewByPredicate(predicate);
+
+                    if (v != null) {
+                        return v;
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
     @Override
     public boolean onTouchEvent(MotionEvent ev) {
         if (mItemsCanFocus && ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) {
diff --git a/core/java/android/widget/SearchView.java b/core/java/android/widget/SearchView.java
index dcd58b0..3bf5af3 100644
--- a/core/java/android/widget/SearchView.java
+++ b/core/java/android/widget/SearchView.java
@@ -623,14 +623,16 @@
 
             // If there is text in the query box, handle enter, and action keys
             // The search key is handled by the dialog's onKeyDown().
-            if (!mQueryTextView.isEmpty()) {
-                if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_UP) {
-                    v.cancelLongPress();
+            if (!mQueryTextView.isEmpty() && event.hasNoModifiers()) {
+                if (event.getAction() == KeyEvent.ACTION_UP) {
+                    if (keyCode == KeyEvent.KEYCODE_ENTER) {
+                        v.cancelLongPress();
 
-                    // Launch as a regular search.
-                    launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, mQueryTextView.getText()
-                            .toString());
-                    return true;
+                        // Launch as a regular search.
+                        launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, mQueryTextView.getText()
+                                .toString());
+                        return true;
+                    }
                 }
                 if (event.getAction() == KeyEvent.ACTION_DOWN) {
                     SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
@@ -658,11 +660,11 @@
         if (mSuggestionsAdapter == null) {
             return false;
         }
-        if (event.getAction() == KeyEvent.ACTION_DOWN) {
-
+        if (event.getAction() == KeyEvent.ACTION_DOWN && event.hasNoModifiers()) {
             // First, check for enter or search (both of which we'll treat as a
             // "click")
-            if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH) {
+            if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH
+                    || keyCode == KeyEvent.KEYCODE_TAB) {
                 int position = mQueryTextView.getListSelection();
                 return onItemClicked(position, KeyEvent.KEYCODE_UNKNOWN, null);
             }
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index d03b0ea..052760f 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -4586,9 +4586,8 @@
 
         if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
             int variation = mInputType & EditorInfo.TYPE_MASK_VARIATION;
-
-            if (variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS ||
-                variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_SUBJECT) {
+            if (variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
+                    || variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_SUBJECT) {
                 return true;
             }
         }
@@ -4596,6 +4595,23 @@
         return false;
     }
 
+    /**
+     * Returns true if pressing TAB in this field advances focus instead
+     * of inserting the character.  Insert tabs only in multi-line editors.
+     */
+    private boolean shouldAdvanceFocusOnTab() {
+        if (mInput != null && !mSingleLine) {
+            if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
+                int variation = mInputType & EditorInfo.TYPE_MASK_VARIATION;
+                if (variation == EditorInfo.TYPE_TEXT_FLAG_IME_MULTI_LINE
+                        || variation == EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
     private int doKeyDown(int keyCode, KeyEvent event, KeyEvent otherEvent) {
         if (!isEnabled()) {
             return 0;
@@ -4604,16 +4620,12 @@
         switch (keyCode) {
             case KeyEvent.KEYCODE_ENTER:
                 mEnterKeyIsDown = true;
-                // If ALT modifier is held, then we always insert a
-                // newline character.
-                if ((event.getMetaState()&KeyEvent.META_ALT_ON) == 0) {
-                    
+                if (event.hasNoModifiers()) {
                     // When mInputContentType is set, we know that we are
                     // running in a "modern" cupcake environment, so don't need
                     // to worry about the application trying to capture
                     // enter key events.
                     if (mInputContentType != null) {
-                        
                         // If there is an action listener, given them a
                         // chance to consume the event.
                         if (mInputContentType.onEditorActionListener != null &&
@@ -4624,11 +4636,11 @@
                             return -1;
                         }
                     }
-                    
+
                     // If our editor should move focus when enter is pressed, or
                     // this is a generated event from an IME action button, then
                     // don't let it be inserted into the text.
-                    if ((event.getFlags()&KeyEvent.FLAG_EDITOR_ACTION) != 0
+                    if ((event.getFlags() & KeyEvent.FLAG_EDITOR_ACTION) != 0
                             || shouldAdvanceFocusOnEnter()) {
                         return -1;
                     }
@@ -4637,8 +4649,18 @@
                 
             case KeyEvent.KEYCODE_DPAD_CENTER:
                 mDPadCenterIsDown = true;
-                if (shouldAdvanceFocusOnEnter()) {
-                    return 0;
+                if (event.hasNoModifiers()) {
+                    if (shouldAdvanceFocusOnEnter()) {
+                        return 0;
+                    }
+                }
+                break;
+
+            case KeyEvent.KEYCODE_TAB:
+                if (event.hasNoModifiers() || event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
+                    if (shouldAdvanceFocusOnTab()) {
+                        return 0;
+                    }
                 }
                 break;
 
@@ -4733,76 +4755,80 @@
         switch (keyCode) {
             case KeyEvent.KEYCODE_DPAD_CENTER:
                 mDPadCenterIsDown = false;
-                /*
-                 * If there is a click listener, just call through to
-                 * super, which will invoke it.
-                 *
-                 * If there isn't a click listener, try to show the soft
-                 * input method.  (It will also
-                 * call performClick(), but that won't do anything in
-                 * this case.)
-                 */
-                if (mOnClickListener == null) {
-                    if (mMovement != null && mText instanceof Editable
-                            && mLayout != null && onCheckIsTextEditor()) {
-                        InputMethodManager imm = (InputMethodManager)
-                                getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
-                        imm.showSoftInput(this, 0);
-                    }
-                }
-                return super.onKeyUp(keyCode, event);
-                
-            case KeyEvent.KEYCODE_ENTER:
-                mEnterKeyIsDown = false;
-                if (mInputContentType != null
-                        && mInputContentType.onEditorActionListener != null
-                        && mInputContentType.enterDown) {
-                    mInputContentType.enterDown = false;
-                    if (mInputContentType.onEditorActionListener.onEditorAction(
-                            this, EditorInfo.IME_NULL, event)) {
-                        return true;
-                    }
-                }
-                
-                if ((event.getFlags()&KeyEvent.FLAG_EDITOR_ACTION) != 0
-                        || shouldAdvanceFocusOnEnter()) {
+                if (event.hasNoModifiers()) {
                     /*
                      * If there is a click listener, just call through to
                      * super, which will invoke it.
                      *
-                     * If there isn't a click listener, try to advance focus,
-                     * but still call through to super, which will reset the
-                     * pressed state and longpress state.  (It will also
+                     * If there isn't a click listener, try to show the soft
+                     * input method.  (It will also
                      * call performClick(), but that won't do anything in
                      * this case.)
                      */
                     if (mOnClickListener == null) {
-                        View v = focusSearch(FOCUS_DOWN);
+                        if (mMovement != null && mText instanceof Editable
+                                && mLayout != null && onCheckIsTextEditor()) {
+                            InputMethodManager imm = (InputMethodManager)
+                                    getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
+                            imm.showSoftInput(this, 0);
+                        }
+                    }
+                }
+                return super.onKeyUp(keyCode, event);
 
-                        if (v != null) {
-                            if (!v.requestFocus(FOCUS_DOWN)) {
-                                throw new IllegalStateException("focus search returned a view " +
-                                        "that wasn't able to take focus!");
-                            }
-
-                            /*
-                             * Return true because we handled the key; super
-                             * will return false because there was no click
-                             * listener.
-                             */
-                            super.onKeyUp(keyCode, event);
+            case KeyEvent.KEYCODE_ENTER:
+                mEnterKeyIsDown = false;
+                if (event.hasNoModifiers()) {
+                    if (mInputContentType != null
+                            && mInputContentType.onEditorActionListener != null
+                            && mInputContentType.enterDown) {
+                        mInputContentType.enterDown = false;
+                        if (mInputContentType.onEditorActionListener.onEditorAction(
+                                this, EditorInfo.IME_NULL, event)) {
                             return true;
-                        } else if ((event.getFlags()
-                                & KeyEvent.FLAG_EDITOR_ACTION) != 0) {
-                            // No target for next focus, but make sure the IME
-                            // if this came from it.
-                            InputMethodManager imm = InputMethodManager.peekInstance();
-                            if (imm != null) {
-                                imm.hideSoftInputFromWindow(getWindowToken(), 0);
-                            }
                         }
                     }
 
+                    if ((event.getFlags() & KeyEvent.FLAG_EDITOR_ACTION) != 0
+                            || shouldAdvanceFocusOnEnter()) {
+                        /*
+                         * If there is a click listener, just call through to
+                         * super, which will invoke it.
+                         *
+                         * If there isn't a click listener, try to advance focus,
+                         * but still call through to super, which will reset the
+                         * pressed state and longpress state.  (It will also
+                         * call performClick(), but that won't do anything in
+                         * this case.)
+                         */
+                        if (mOnClickListener == null) {
+                            View v = focusSearch(FOCUS_DOWN);
+
+                            if (v != null) {
+                                if (!v.requestFocus(FOCUS_DOWN)) {
+                                    throw new IllegalStateException(
+                                            "focus search returned a view " +
+                                            "that wasn't able to take focus!");
+                                }
+
+                                /*
+                                 * Return true because we handled the key; super
+                                 * will return false because there was no click
+                                 * listener.
+                                 */
+                                super.onKeyUp(keyCode, event);
+                                return true;
+                            } else if ((event.getFlags()
+                                    & KeyEvent.FLAG_EDITOR_ACTION) != 0) {
+                                // No target for next focus, but make sure the IME
+                                // if this came from it.
+                                InputMethodManager imm = InputMethodManager.peekInstance();
+                                if (imm != null) {
+                                    imm.hideSoftInputFromWindow(getWindowToken(), 0);
+                                }
+                            }
+                        }
+                    }
                     return super.onKeyUp(keyCode, event);
                 }
                 break;
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 873f539..9598f96 100755
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -1635,6 +1635,14 @@
              will result when the reference is accessed.-->
         <attr name="nextFocusDown" format="reference"/>
 
+        <!-- Defines the next view to give focus to when the next focus is
+             {@link android.view.View#FOCUS_FORWARD}
+
+             If the reference refers to a view that does not exist or is part
+             of a hierarchy that is invisible, a {@link java.lang.RuntimeException}
+             will result when the reference is accessed.-->
+        <attr name="nextFocusForward" format="reference"/>
+
         <!-- Defines whether this view reacts to click events. -->
         <attr name="clickable" format="boolean" />
 
diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml
index 7e06c86..5aed668 100644
--- a/core/res/res/values/public.xml
+++ b/core/res/res/values/public.xml
@@ -1408,6 +1408,7 @@
   <public type="attr" name="fastScrollTrackDrawable" />
   <public type="attr" name="fastScrollOverlayPosition" />
   <public type="attr" name="customTokens" />
+  <public type="attr" name="nextFocusForward" />
 
   <public type="anim" name="animator_fade_in" />
   <public type="anim" name="animator_fade_out" />