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) {