Merge "Add "KeyFallback" handling ability to Views"
diff --git a/api/current.txt b/api/current.txt
index 6043aba..06163cc 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -46087,6 +46087,7 @@
     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);
@@ -46421,6 +46422,7 @@
     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);
@@ -46474,6 +46476,7 @@
     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 requestApplyInsets();
@@ -46892,6 +46895,10 @@
     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);
   }
diff --git a/api/system-current.txt b/api/system-current.txt
index f0124ae..808f222 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -49832,6 +49832,7 @@
     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);
@@ -50166,6 +50167,7 @@
     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);
@@ -50219,6 +50221,7 @@
     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 requestApplyInsets();
@@ -50637,6 +50640,10 @@
     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);
   }
diff --git a/api/test-current.txt b/api/test-current.txt
index 390e7c9..da59056 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -46744,6 +46744,7 @@
     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);
@@ -47080,6 +47081,7 @@
     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);
@@ -47133,6 +47135,7 @@
     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 requestApplyInsets();
@@ -47555,6 +47558,10 @@
     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);
   }
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index be09fe8..e36a298 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -4245,6 +4245,8 @@
         OnApplyWindowInsetsListener mOnApplyWindowInsetsListener;
 
         OnCapturedPointerListener mOnCapturedPointerListener;
+
+        private ArrayList<OnKeyFallbackListener> mKeyFallbackListeners;
     }
 
     ListenerInfo mListenerInfo;
@@ -25194,6 +25196,29 @@
     }
 
     /**
+     * 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.
+     */
+    public interface OnKeyFallbackListener {
+        /**
+         * 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.
+         *
+         * @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.
+         */
+        boolean onKeyFallback(View v, KeyEvent event);
+    }
+
+    /**
      * Interface definition for a callback to be invoked when a touch event is
      * dispatched to this view. The callback will be invoked before the touch
      * event is given to the view.
@@ -26866,4 +26891,56 @@
         }
         return mTooltipInfo.mTooltipPopup.getContentView();
     }
+
+    /**
+     * 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).
+     *
+     * @param event A not-previously-handled event.
+     * @return {@code true} if the event was handled, {@code false} otherwise.
+     * @see #addKeyFallbackListener
+     */
+    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)) {
+                    return true;
+                }
+            }
+        }
+        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);
+    }
+
+    /**
+     * Removes a listener which will receive unhandled {@link KeyEvent}s.
+     * @param listener the receiver of fallback {@link KeyEvent}s.
+     * @see #onKeyFallback(KeyEvent)
+     */
+    public void removeKeyFallbackListener(OnKeyFallbackListener listener) {
+        if (mListenerInfo != null) {
+            if (mListenerInfo.mKeyFallbackListeners != null) {
+                mListenerInfo.mKeyFallbackListeners.remove(listener);
+                if (mListenerInfo.mKeyFallbackListeners.isEmpty()) {
+                    mListenerInfo.mKeyFallbackListeners = null;
+                }
+            }
+        }
+    }
 }
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index e30496f..e9509b7 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -73,6 +73,7 @@
 import android.util.MergedConfiguration;
 import android.util.Slog;
 import android.util.SparseArray;
+import android.util.SparseBooleanArray;
 import android.util.TimeUtils;
 import android.util.TypedValue;
 import android.view.Surface.OutOfResourcesException;
@@ -362,6 +363,8 @@
     InputStage mFirstPostImeInputStage;
     InputStage mSyntheticInputStage;
 
+    private final KeyFallbackManager mKeyFallbackManager = new KeyFallbackManager();
+
     boolean mWindowAttributesChanged = false;
     int mWindowAttributesChangesFlag = 0;
 
@@ -4764,6 +4767,13 @@
         private int processKeyEvent(QueuedInputEvent q) {
             final KeyEvent event = (KeyEvent)q.mEvent;
 
+            mKeyFallbackManager.mDispatched = false;
+
+            if (mKeyFallbackManager.hasFocus()
+                    && mKeyFallbackManager.dispatchUnique(mView, event)) {
+                return FINISH_HANDLED;
+            }
+
             // Deliver the key to the view hierarchy.
             if (mView.dispatchKeyEvent(event)) {
                 return FINISH_HANDLED;
@@ -4773,6 +4783,10 @@
                 return FINISH_NOT_HANDLED;
             }
 
+            if (mKeyFallbackManager.dispatchUnique(mView, event)) {
+                return FINISH_HANDLED;
+            }
+
             int groupNavigationDirection = 0;
 
             if (event.getAction() == KeyEvent.ACTION_DOWN
@@ -7529,6 +7543,16 @@
         }
     }
 
+    /**
+     * Dispatches a KeyEvent to all registered key fallback handlers.
+     *
+     * @param event
+     * @return {@code true} if the event was handled, {@code false} otherwise.
+     */
+    public boolean dispatchKeyFallbackEvent(KeyEvent event) {
+        return mKeyFallbackManager.dispatch(mView, event);
+    }
+
     class TakenSurfaceHolder extends BaseSurfaceHolder {
         @Override
         public boolean onAllowLockCanvas() {
@@ -8093,4 +8117,92 @@
             run();
         }
     }
+
+    private static class KeyFallbackManager {
+
+        // This is used to ensure that key-fallback 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
+        // 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.
+        boolean mDispatched = false;
+
+        SparseBooleanArray mCapturedKeys = new SparseBooleanArray();
+        WeakReference<View> mFallbackReceiver = null;
+        int mVisitCount = 0;
+
+        private void updateCaptureState(KeyEvent event) {
+            if (event.getAction() == KeyEvent.ACTION_DOWN) {
+                mCapturedKeys.append(event.getKeyCode(), true);
+            }
+            if (event.getAction() == KeyEvent.ACTION_UP) {
+                mCapturedKeys.delete(event.getKeyCode());
+            }
+        }
+
+        boolean dispatch(View root, KeyEvent event) {
+            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "KeyFallback dispatch");
+            mDispatched = true;
+
+            updateCaptureState(event);
+
+            if (mFallbackReceiver != null) {
+                View target = mFallbackReceiver.get();
+                if (mCapturedKeys.size() == 0) {
+                    mFallbackReceiver = null;
+                }
+                if (target != null && target.isAttachedToWindow()) {
+                    return target.onKeyFallback(event);
+                }
+                // consume anyways so that we don't feed uncaptured key events to other views
+                return true;
+            }
+
+            boolean result = dispatchInZOrder(root, event);
+            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;
+        }
+
+        boolean hasFocus() {
+            return mFallbackReceiver != null;
+        }
+
+        boolean dispatchUnique(View root, KeyEvent event) {
+            if (mDispatched) {
+                return false;
+            }
+            return dispatch(root, event);
+        }
+    }
 }
diff --git a/core/java/com/android/internal/policy/DecorView.java b/core/java/com/android/internal/policy/DecorView.java
index 85251d4..5fddfba 100644
--- a/core/java/com/android/internal/policy/DecorView.java
+++ b/core/java/com/android/internal/policy/DecorView.java
@@ -431,7 +431,11 @@
             }
         }
 
-        return super.dispatchKeyEvent(event);
+        if (super.dispatchKeyEvent(event)) {
+            return true;
+        }
+
+        return (getViewRootImpl() != null) && getViewRootImpl().dispatchKeyFallbackEvent(event);
     }
 
     public boolean superDispatchKeyShortcutEvent(KeyEvent event) {