Merge "Renamed KeyFallbackEvent to UnhandledKeyEvent and exposed dispatch" into pi-dev
diff --git a/api/current.txt b/api/current.txt
index 37c5c6f..992c60c 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -47301,10 +47301,10 @@
     method public void addExtraDataToAccessibilityNodeInfo(android.view.accessibility.AccessibilityNodeInfo, java.lang.String, android.os.Bundle);
     method public void addFocusables(java.util.ArrayList<android.view.View>, int);
     method public void addFocusables(java.util.ArrayList<android.view.View>, int, int);
-    method public void addKeyFallbackListener(android.view.View.OnKeyFallbackListener);
     method public void addKeyboardNavigationClusters(java.util.Collection<android.view.View>, int);
     method public void addOnAttachStateChangeListener(android.view.View.OnAttachStateChangeListener);
     method public void addOnLayoutChangeListener(android.view.View.OnLayoutChangeListener);
+    method public void addOnUnhandledKeyEventListener(android.view.View.OnUnhandledKeyEventListener);
     method public void addTouchables(java.util.ArrayList<android.view.View>);
     method public android.view.ViewPropertyAnimator animate();
     method public void announceForAccessibility(java.lang.CharSequence);
@@ -47642,7 +47642,6 @@
     method public void onInitializeAccessibilityEvent(android.view.accessibility.AccessibilityEvent);
     method public void onInitializeAccessibilityNodeInfo(android.view.accessibility.AccessibilityNodeInfo);
     method public boolean onKeyDown(int, android.view.KeyEvent);
-    method public boolean onKeyFallback(android.view.KeyEvent);
     method public boolean onKeyLongPress(int, android.view.KeyEvent);
     method public boolean onKeyMultiple(int, int, android.view.KeyEvent);
     method public boolean onKeyPreIme(int, android.view.KeyEvent);
@@ -47696,9 +47695,9 @@
     method public void refreshDrawableState();
     method public void releasePointerCapture();
     method public boolean removeCallbacks(java.lang.Runnable);
-    method public void removeKeyFallbackListener(android.view.View.OnKeyFallbackListener);
     method public void removeOnAttachStateChangeListener(android.view.View.OnAttachStateChangeListener);
     method public void removeOnLayoutChangeListener(android.view.View.OnLayoutChangeListener);
+    method public void removeOnUnhandledKeyEventListener(android.view.View.OnUnhandledKeyEventListener);
     method public void requestApplyInsets();
     method public deprecated void requestFitSystemWindows();
     method public final boolean requestFocus();
@@ -48122,10 +48121,6 @@
     method public abstract boolean onHover(android.view.View, android.view.MotionEvent);
   }
 
-  public static abstract interface View.OnKeyFallbackListener {
-    method public abstract boolean onKeyFallback(android.view.View, android.view.KeyEvent);
-  }
-
   public static abstract interface View.OnKeyListener {
     method public abstract boolean onKey(android.view.View, int, android.view.KeyEvent);
   }
@@ -48150,6 +48145,10 @@
     method public abstract boolean onTouch(android.view.View, android.view.MotionEvent);
   }
 
+  public static abstract interface View.OnUnhandledKeyEventListener {
+    method public abstract boolean onUnhandledKeyEvent(android.view.View, android.view.KeyEvent);
+  }
+
   public final class ViewAnimationUtils {
     method public static android.animation.Animator createCircularReveal(android.view.View, int, int, float, float);
   }
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index ea3710c..7e6ee76 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -4320,7 +4320,7 @@
 
         OnCapturedPointerListener mOnCapturedPointerListener;
 
-        private ArrayList<OnKeyFallbackListener> mKeyFallbackListeners;
+        private ArrayList<OnUnhandledKeyEventListener> mUnhandledKeyListeners;
     }
 
     ListenerInfo mListenerInfo;
