Tell system server whether the app handled input events.

Refactored ViewRoot, NativeActivity and related classes to tell the
dispatcher whether an input event was actually handled by the application.

This will be used to move more of the global default key processing
into the system server instead of the application.

Change-Id: If06b98b6f45c543e5ac5b1eae2b3baf9371fba28
diff --git a/core/java/android/app/NativeActivity.java b/core/java/android/app/NativeActivity.java
index de36f27..a5c49ec 100644
--- a/core/java/android/app/NativeActivity.java
+++ b/core/java/android/app/NativeActivity.java
@@ -344,12 +344,14 @@
         }
     }
 
-    void dispatchUnhandledKeyEvent(KeyEvent event) {
+    boolean dispatchUnhandledKeyEvent(KeyEvent event) {
         try {
             mDispatchingUnhandledKey = true;
             View decor = getWindow().getDecorView();
             if (decor != null) {
-                decor.dispatchKeyEvent(event);
+                return decor.dispatchKeyEvent(event);
+            } else {
+                return false;
             }
         } finally {
             mDispatchingUnhandledKey = false;
diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java
index 26346d2..755e39f 100644
--- a/core/java/android/service/wallpaper/WallpaperService.java
+++ b/core/java/android/service/wallpaper/WallpaperService.java
@@ -219,14 +219,17 @@
         
         final InputHandler mInputHandler = new BaseInputHandler() {
             @Override
-            public void handleMotion(MotionEvent event, Runnable finishedCallback) {
+            public void handleMotion(MotionEvent event,
+                    InputQueue.FinishedCallback finishedCallback) {
+                boolean handled = false;
                 try {
                     int source = event.getSource();
                     if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
                         dispatchPointer(event);
+                        handled = true;
                     }
                 } finally {
-                    finishedCallback.run();
+                    finishedCallback.finished(handled);
                 }
             }
         };
diff --git a/core/java/android/view/InputHandler.java b/core/java/android/view/InputHandler.java
index 41a152d..14ce14c 100644
--- a/core/java/android/view/InputHandler.java
+++ b/core/java/android/view/InputHandler.java
@@ -29,7 +29,7 @@
      * @param event The key event data.
      * @param finishedCallback The callback to invoke when event processing is finished.
      */
-    public void handleKey(KeyEvent event, Runnable finishedCallback);
+    public void handleKey(KeyEvent event, InputQueue.FinishedCallback finishedCallback);
     
     /**
      * Handle a motion event.
@@ -39,5 +39,5 @@
      * @param event The motion event data.
      * @param finishedCallback The callback to invoke when event processing is finished.
      */
-    public void handleMotion(MotionEvent event, Runnable finishedCallback);
+    public void handleMotion(MotionEvent event, InputQueue.FinishedCallback finishedCallback);
 }
diff --git a/core/java/android/view/InputQueue.java b/core/java/android/view/InputQueue.java
index 9e800df..5735b63 100644
--- a/core/java/android/view/InputQueue.java
+++ b/core/java/android/view/InputQueue.java
@@ -53,7 +53,7 @@
     private static native void nativeRegisterInputChannel(InputChannel inputChannel,
             InputHandler inputHandler, MessageQueue messageQueue);
     private static native void nativeUnregisterInputChannel(InputChannel inputChannel);
-    private static native void nativeFinished(long finishedToken);
+    private static native void nativeFinished(long finishedToken, boolean handled);
     
     /** @hide */
     public InputQueue(InputChannel channel) {
@@ -116,18 +116,22 @@
     @SuppressWarnings("unused")
     private static void dispatchKeyEvent(InputHandler inputHandler,
             KeyEvent event, long finishedToken) {
-        Runnable finishedCallback = FinishedCallback.obtain(finishedToken);
+        FinishedCallback finishedCallback = FinishedCallback.obtain(finishedToken);
         inputHandler.handleKey(event, finishedCallback);
     }
 
     @SuppressWarnings("unused")
     private static void dispatchMotionEvent(InputHandler inputHandler,
             MotionEvent event, long finishedToken) {
-        Runnable finishedCallback = FinishedCallback.obtain(finishedToken);
+        FinishedCallback finishedCallback = FinishedCallback.obtain(finishedToken);
         inputHandler.handleMotion(event, finishedCallback);
     }
     
-    private static class FinishedCallback implements Runnable {
+    /**
+     * A callback that must be invoked to when finished processing an event.
+     * @hide
+     */
+    public static final class FinishedCallback {
         private static final boolean DEBUG_RECYCLING = false;
         
         private static final int RECYCLE_MAX_COUNT = 4;
@@ -156,13 +160,13 @@
             }
         }
         
-        public void run() {
+        public void finished(boolean handled) {
             synchronized (sLock) {
                 if (mFinishedToken == -1) {
                     throw new IllegalStateException("Event finished callback already invoked.");
                 }
                 
-                nativeFinished(mFinishedToken);
+                nativeFinished(mFinishedToken, handled);
                 mFinishedToken = -1;
 
                 if (sRecycleCount < RECYCLE_MAX_COUNT) {
diff --git a/core/java/android/view/ViewRoot.java b/core/java/android/view/ViewRoot.java
index 5b18715..fa7fe80 100644
--- a/core/java/android/view/ViewRoot.java
+++ b/core/java/android/view/ViewRoot.java
@@ -53,6 +53,7 @@
 import android.util.Log;
 import android.util.Slog;
 import android.util.SparseArray;
+import android.view.InputQueue.FinishedCallback;
 import android.view.View.MeasureSpec;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityManager;
@@ -1742,34 +1743,14 @@
             handleFinishedEvent(msg.arg1, msg.arg2 != 0);
             break;
         case DISPATCH_KEY:
-            if (LOCAL_LOGV) Log.v(
-                TAG, "Dispatching key "
-                + msg.obj + " to " + mView);
             deliverKeyEvent((KeyEvent)msg.obj, msg.arg1 != 0);
             break;
-        case DISPATCH_POINTER: {
-            MotionEvent event = (MotionEvent) msg.obj;
-            try {
-                deliverPointerEvent(event);
-            } finally {
-                event.recycle();
-                if (msg.arg1 != 0) {
-                    finishInputEvent();
-                }
-                if (LOCAL_LOGV || WATCH_POINTER) Log.i(TAG, "Done dispatching!");
-            }
-        } break;
-        case DISPATCH_TRACKBALL: {
-            MotionEvent event = (MotionEvent) msg.obj;
-            try {
-                deliverTrackballEvent(event);
-            } finally {
-                event.recycle();
-                if (msg.arg1 != 0) {
-                    finishInputEvent();
-                }
-            }
-        } break;
+        case DISPATCH_POINTER:
+            deliverPointerEvent((MotionEvent) msg.obj, msg.arg1 != 0);
+            break;
+        case DISPATCH_TRACKBALL:
+            deliverTrackballEvent((MotionEvent) msg.obj, msg.arg1 != 0);
+            break;
         case DISPATCH_APP_VISIBILITY:
             handleAppVisibility(msg.arg1 != 0);
             break;
@@ -1871,7 +1852,7 @@
                 // system!  Bad bad bad!
                 event = KeyEvent.changeFlags(event, event.getFlags() & ~KeyEvent.FLAG_FROM_SYSTEM);
             }
-            deliverKeyEventToViewHierarchy((KeyEvent)msg.obj, false);
+            deliverKeyEventPostIme((KeyEvent)msg.obj, false);
         } break;
         case FINISH_INPUT_CONNECTION: {
             InputMethodManager imm = InputMethodManager.peekInstance();
@@ -1897,7 +1878,7 @@
         }
     }
     
-    private void startInputEvent(Runnable finishedCallback) {
+    private void startInputEvent(InputQueue.FinishedCallback finishedCallback) {
         if (mFinishedCallback != null) {
             Slog.w(TAG, "Received a new input event from the input queue but there is "
                     + "already an unfinished input event in progress.");
@@ -1906,11 +1887,11 @@
         mFinishedCallback = finishedCallback;
     }
 
-    private void finishInputEvent() {
+    private void finishInputEvent(boolean handled) {
         if (LOCAL_LOGV) Log.v(TAG, "Telling window manager input event is finished");
 
         if (mFinishedCallback != null) {
-            mFinishedCallback.run();
+            mFinishedCallback.finished(handled);
             mFinishedCallback = null;
         } else {
             Slog.w(TAG, "Attempted to tell the input queue that the current input event "
@@ -2039,105 +2020,134 @@
         return false;
     }
 
-    private void deliverPointerEvent(MotionEvent event) {
+    private void deliverPointerEvent(MotionEvent event, boolean sendDone) {
+        // If there is no view, then the event will not be handled.
+        if (mView == null || !mAdded) {
+            finishPointerEvent(event, sendDone, false);
+            return;
+        }
+
+        // Translate the pointer event for compatibility, if needed.
         if (mTranslator != null) {
             mTranslator.translateEventInScreenToAppWindow(event);
         }
-        
-        boolean handled;
-        if (mView != null && mAdded) {
 
-            // enter touch mode on the down
-            boolean isDown = event.getAction() == MotionEvent.ACTION_DOWN;
-            if (isDown) {
-                ensureTouchMode(true);
-            }
-            if(Config.LOGV) {
-                captureMotionLog("captureDispatchPointer", event);
-            }
-            if (mCurScrollY != 0) {
-                event.offsetLocation(0, mCurScrollY);
-            }
-            if (MEASURE_LATENCY) {
-                lt.sample("A Dispatching TouchEvents", System.nanoTime() - event.getEventTimeNano());
-            }
-            // cache for possible drag-initiation
-            mLastTouchPoint.x = event.getRawX();
-            mLastTouchPoint.y = event.getRawY();
-            handled = mView.dispatchTouchEvent(event);
-            if (MEASURE_LATENCY) {
-                lt.sample("B Dispatched TouchEvents ", System.nanoTime() - event.getEventTimeNano());
-            }
-            if (!handled && isDown) {
-                int edgeSlop = mViewConfiguration.getScaledEdgeSlop();
+        // Enter touch mode on the down.
+        boolean isDown = event.getAction() == MotionEvent.ACTION_DOWN;
+        if (isDown) {
+            ensureTouchMode(true);
+        }
+        if(Config.LOGV) {
+            captureMotionLog("captureDispatchPointer", event);
+        }
 
-                final int edgeFlags = event.getEdgeFlags();
-                int direction = View.FOCUS_UP;
-                int x = (int)event.getX();
-                int y = (int)event.getY();
-                final int[] deltas = new int[2];
+        // Offset the scroll position.
+        if (mCurScrollY != 0) {
+            event.offsetLocation(0, mCurScrollY);
+        }
+        if (MEASURE_LATENCY) {
+            lt.sample("A Dispatching TouchEvents", System.nanoTime() - event.getEventTimeNano());
+        }
 
-                if ((edgeFlags & MotionEvent.EDGE_TOP) != 0) {
-                    direction = View.FOCUS_DOWN;
-                    if ((edgeFlags & MotionEvent.EDGE_LEFT) != 0) {
-                        deltas[0] = edgeSlop;
-                        x += edgeSlop;
-                    } else if ((edgeFlags & MotionEvent.EDGE_RIGHT) != 0) {
-                        deltas[0] = -edgeSlop;
-                        x -= edgeSlop;
-                    }
-                } else if ((edgeFlags & MotionEvent.EDGE_BOTTOM) != 0) {
-                    direction = View.FOCUS_UP;
-                    if ((edgeFlags & MotionEvent.EDGE_LEFT) != 0) {
-                        deltas[0] = edgeSlop;
-                        x += edgeSlop;
-                    } else if ((edgeFlags & MotionEvent.EDGE_RIGHT) != 0) {
-                        deltas[0] = -edgeSlop;
-                        x -= edgeSlop;
-                    }
-                } else if ((edgeFlags & MotionEvent.EDGE_LEFT) != 0) {
-                    direction = View.FOCUS_RIGHT;
+        // Remember the touch position for possible drag-initiation.
+        mLastTouchPoint.x = event.getRawX();
+        mLastTouchPoint.y = event.getRawY();
+
+        // Dispatch touch to view hierarchy.
+        boolean handled = mView.dispatchTouchEvent(event);
+        if (MEASURE_LATENCY) {
+            lt.sample("B Dispatched TouchEvents ", System.nanoTime() - event.getEventTimeNano());
+        }
+        if (handled) {
+            finishPointerEvent(event, sendDone, true);
+            return;
+        }
+
+        // Apply edge slop and try again, if appropriate.
+        final int edgeFlags = event.getEdgeFlags();
+        if (edgeFlags != 0 && mView instanceof ViewGroup) {
+            final int edgeSlop = mViewConfiguration.getScaledEdgeSlop();
+            int direction = View.FOCUS_UP;
+            int x = (int)event.getX();
+            int y = (int)event.getY();
+            final int[] deltas = new int[2];
+
+            if ((edgeFlags & MotionEvent.EDGE_TOP) != 0) {
+                direction = View.FOCUS_DOWN;
+                if ((edgeFlags & MotionEvent.EDGE_LEFT) != 0) {
+                    deltas[0] = edgeSlop;
+                    x += edgeSlop;
                 } else if ((edgeFlags & MotionEvent.EDGE_RIGHT) != 0) {
-                    direction = View.FOCUS_LEFT;
+                    deltas[0] = -edgeSlop;
+                    x -= edgeSlop;
                 }
+            } else if ((edgeFlags & MotionEvent.EDGE_BOTTOM) != 0) {
+                direction = View.FOCUS_UP;
+                if ((edgeFlags & MotionEvent.EDGE_LEFT) != 0) {
+                    deltas[0] = edgeSlop;
+                    x += edgeSlop;
+                } else if ((edgeFlags & MotionEvent.EDGE_RIGHT) != 0) {
+                    deltas[0] = -edgeSlop;
+                    x -= edgeSlop;
+                }
+            } else if ((edgeFlags & MotionEvent.EDGE_LEFT) != 0) {
+                direction = View.FOCUS_RIGHT;
+            } else if ((edgeFlags & MotionEvent.EDGE_RIGHT) != 0) {
+                direction = View.FOCUS_LEFT;
+            }
 
-                if (edgeFlags != 0 && mView instanceof ViewGroup) {
-                    View nearest = FocusFinder.getInstance().findNearestTouchable(
-                            ((ViewGroup) mView), x, y, direction, deltas);
-                    if (nearest != null) {
-                        event.offsetLocation(deltas[0], deltas[1]);
-                        event.setEdgeFlags(0);
-                        mView.dispatchTouchEvent(event);
-                    }
+            View nearest = FocusFinder.getInstance().findNearestTouchable(
+                    ((ViewGroup) mView), x, y, direction, deltas);
+            if (nearest != null) {
+                event.offsetLocation(deltas[0], deltas[1]);
+                event.setEdgeFlags(0);
+                if (mView.dispatchTouchEvent(event)) {
+                    finishPointerEvent(event, sendDone, true);
+                    return;
                 }
             }
         }
+
+        // Pointer event was unhandled.
+        finishPointerEvent(event, sendDone, false);
     }
 
-    private void deliverTrackballEvent(MotionEvent event) {
+    private void finishPointerEvent(MotionEvent event, boolean sendDone, boolean handled) {
+        event.recycle();
+        if (sendDone) {
+            finishInputEvent(handled);
+        }
+        if (LOCAL_LOGV || WATCH_POINTER) Log.i(TAG, "Done dispatching!");
+    }
+
+    private void deliverTrackballEvent(MotionEvent event, boolean sendDone) {
         if (DEBUG_TRACKBALL) Log.v(TAG, "Motion event:" + event);
 
-        boolean handled = false;
-        if (mView != null && mAdded) {
-            handled = mView.dispatchTrackballEvent(event);
-            if (handled) {
-                // If we reach this, we delivered a trackball event to mView and
-                // mView consumed it. Because we will not translate the trackball
-                // event into a key event, touch mode will not exit, so we exit
-                // touch mode here.
-                ensureTouchMode(false);
-                return;
-            }
-            
-            // Otherwise we could do something here, like changing the focus
-            // or something?
+        // If there is no view, then the event will not be handled.
+        if (mView == null || !mAdded) {
+            finishTrackballEvent(event, sendDone, false);
+            return;
         }
 
+        // Deliver the trackball event to the view.
+        if (mView.dispatchTrackballEvent(event)) {
+            // If we reach this, we delivered a trackball event to mView and
+            // mView consumed it. Because we will not translate the trackball
+            // event into a key event, touch mode will not exit, so we exit
+            // touch mode here.
+            ensureTouchMode(false);
+
+            finishTrackballEvent(event, sendDone, true);
+            mLastTrackballTime = Integer.MIN_VALUE;
+            return;
+        }
+
+        // Translate the trackball event into DPAD keys and try to deliver those.
         final TrackballAxis x = mTrackballAxisX;
         final TrackballAxis y = mTrackballAxisY;
 
         long curTime = SystemClock.uptimeMillis();
-        if ((mLastTrackballTime+MAX_TRACKBALL_DELAY) < curTime) {
+        if ((mLastTrackballTime + MAX_TRACKBALL_DELAY) < curTime) {
             // It has been too long since the last movement,
             // so restart at the beginning.
             x.reset(0);
@@ -2226,6 +2236,17 @@
             }
             mLastTrackballTime = curTime;
         }
+
+        // Unfortunately we can't tell whether the application consumed the keys, so
+        // we always consider the trackball event handled.
+        finishTrackballEvent(event, sendDone, true);
+    }
+
+    private void finishTrackballEvent(MotionEvent event, boolean sendDone, boolean handled) {
+        event.recycle();
+        if (sendDone) {
+            finishInputEvent(handled);
+        }
     }
 
     /**
@@ -2371,123 +2392,137 @@
     }
 
     private void deliverKeyEvent(KeyEvent event, boolean sendDone) {
-        // If mView is null, we just consume the key event because it doesn't
-        // make sense to do anything else with it.
-        boolean handled = mView == null || mView.dispatchKeyEventPreIme(event);
-        if (handled) {
-            if (sendDone) {
-                finishInputEvent();
-            }
+        // If there is no view, then the event will not be handled.
+        if (mView == null || !mAdded) {
+            finishKeyEvent(event, sendDone, false);
             return;
         }
-        // If it is possible for this window to interact with the input
-        // method window, then we want to first dispatch our key events
-        // to the input method.
+
+        if (LOCAL_LOGV) Log.v(TAG, "Dispatching key " + event + " to " + mView);
+
+        // Perform predispatching before the IME.
+        if (mView.dispatchKeyEventPreIme(event)) {
+            finishKeyEvent(event, sendDone, true);
+            return;
+        }
+
+        // Dispatch to the IME before propagating down the view hierarchy.
+        // The IME will eventually call back into handleFinishedEvent.
         if (mLastWasImTarget) {
             InputMethodManager imm = InputMethodManager.peekInstance();
-            if (imm != null && mView != null) {
+            if (imm != null) {
                 int seq = enqueuePendingEvent(event, sendDone);
                 if (DEBUG_IMF) Log.v(TAG, "Sending key event to IME: seq="
                         + seq + " event=" + event);
-                imm.dispatchKeyEvent(mView.getContext(), seq, event,
-                        mInputMethodCallback);
+                imm.dispatchKeyEvent(mView.getContext(), seq, event, mInputMethodCallback);
                 return;
             }
         }
-        deliverKeyEventToViewHierarchy(event, sendDone);
+
+        // Not dispatching to IME, continue with post IME actions.
+        deliverKeyEventPostIme(event, sendDone);
     }
 
-    void handleFinishedEvent(int seq, boolean handled) {
+    private void handleFinishedEvent(int seq, boolean handled) {
         final KeyEvent event = (KeyEvent)retrievePendingEvent(seq);
         if (DEBUG_IMF) Log.v(TAG, "IME finished event: seq=" + seq
                 + " handled=" + handled + " event=" + event);
         if (event != null) {
             final boolean sendDone = seq >= 0;
-            if (!handled) {
-                deliverKeyEventToViewHierarchy(event, sendDone);
-            } else if (sendDone) {
-                finishInputEvent();
+            if (handled) {
+                finishKeyEvent(event, sendDone, true);
             } else {
-                Log.w(TAG, "handleFinishedEvent(seq=" + seq
-                        + " handled=" + handled + " ev=" + event
-                        + ") neither delivering nor finishing key");
+                deliverKeyEventPostIme(event, sendDone);
             }
         }
     }
 
-    private void deliverKeyEventToViewHierarchy(KeyEvent event, boolean sendDone) {
-        try {
-            if (mView != null && mAdded) {
-                final int action = event.getAction();
-                boolean isDown = (action == KeyEvent.ACTION_DOWN);
+    private void deliverKeyEventPostIme(KeyEvent event, boolean sendDone) {
+        // If the view went away, then the event will not be handled.
+        if (mView == null || !mAdded) {
+            finishKeyEvent(event, sendDone, false);
+            return;
+        }
 
-                if (checkForLeavingTouchModeAndConsume(event)) {
-                    return;
-                }
+        // If the key's purpose is to exit touch mode then we consume it and consider it handled.
+        if (checkForLeavingTouchModeAndConsume(event)) {
+            finishKeyEvent(event, sendDone, true);
+            return;
+        }
 
-                if (Config.LOGV) {
-                    captureKeyLog("captureDispatchKeyEvent", event);
-                }
-                mFallbackEventHandler.preDispatchKeyEvent(event);
-                boolean keyHandled = mView.dispatchKeyEvent(event);
+        if (Config.LOGV) {
+            captureKeyLog("captureDispatchKeyEvent", event);
+        }
 
-                if (!keyHandled) {
-                    mFallbackEventHandler.dispatchKeyEvent(event);
-                }
+        // Deliver the key to the view hierarchy.
+        if (mView.dispatchKeyEvent(event)) {
+            finishKeyEvent(event, sendDone, true);
+            return;
+        }
 
-                if (!keyHandled && isDown) {
-                    int direction = 0;
-                    switch (event.getKeyCode()) {
-                    case KeyEvent.KEYCODE_DPAD_LEFT:
-                        direction = View.FOCUS_LEFT;
-                        break;
-                    case KeyEvent.KEYCODE_DPAD_RIGHT:
-                        direction = View.FOCUS_RIGHT;
-                        break;
-                    case KeyEvent.KEYCODE_DPAD_UP:
-                        direction = View.FOCUS_UP;
-                        break;
-                    case KeyEvent.KEYCODE_DPAD_DOWN:
-                        direction = View.FOCUS_DOWN;
-                        break;
-                    }
+        // Apply the fallback event policy.
+        if (mFallbackEventHandler.dispatchKeyEvent(event)) {
+            finishKeyEvent(event, sendDone, true);
+            return;
+        }
 
-                    if (direction != 0) {
+        // Handle automatic focus changes.
+        if (event.getAction() == KeyEvent.ACTION_DOWN) {
+            int direction = 0;
+            switch (event.getKeyCode()) {
+            case KeyEvent.KEYCODE_DPAD_LEFT:
+                direction = View.FOCUS_LEFT;
+                break;
+            case KeyEvent.KEYCODE_DPAD_RIGHT:
+                direction = View.FOCUS_RIGHT;
+                break;
+            case KeyEvent.KEYCODE_DPAD_UP:
+                direction = View.FOCUS_UP;
+                break;
+            case KeyEvent.KEYCODE_DPAD_DOWN:
+                direction = View.FOCUS_DOWN;
+                break;
+            }
 
-                        View focused = mView != null ? mView.findFocus() : null;
-                        if (focused != null) {
-                            View v = focused.focusSearch(direction);
-                            boolean focusPassed = false;
-                            if (v != null && v != focused) {
-                                // do the math the get the interesting rect
-                                // of previous focused into the coord system of
-                                // newly focused view
-                                focused.getFocusedRect(mTempRect);
-                                if (mView instanceof ViewGroup) {
-                                    ((ViewGroup) mView).offsetDescendantRectToMyCoords(
-                                            focused, mTempRect);
-                                    ((ViewGroup) mView).offsetRectIntoDescendantCoords(
-                                            v, mTempRect);
-                                }
-                                focusPassed = v.requestFocus(direction, mTempRect);
-                            }
-
-                            if (!focusPassed) {
-                                mView.dispatchUnhandledMove(focused, direction);
-                            } else {
-                                playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction));
-                            }
+            if (direction != 0) {
+                View focused = mView != null ? mView.findFocus() : null;
+                if (focused != null) {
+                    View v = focused.focusSearch(direction);
+                    if (v != null && v != focused) {
+                        // do the math the get the interesting rect
+                        // of previous focused into the coord system of
+                        // newly focused view
+                        focused.getFocusedRect(mTempRect);
+                        if (mView instanceof ViewGroup) {
+                            ((ViewGroup) mView).offsetDescendantRectToMyCoords(
+                                    focused, mTempRect);
+                            ((ViewGroup) mView).offsetRectIntoDescendantCoords(
+                                    v, mTempRect);
+                        }
+                        if (v.requestFocus(direction, mTempRect)) {
+                            playSoundEffect(
+                                    SoundEffectConstants.getContantForFocusDirection(direction));
+                            finishKeyEvent(event, sendDone, true);
+                            return;
                         }
                     }
+
+                    // Give the focused view a last chance to handle the dpad key.
+                    if (mView.dispatchUnhandledMove(focused, direction)) {
+                        finishKeyEvent(event, sendDone, true);
+                        return;
+                    }
                 }
             }
+        }
 
-        } finally {
-            if (sendDone) {
-                finishInputEvent();
-            }
-            // Let the exception fall through -- the looper will catch
-            // it and take care of the bad app for us.
+        // Key was unhandled.
+        finishKeyEvent(event, sendDone, false);
+    }
+
+    private void finishKeyEvent(KeyEvent event, boolean sendDone, boolean handled) {
+        if (sendDone) {
+            finishInputEvent(handled);
         }
     }
 
@@ -2759,15 +2794,15 @@
         sendMessage(msg);
     }
     
-    private Runnable mFinishedCallback;
+    private InputQueue.FinishedCallback mFinishedCallback;
     
     private final InputHandler mInputHandler = new InputHandler() {
-        public void handleKey(KeyEvent event, Runnable finishedCallback) {
+        public void handleKey(KeyEvent event, InputQueue.FinishedCallback finishedCallback) {
             startInputEvent(finishedCallback);
             dispatchKey(event, true);
         }
 
-        public void handleMotion(MotionEvent event, Runnable finishedCallback) {
+        public void handleMotion(MotionEvent event, InputQueue.FinishedCallback finishedCallback) {
             startInputEvent(finishedCallback);
             dispatchMotion(event, true);
         }
@@ -2814,7 +2849,7 @@
             // TODO
             Log.v(TAG, "Dropping unsupported motion event (unimplemented): " + event);
             if (sendDone) {
-                finishInputEvent();
+                finishInputEvent(false);
             }
         }
     }
diff --git a/core/java/android/view/WindowManagerPolicy.java b/core/java/android/view/WindowManagerPolicy.java
index e78d6a8..4deff5e 100644
--- a/core/java/android/view/WindowManagerPolicy.java
+++ b/core/java/android/view/WindowManagerPolicy.java
@@ -568,12 +568,8 @@
      * Called from the input dispatcher thread before a key is dispatched to a window.
      *
      * <p>Allows you to define
-     * behavior for keys that can not be overridden by applications or redirect
-     * key events to a different window.  This method is called from the
-     * input thread, with no locks held.
-     * 
-     * <p>Note that if you change the window a key is dispatched to, the new
-     * target window will receive the key event without having input focus.
+     * behavior for keys that can not be overridden by applications.
+     * This method is called from the input thread, with no locks held.
      * 
      * @param win The window that currently has focus.  This is where the key
      *            event will normally go.
@@ -591,6 +587,27 @@
             int keyCode, int scanCode, int metaState, int repeatCount, int policyFlags);
 
     /**
+     * Called from the input dispatcher thread when an application did not handle
+     * a key that was dispatched to it.
+     *
+     * <p>Allows you to define default global behavior for keys that were not handled
+     * by applications.  This method is called from the input thread, with no locks held.
+     * 
+     * @param win The window that currently has focus.  This is where the key
+     *            event will normally go.
+     * @param action The key event action.
+     * @param flags The key event flags.
+     * @param keyCode The key code.
+     * @param scanCode The key's scan code.
+     * @param metaState bit mask of meta keys that are held.
+     * @param repeatCount Number of times a key down has repeated.
+     * @param policyFlags The policy flags associated with the key.
+     * @return Returns true if the policy consumed the event.
+     */
+    public boolean dispatchUnhandledKey(WindowState win, int action, int flags,
+            int keyCode, int scanCode, int metaState, int repeatCount, int policyFlags);
+
+    /**
      * Called when layout of the windows is about to start.
      * 
      * @param displayWidth The current full width of the screen.