@@ -25882,26 +25882,19 @@
     }
 
     /**
-     * Interface definition for a callback to be invoked when a hardware key event is
-     * dispatched to this view during the fallback phase. This means no view in the hierarchy
-     * has handled this event.
+     * Interface definition for a callback to be invoked when a hardware key event hasn't
+     * been handled by the view hierarchy.
      */
-    public interface OnKeyFallbackListener {
+    public interface OnUnhandledKeyEventListener {
         /**
-         * Called when a hardware key is dispatched to a view in the fallback phase. This allows
-         * listeners to respond to events after the view hierarchy has had a chance to respond.
-         * <p>Key presses in software keyboards will generally NOT trigger this method,
-         * although some may elect to do so in some situations. Do not assume a
-         * software input method has to be key-based; even if it is, it may use key presses
-         * in a different way than you expect, so there is no way to reliably catch soft
-         * input key presses.
+         * Called when a hardware key is dispatched to a view after being unhandled during normal
+         * {@link KeyEvent} dispatch.
          *
          * @param v The view the key has been dispatched to.
-         * @param event The KeyEvent object containing full information about
-         *        the event.
-         * @return True if the listener has consumed the event, false otherwise.
+         * @param event The KeyEvent object containing information about the event.
+         * @return {@code true} if the listener has consumed the event, {@code false} otherwise.
          */
-        boolean onKeyFallback(View v, KeyEvent event);
+        boolean onUnhandledKeyEvent(View v, KeyEvent event);
     }
 
     /**
@@ -27600,21 +27593,36 @@
         return sUseDefaultFocusHighlight;
     }
 
-  /**
+    /**
+     * Dispatch a previously unhandled {@link KeyEvent} to this view. Unlike normal key dispatch,
+     * this dispatches to ALL child views until it is consumed. The dispatch order is z-order
+     * (visually on-top views first).
+     *
+     * @param evt the previously unhandled {@link KeyEvent}.
+     * @return the {@link View} which consumed the event or {@code null} if not consumed.
+     */
+    View dispatchUnhandledKeyEvent(KeyEvent evt) {
+        if (onUnhandledKeyEvent(evt)) {
+            return this;
+        }
+        return null;
+    }
+
+    /**
      * Allows this view to handle {@link KeyEvent}s which weren't handled by normal dispatch. This
      * occurs after the normal view hierarchy dispatch, but before the window callback. By default,
      * this will dispatch into all the listeners registered via
-     * {@link #addKeyFallbackListener(OnKeyFallbackListener)} in last-in-first-out order (most
-     * recently added will receive events first).
+     * {@link #addOnUnhandledKeyEventListener(OnUnhandledKeyEventListener)} in last-in-first-out
+     * order (most recently added will receive events first).
      *
-     * @param event A not-previously-handled event.
+     * @param event An unhandled event.
      * @return {@code true} if the event was handled, {@code false} otherwise.
-     * @see #addKeyFallbackListener
+     * @see #addOnUnhandledKeyEventListener
      */
-    public boolean onKeyFallback(@NonNull KeyEvent event) {
-        if (mListenerInfo != null && mListenerInfo.mKeyFallbackListeners != null) {
-            for (int i = mListenerInfo.mKeyFallbackListeners.size() - 1; i >= 0; --i) {
-                if (mListenerInfo.mKeyFallbackListeners.get(i).onKeyFallback(this, event)) {
+    boolean onUnhandledKeyEvent(@NonNull KeyEvent event) {
+        if (mListenerInfo != null && mListenerInfo.mUnhandledKeyListeners != null) {
+            for (int i = mListenerInfo.mUnhandledKeyListeners.size() - 1; i >= 0; --i) {
+                if (mListenerInfo.mUnhandledKeyListeners.get(i).onUnhandledKeyEvent(this, event)) {
                     return true;
                 }
             }
@@ -27622,31 +27630,47 @@
         return false;
     }
 
-    /**
-     * Adds a listener which will receive unhandled {@link KeyEvent}s.
-     * @param listener the receiver of fallback {@link KeyEvent}s.
-     * @see #onKeyFallback(KeyEvent)
-     */
-    public void addKeyFallbackListener(OnKeyFallbackListener listener) {
-        ArrayList<OnKeyFallbackListener> fallbacks = getListenerInfo().mKeyFallbackListeners;
-        if (fallbacks == null) {
-            fallbacks = new ArrayList<>();
-            getListenerInfo().mKeyFallbackListeners = fallbacks;
-        }
-        fallbacks.add(listener);
+    boolean hasUnhandledKeyListener() {
+        return (mListenerInfo != null && mListenerInfo.mUnhandledKeyListeners != null
+                && !mListenerInfo.mUnhandledKeyListeners.isEmpty());
     }
 
     /**
-     * Removes a listener which will receive unhandled {@link KeyEvent}s.
-     * @param listener the receiver of fallback {@link KeyEvent}s.
-     * @see #onKeyFallback(KeyEvent)
+     * Adds a listener which will receive unhandled {@link KeyEvent}s. This must be called on the
+     * UI thread.
+     *
+     * @param listener a receiver of unhandled {@link KeyEvent}s.
+     * @see #removeOnUnhandledKeyEventListener
      */
-    public void removeKeyFallbackListener(OnKeyFallbackListener listener) {
+    public void addOnUnhandledKeyEventListener(OnUnhandledKeyEventListener listener) {
+        ArrayList<OnUnhandledKeyEventListener> listeners = getListenerInfo().mUnhandledKeyListeners;
+        if (listeners == null) {
+            listeners = new ArrayList<>();
+            getListenerInfo().mUnhandledKeyListeners = listeners;
+        }
+        listeners.add(listener);
+        if (listeners.size() == 1 && mParent instanceof ViewGroup) {
+            ((ViewGroup) mParent).incrementChildUnhandledKeyListeners();
+        }
+    }
+
+    /**
+     * Removes a listener which will receive unhandled {@link KeyEvent}s. This must be called on the
+     * UI thread.
+     *
+     * @param listener a receiver of unhandled {@link KeyEvent}s.
+     * @see #addOnUnhandledKeyEventListener
+     */
+    public void removeOnUnhandledKeyEventListener(OnUnhandledKeyEventListener listener) {
         if (mListenerInfo != null) {
-            if (mListenerInfo.mKeyFallbackListeners != null) {
-                mListenerInfo.mKeyFallbackListeners.remove(listener);
-                if (mListenerInfo.mKeyFallbackListeners.isEmpty()) {
-                    mListenerInfo.mKeyFallbackListeners = null;
+            if (mListenerInfo.mUnhandledKeyListeners != null
+                    && !mListenerInfo.mUnhandledKeyListeners.isEmpty()) {
+                mListenerInfo.mUnhandledKeyListeners.remove(listener);
+                if (mListenerInfo.mUnhandledKeyListeners.isEmpty()) {
+                    mListenerInfo.mUnhandledKeyListeners = null;
+                    if (mParent instanceof ViewGroup) {
+                        ((ViewGroup) mParent).decrementChildUnhandledKeyListeners();
+                    }
                 }
             }
         }
diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java
index 33fcf6a..6002fe5 100644
--- a/core/java/android/view/ViewGroup.java
+++ b/core/java/android/view/ViewGroup.java
@@ -583,6 +583,11 @@
     private List<Integer> mTransientIndices = null;
     private List<View> mTransientViews = null;
 
+    /**
+     * Keeps track of how many child views have UnhandledKeyEventListeners. This should only be
+     * updated on the UI thread so shouldn't require explicit synchronization.
+     */
+    int mChildUnhandledKeyListeners = 0;
 
     /**
      * Empty ActionMode used as a sentinel in recursive entries to startActionModeForChild.
@@ -5055,6 +5060,9 @@
             child.assignParent(this);
         } else {
             child.mParent = this;
+            if (child.hasUnhandledKeyListener()) {
+                incrementChildUnhandledKeyListeners();
+            }
         }
 
         final boolean childHasFocus = child.hasFocus();
@@ -5359,6 +5367,10 @@
 
         removeFromArray(index);
 
+        if (view.hasUnhandledKeyListener()) {
+            decrementChildUnhandledKeyListeners();
+        }
+
         if (view == mDefaultFocus) {
             clearDefaultFocus(view);
         }
@@ -7537,6 +7549,62 @@
         }
     }
 
+    @Override
+    boolean hasUnhandledKeyListener() {
+        return (mChildUnhandledKeyListeners > 0) || super.hasUnhandledKeyListener();
+    }
+
+    void incrementChildUnhandledKeyListeners() {
+        mChildUnhandledKeyListeners += 1;
+        if (mChildUnhandledKeyListeners == 1) {
+            if (mParent instanceof ViewGroup) {
+                ((ViewGroup) mParent).incrementChildUnhandledKeyListeners();
+            }
+        }
+    }
+
+    void decrementChildUnhandledKeyListeners() {
+        mChildUnhandledKeyListeners -= 1;
+        if (mChildUnhandledKeyListeners == 0) {
+            if (mParent instanceof ViewGroup) {
+                ((ViewGroup) mParent).decrementChildUnhandledKeyListeners();
+            }
+        }
+    }
+
+    @Override
+    View dispatchUnhandledKeyEvent(KeyEvent evt) {
+        if (!hasUnhandledKeyListener()) {
+            return null;
+        }
+        ArrayList<View> orderedViews = buildOrderedChildList();
+        if (orderedViews != null) {
+            try {
+                for (int i = orderedViews.size() - 1; i >= 0; --i) {
+                    View v = orderedViews.get(i);
+                    View consumer = v.dispatchUnhandledKeyEvent(evt);
+                    if (consumer != null) {
+                        return consumer;
+                    }
+                }
+            } finally {
+                orderedViews.clear();
+            }
+        } else {
+            for (int i = getChildCount() - 1; i >= 0; --i) {
+                View v = getChildAt(i);
+                View consumer = v.dispatchUnhandledKeyEvent(evt);
+                if (consumer != null) {
+                    return consumer;
+                }
+            }
+        }
+        if (onUnhandledKeyEvent(evt)) {
+            return this;
+        }
+        return null;
+    }
+
     /**
      * LayoutParams are used by views to tell their parents how they want to be
      * laid out. See
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index d521684..433c90b 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -380,7 +380,7 @@
     InputStage mFirstPostImeInputStage;
     InputStage mSyntheticInputStage;
 
-    private final KeyFallbackManager mKeyFallbackManager = new KeyFallbackManager();
+    private final UnhandledKeyManager mUnhandledKeyManager = new UnhandledKeyManager();
 
     boolean mWindowAttributesChanged = false;
     int mWindowAttributesChangesFlag = 0;
@@ -4975,10 +4975,10 @@
         private int processKeyEvent(QueuedInputEvent q) {
             final KeyEvent event = (KeyEvent)q.mEvent;
 
-            mKeyFallbackManager.mDispatched = false;
+            mUnhandledKeyManager.mDispatched = false;
 
-            if (mKeyFallbackManager.hasFocus()
-                    && mKeyFallbackManager.dispatchUnique(mView, event)) {
+            if (mUnhandledKeyManager.hasFocus()
+                    && mUnhandledKeyManager.dispatchUnique(mView, event)) {
                 return FINISH_HANDLED;
             }
 
@@ -4991,7 +4991,7 @@
                 return FINISH_NOT_HANDLED;
             }
 
-            if (mKeyFallbackManager.dispatchUnique(mView, event)) {
+            if (mUnhandledKeyManager.dispatchUnique(mView, event)) {
                 return FINISH_HANDLED;
             }
 
@@ -7798,7 +7798,7 @@
      * @return {@code true} if the event was handled, {@code false} otherwise.
      */
     public boolean dispatchKeyFallbackEvent(KeyEvent event) {
-        return mKeyFallbackManager.dispatch(mView, event);
+        return mUnhandledKeyManager.dispatch(mView, event);
     }
 
     class TakenSurfaceHolder extends BaseSurfaceHolder {
@@ -8374,18 +8374,17 @@
         }
     }
 
-    private static class KeyFallbackManager {
+    private static class UnhandledKeyManager {
 
-        // This is used to ensure that key-fallback events are only dispatched once. We attempt
+        // This is used to ensure that unhandled events are only dispatched once. We attempt
         // to dispatch more than once in order to achieve a certain order. Specifically, if we
-        // are in an Activity or Dialog (and have a Window.Callback), the keyfallback events should
+        // are in an Activity or Dialog (and have a Window.Callback), the unhandled events should
         // be dispatched after the view hierarchy, but before the Activity. However, if we aren't
-        // in an activity, we still want key fallbacks to be dispatched.
+        // in an activity, we still want unhandled keys to be dispatched.
         boolean mDispatched = false;
 
         SparseBooleanArray mCapturedKeys = new SparseBooleanArray();
-        WeakReference<View> mFallbackReceiver = null;
-        int mVisitCount = 0;
+        WeakReference<View> mCurrentReceiver = null;
 
         private void updateCaptureState(KeyEvent event) {
             if (event.getAction() == KeyEvent.ACTION_DOWN) {
@@ -8402,56 +8401,28 @@
 
             updateCaptureState(event);
 
-            if (mFallbackReceiver != null) {
-                View target = mFallbackReceiver.get();
+            if (mCurrentReceiver != null) {
+                View target = mCurrentReceiver.get();
                 if (mCapturedKeys.size() == 0) {
-                    mFallbackReceiver = null;
+                    mCurrentReceiver = null;
                 }
                 if (target != null && target.isAttachedToWindow()) {
-                    return target.onKeyFallback(event);
+                    target.onUnhandledKeyEvent(event);
                 }
                 // consume anyways so that we don't feed uncaptured key events to other views
                 return true;
             }
 
-            boolean result = dispatchInZOrder(root, event);
+            View consumer = root.dispatchUnhandledKeyEvent(event);
+            if (consumer != null) {
+                mCurrentReceiver = new WeakReference<>(consumer);
+            }
             Trace.traceEnd(Trace.TRACE_TAG_VIEW);
-            return result;
-        }
-
-        private boolean dispatchInZOrder(View view, KeyEvent evt) {
-            if (view instanceof ViewGroup) {
-                ViewGroup vg = (ViewGroup) view;
-                ArrayList<View> orderedViews = vg.buildOrderedChildList();
-                if (orderedViews != null) {
-                    try {
-                        for (int i = orderedViews.size() - 1; i >= 0; --i) {
-                            View v = orderedViews.get(i);
-                            if (dispatchInZOrder(v, evt)) {
-                                return true;
-                            }
-                        }
-                    } finally {
-                        orderedViews.clear();
-                    }
-                } else {
-                    for (int i = vg.getChildCount() - 1; i >= 0; --i) {
-                        View v = vg.getChildAt(i);
-                        if (dispatchInZOrder(v, evt)) {
-                            return true;
-                        }
-                    }
-                }
-            }
-            if (view.onKeyFallback(evt)) {
-                mFallbackReceiver = new WeakReference<>(view);
-                return true;
-            }
-            return false;
+            return consumer != null;
         }
 
         boolean hasFocus() {
-            return mFallbackReceiver != null;
+            return mCurrentReceiver != null;
         }
 
         boolean dispatchUnique(View root, KeyEvent event) {