/*
* Copyright (C) 2006 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.view;
import static android.view.WindowCallbacks.RESIZE_MODE_DOCKED_DIVIDER;
import static android.view.WindowCallbacks.RESIZE_MODE_FREEFORM;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_DECOR_VIEW_VISIBILITY;
import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL;
import static android.view.WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY;
import android.Manifest;
import android.animation.LayoutTransition;
import android.annotation.NonNull;
import android.app.ActivityManagerNative;
import android.app.ResourcesManager;
import android.content.ClipDescription;
import android.content.ComponentCallbacks;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.CompatibilityInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.PixelFormat;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.Region;
import android.graphics.drawable.AnimatedVectorDrawable;
import android.graphics.drawable.Drawable;
import android.hardware.display.DisplayManager;
import android.hardware.display.DisplayManager.DisplayListener;
import android.hardware.input.InputManager;
import android.media.AudioManager;
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
import android.os.Debug;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.Trace;
import android.util.AndroidRuntimeException;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.Slog;
import android.util.TimeUtils;
import android.util.TypedValue;
import android.view.Surface.OutOfResourcesException;
import android.view.View.AttachInfo;
import android.view.View.MeasureSpec;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener;
import android.view.accessibility.AccessibilityManager.HighTextContrastChangeListener;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
import android.view.accessibility.AccessibilityNodeProvider;
import android.view.accessibility.IAccessibilityInteractionConnection;
import android.view.accessibility.IAccessibilityInteractionConnectionCallback;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.Interpolator;
import android.view.inputmethod.InputMethodManager;
import android.widget.Scroller;
import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.os.IResultReceiver;
import com.android.internal.os.SomeArgs;
import com.android.internal.policy.PhoneFallbackEventHandler;
import com.android.internal.view.BaseSurfaceHolder;
import com.android.internal.view.RootViewSurfaceTaker;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.concurrent.CountDownLatch;
/**
* The top of a view hierarchy, implementing the needed protocol between View
* and the WindowManager. This is for the most part an internal implementation
* detail of {@link WindowManagerGlobal}.
*
* {@hide}
*/
@SuppressWarnings({"EmptyCatchBlock", "PointlessBooleanExpression"})
public final class ViewRootImpl implements ViewParent,
View.AttachInfo.Callbacks, ThreadedRenderer.HardwareDrawCallbacks {
private static final String TAG = "ViewRootImpl";
private static final boolean DBG = false;
private static final boolean LOCAL_LOGV = false;
/** @noinspection PointlessBooleanExpression*/
private static final boolean DEBUG_DRAW = false || LOCAL_LOGV;
private static final boolean DEBUG_LAYOUT = false || LOCAL_LOGV;
private static final boolean DEBUG_DIALOG = false || LOCAL_LOGV;
private static final boolean DEBUG_INPUT_RESIZE = false || LOCAL_LOGV;
private static final boolean DEBUG_ORIENTATION = false || LOCAL_LOGV;
private static final boolean DEBUG_TRACKBALL = false || LOCAL_LOGV;
private static final boolean DEBUG_IMF = false || LOCAL_LOGV;
private static final boolean DEBUG_CONFIGURATION = false || LOCAL_LOGV;
private static final boolean DEBUG_FPS = false;
private static final boolean DEBUG_INPUT_STAGES = false || LOCAL_LOGV;
private static final boolean DEBUG_KEEP_SCREEN_ON = false || LOCAL_LOGV;
/**
* Set to false if we do not want to use the multi threaded renderer. Note that by disabling
* this, WindowCallbacks will not fire.
*/
private static final boolean USE_MT_RENDERER = true;
/**
* Set this system property to true to force the view hierarchy to render
* at 60 Hz. This can be used to measure the potential framerate.
*/
private static final String PROPERTY_PROFILE_RENDERING = "viewroot.profile_rendering";
// properties used by emulator to determine display shape
public static final String PROPERTY_EMULATOR_WIN_OUTSET_BOTTOM_PX =
"ro.emu.win_outset_bottom_px";
/**
* Maximum time we allow the user to roll the trackball enough to generate
* a key event, before resetting the counters.
*/
static final int MAX_TRACKBALL_DELAY = 250;
static final ThreadLocal
* Events are delivered to the stage by the {@link #deliver} method. The stage * then has the choice of finishing the event or forwarding it to the next stage. *
*/ abstract class InputStage { private final InputStage mNext; protected static final int FORWARD = 0; protected static final int FINISH_HANDLED = 1; protected static final int FINISH_NOT_HANDLED = 2; /** * Creates an input stage. * @param next The next stage to which events should be forwarded. */ public InputStage(InputStage next) { mNext = next; } /** * Delivers an event to be processed. */ public final void deliver(QueuedInputEvent q) { if ((q.mFlags & QueuedInputEvent.FLAG_FINISHED) != 0) { forward(q); } else if (shouldDropInputEvent(q)) { finish(q, false); } else { apply(q, onProcess(q)); } } /** * Marks the the input event as finished then forwards it to the next stage. */ protected void finish(QueuedInputEvent q, boolean handled) { q.mFlags |= QueuedInputEvent.FLAG_FINISHED; if (handled) { q.mFlags |= QueuedInputEvent.FLAG_FINISHED_HANDLED; } forward(q); } /** * Forwards the event to the next stage. */ protected void forward(QueuedInputEvent q) { onDeliverToNext(q); } /** * Applies a result code from {@link #onProcess} to the specified event. */ protected void apply(QueuedInputEvent q, int result) { if (result == FORWARD) { forward(q); } else if (result == FINISH_HANDLED) { finish(q, true); } else if (result == FINISH_NOT_HANDLED) { finish(q, false); } else { throw new IllegalArgumentException("Invalid result: " + result); } } /** * Called when an event is ready to be processed. * @return A result code indicating how the event was handled. */ protected int onProcess(QueuedInputEvent q) { return FORWARD; } /** * Called when an event is being delivered to the next stage. */ protected void onDeliverToNext(QueuedInputEvent q) { if (DEBUG_INPUT_STAGES) { Log.v(mTag, "Done with " + getClass().getSimpleName() + ". " + q); } if (mNext != null) { mNext.deliver(q); } else { finishInputEvent(q); } } protected boolean shouldDropInputEvent(QueuedInputEvent q) { if (mView == null || !mAdded) { Slog.w(mTag, "Dropping event due to root view being removed: " + q.mEvent); return true; } else if ((!mAttachInfo.mHasWindowFocus && !q.mEvent.isFromSource(InputDevice.SOURCE_CLASS_POINTER)) || mStopped || (mIsAmbientMode && !q.mEvent.isFromSource(InputDevice.SOURCE_CLASS_BUTTON)) || (mPausedForTransition && !isBack(q.mEvent))) { // This is a focus event and the window doesn't currently have input focus or // has stopped. This could be an event that came back from the previous stage // but the window has lost focus or stopped in the meantime. if (isTerminalInputEvent(q.mEvent)) { // Don't drop terminal input events, however mark them as canceled. q.mEvent.cancel(); Slog.w(mTag, "Cancelling event due to no window focus: " + q.mEvent); return false; } // Drop non-terminal input events. Slog.w(mTag, "Dropping event due to no window focus: " + q.mEvent); return true; } return false; } void dump(String prefix, PrintWriter writer) { if (mNext != null) { mNext.dump(prefix, writer); } } private boolean isBack(InputEvent event) { if (event instanceof KeyEvent) { return ((KeyEvent) event).getKeyCode() == KeyEvent.KEYCODE_BACK; } else { return false; } } } /** * Base class for implementing an input pipeline stage that supports * asynchronous and out-of-order processing of input events. ** In addition to what a normal input stage can do, an asynchronous * input stage may also defer an input event that has been delivered to it * and finish or forward it later. *
*/ abstract class AsyncInputStage extends InputStage { private final String mTraceCounter; private QueuedInputEvent mQueueHead; private QueuedInputEvent mQueueTail; private int mQueueLength; protected static final int DEFER = 3; /** * Creates an asynchronous input stage. * @param next The next stage to which events should be forwarded. * @param traceCounter The name of a counter to record the size of * the queue of pending events. */ public AsyncInputStage(InputStage next, String traceCounter) { super(next); mTraceCounter = traceCounter; } /** * Marks the event as deferred, which is to say that it will be handled * asynchronously. The caller is responsible for calling {@link #forward} * or {@link #finish} later when it is done handling the event. */ protected void defer(QueuedInputEvent q) { q.mFlags |= QueuedInputEvent.FLAG_DEFERRED; enqueue(q); } @Override protected void forward(QueuedInputEvent q) { // Clear the deferred flag. q.mFlags &= ~QueuedInputEvent.FLAG_DEFERRED; // Fast path if the queue is empty. QueuedInputEvent curr = mQueueHead; if (curr == null) { super.forward(q); return; } // Determine whether the event must be serialized behind any others // before it can be delivered to the next stage. This is done because // deferred events might be handled out of order by the stage. final int deviceId = q.mEvent.getDeviceId(); QueuedInputEvent prev = null; boolean blocked = false; while (curr != null && curr != q) { if (!blocked && deviceId == curr.mEvent.getDeviceId()) { blocked = true; } prev = curr; curr = curr.mNext; } // If the event is blocked, then leave it in the queue to be delivered later. // Note that the event might not yet be in the queue if it was not previously // deferred so we will enqueue it if needed. if (blocked) { if (curr == null) { enqueue(q); } return; } // The event is not blocked. Deliver it immediately. if (curr != null) { curr = curr.mNext; dequeue(q, prev); } super.forward(q); // Dequeuing this event may have unblocked successors. Deliver them. while (curr != null) { if (deviceId == curr.mEvent.getDeviceId()) { if ((curr.mFlags & QueuedInputEvent.FLAG_DEFERRED) != 0) { break; } QueuedInputEvent next = curr.mNext; dequeue(curr, prev); super.forward(curr); curr = next; } else { prev = curr; curr = curr.mNext; } } } @Override protected void apply(QueuedInputEvent q, int result) { if (result == DEFER) { defer(q); } else { super.apply(q, result); } } private void enqueue(QueuedInputEvent q) { if (mQueueTail == null) { mQueueHead = q; mQueueTail = q; } else { mQueueTail.mNext = q; mQueueTail = q; } mQueueLength += 1; Trace.traceCounter(Trace.TRACE_TAG_INPUT, mTraceCounter, mQueueLength); } private void dequeue(QueuedInputEvent q, QueuedInputEvent prev) { if (prev == null) { mQueueHead = q.mNext; } else { prev.mNext = q.mNext; } if (mQueueTail == q) { mQueueTail = prev; } q.mNext = null; mQueueLength -= 1; Trace.traceCounter(Trace.TRACE_TAG_INPUT, mTraceCounter, mQueueLength); } @Override void dump(String prefix, PrintWriter writer) { writer.print(prefix); writer.print(getClass().getName()); writer.print(": mQueueLength="); writer.println(mQueueLength); super.dump(prefix, writer); } } /** * Delivers pre-ime input events to a native activity. * Does not support pointer events. */ final class NativePreImeInputStage extends AsyncInputStage implements InputQueue.FinishedInputEventCallback { public NativePreImeInputStage(InputStage next, String traceCounter) { super(next, traceCounter); } @Override protected int onProcess(QueuedInputEvent q) { if (mInputQueue != null && q.mEvent instanceof KeyEvent) { mInputQueue.sendInputEvent(q.mEvent, q, true, this); return DEFER; } return FORWARD; } @Override public void onFinishedInputEvent(Object token, boolean handled) { QueuedInputEvent q = (QueuedInputEvent)token; if (handled) { finish(q, true); return; } forward(q); } } /** * Delivers pre-ime input events to the view hierarchy. * Does not support pointer events. */ final class ViewPreImeInputStage extends InputStage { public ViewPreImeInputStage(InputStage next) { super(next); } @Override protected int onProcess(QueuedInputEvent q) { if (q.mEvent instanceof KeyEvent) { return processKeyEvent(q); } return FORWARD; } private int processKeyEvent(QueuedInputEvent q) { final KeyEvent event = (KeyEvent)q.mEvent; if (mView.dispatchKeyEventPreIme(event)) { return FINISH_HANDLED; } return FORWARD; } } /** * Delivers input events to the ime. * Does not support pointer events. */ final class ImeInputStage extends AsyncInputStage implements InputMethodManager.FinishedInputEventCallback { public ImeInputStage(InputStage next, String traceCounter) { super(next, traceCounter); } @Override protected int onProcess(QueuedInputEvent q) { if (mLastWasImTarget && !isInLocalFocusMode()) { InputMethodManager imm = InputMethodManager.peekInstance(); if (imm != null) { final InputEvent event = q.mEvent; if (DEBUG_IMF) Log.v(mTag, "Sending input event to IME: " + event); int result = imm.dispatchInputEvent(event, q, this, mHandler); if (result == InputMethodManager.DISPATCH_HANDLED) { return FINISH_HANDLED; } else if (result == InputMethodManager.DISPATCH_NOT_HANDLED) { // The IME could not handle it, so skip along to the next InputStage return FORWARD; } else { return DEFER; // callback will be invoked later } } } return FORWARD; } @Override public void onFinishedInputEvent(Object token, boolean handled) { QueuedInputEvent q = (QueuedInputEvent)token; if (handled) { finish(q, true); return; } forward(q); } } /** * Performs early processing of post-ime input events. */ final class EarlyPostImeInputStage extends InputStage { public EarlyPostImeInputStage(InputStage next) { super(next); } @Override protected int onProcess(QueuedInputEvent q) { if (q.mEvent instanceof KeyEvent) { return processKeyEvent(q); } else { final int source = q.mEvent.getSource(); if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) { return processPointerEvent(q); } } return FORWARD; } private int processKeyEvent(QueuedInputEvent q) { final KeyEvent event = (KeyEvent)q.mEvent; // If the key's purpose is to exit touch mode then we consume it // and consider it handled. if (checkForLeavingTouchModeAndConsume(event)) { return FINISH_HANDLED; } // Make sure the fallback event policy sees all keys that will be // delivered to the view hierarchy. mFallbackEventHandler.preDispatchKeyEvent(event); return FORWARD; } private int processPointerEvent(QueuedInputEvent q) { final MotionEvent event = (MotionEvent)q.mEvent; // Translate the pointer event for compatibility, if needed. if (mTranslator != null) { mTranslator.translateEventInScreenToAppWindow(event); } // Enter touch mode on down or scroll. final int action = event.getAction(); if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_SCROLL) { ensureTouchMode(true); } // Offset the scroll position. if (mCurScrollY != 0) { event.offsetLocation(0, mCurScrollY); } // Remember the touch position for possible drag-initiation. if (event.isTouchEvent()) { mLastTouchPoint.x = event.getRawX(); mLastTouchPoint.y = event.getRawY(); mLastTouchSource = event.getSource(); } return FORWARD; } } /** * Delivers post-ime input events to a native activity. */ final class NativePostImeInputStage extends AsyncInputStage implements InputQueue.FinishedInputEventCallback { public NativePostImeInputStage(InputStage next, String traceCounter) { super(next, traceCounter); } @Override protected int onProcess(QueuedInputEvent q) { if (mInputQueue != null) { mInputQueue.sendInputEvent(q.mEvent, q, false, this); return DEFER; } return FORWARD; } @Override public void onFinishedInputEvent(Object token, boolean handled) { QueuedInputEvent q = (QueuedInputEvent)token; if (handled) { finish(q, true); return; } forward(q); } } /** * Delivers post-ime input events to the view hierarchy. */ final class ViewPostImeInputStage extends InputStage { public ViewPostImeInputStage(InputStage next) { super(next); } @Override protected int onProcess(QueuedInputEvent q) { if (q.mEvent instanceof KeyEvent) { return processKeyEvent(q); } else { final int source = q.mEvent.getSource(); if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) { return processPointerEvent(q); } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) { return processTrackballEvent(q); } else { return processGenericMotionEvent(q); } } } @Override protected void onDeliverToNext(QueuedInputEvent q) { if (mUnbufferedInputDispatch && q.mEvent instanceof MotionEvent && ((MotionEvent)q.mEvent).isTouchEvent() && isTerminalInputEvent(q.mEvent)) { mUnbufferedInputDispatch = false; scheduleConsumeBatchedInput(); } super.onDeliverToNext(q); } private int processKeyEvent(QueuedInputEvent q) { final KeyEvent event = (KeyEvent)q.mEvent; // Deliver the key to the view hierarchy. if (mView.dispatchKeyEvent(event)) { return FINISH_HANDLED; } if (shouldDropInputEvent(q)) { return FINISH_NOT_HANDLED; } // If the Control modifier is held, try to interpret the key as a shortcut. if (event.getAction() == KeyEvent.ACTION_DOWN && event.isCtrlPressed() && event.getRepeatCount() == 0 && !KeyEvent.isModifierKey(event.getKeyCode())) { if (mView.dispatchKeyShortcutEvent(event)) { return FINISH_HANDLED; } if (shouldDropInputEvent(q)) { return FINISH_NOT_HANDLED; } } // Apply the fallback event policy. if (mFallbackEventHandler.dispatchKeyEvent(event)) { return FINISH_HANDLED; } if (shouldDropInputEvent(q)) { return FINISH_NOT_HANDLED; } // Handle automatic focus changes. if (event.getAction() == KeyEvent.ACTION_DOWN) { int direction = 0; switch (event.getKeyCode()) { case KeyEvent.KEYCODE_DPAD_LEFT: if (event.hasNoModifiers()) { direction = View.FOCUS_LEFT; } break; case KeyEvent.KEYCODE_DPAD_RIGHT: if (event.hasNoModifiers()) { direction = View.FOCUS_RIGHT; } break; case KeyEvent.KEYCODE_DPAD_UP: if (event.hasNoModifiers()) { direction = View.FOCUS_UP; } break; case KeyEvent.KEYCODE_DPAD_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; } if (direction != 0) { View focused = mView.findFocus(); 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)); return FINISH_HANDLED; } } // Give the focused view a last chance to handle the dpad key. if (mView.dispatchUnhandledMove(focused, direction)) { return FINISH_HANDLED; } } else { // find the best view to give focus to in this non-touch-mode with no-focus View v = focusSearch(null, direction); if (v != null && v.requestFocus(direction)) { return FINISH_HANDLED; } } } } return FORWARD; } private int processPointerEvent(QueuedInputEvent q) { final MotionEvent event = (MotionEvent)q.mEvent; mAttachInfo.mUnbufferedDispatchRequested = false; final View eventTarget = (event.isFromSource(InputDevice.SOURCE_MOUSE) && mCapturingView != null) ? mCapturingView : mView; mAttachInfo.mHandlingPointerEvent = true; boolean handled = eventTarget.dispatchPointerEvent(event); maybeUpdatePointerIcon(event); mAttachInfo.mHandlingPointerEvent = false; if (mAttachInfo.mUnbufferedDispatchRequested && !mUnbufferedInputDispatch) { mUnbufferedInputDispatch = true; if (mConsumeBatchedInputScheduled) { scheduleConsumeBatchedInputImmediately(); } } return handled ? FINISH_HANDLED : FORWARD; } private void maybeUpdatePointerIcon(MotionEvent event) { if (event.getPointerCount() == 1 && event.isFromSource(InputDevice.SOURCE_MOUSE)) { if (event.getActionMasked() == MotionEvent.ACTION_HOVER_ENTER || event.getActionMasked() == MotionEvent.ACTION_HOVER_EXIT) { // Other apps or the window manager may change the icon type outside of // this app, therefore the icon type has to be reset on enter/exit event. mPointerIconType = PointerIcon.TYPE_NOT_SPECIFIED; } if (event.getActionMasked() != MotionEvent.ACTION_HOVER_EXIT) { if (!updatePointerIcon(event) && event.getActionMasked() == MotionEvent.ACTION_HOVER_MOVE) { mPointerIconType = PointerIcon.TYPE_NOT_SPECIFIED; } } } } private int processTrackballEvent(QueuedInputEvent q) { final MotionEvent event = (MotionEvent)q.mEvent; if (mView.dispatchTrackballEvent(event)) { return FINISH_HANDLED; } return FORWARD; } private int processGenericMotionEvent(QueuedInputEvent q) { final MotionEvent event = (MotionEvent)q.mEvent; // Deliver the event to the view. if (mView.dispatchGenericMotionEvent(event)) { return FINISH_HANDLED; } return FORWARD; } } private void resetPointerIcon(MotionEvent event) { mPointerIconType = PointerIcon.TYPE_NOT_SPECIFIED; updatePointerIcon(event); } private boolean updatePointerIcon(MotionEvent event) { final int pointerIndex = 0; final float x = event.getX(pointerIndex); final float y = event.getY(pointerIndex); if (mView == null) { // E.g. click outside a popup to dismiss it Slog.d(mTag, "updatePointerIcon called after view was removed"); return false; } if (x < 0 || x >= mView.getWidth() || y < 0 || y >= mView.getHeight()) { // E.g. when moving window divider with mouse Slog.d(mTag, "updatePointerIcon called with position out of bounds"); return false; } final PointerIcon pointerIcon = mView.onResolvePointerIcon(event, pointerIndex); final int pointerType = (pointerIcon != null) ? pointerIcon.getType() : PointerIcon.TYPE_DEFAULT; if (mPointerIconType != pointerType) { mPointerIconType = pointerType; if (mPointerIconType != PointerIcon.TYPE_CUSTOM) { mCustomPointerIcon = null; InputManager.getInstance().setPointerIconType(pointerType); return true; } } if (mPointerIconType == PointerIcon.TYPE_CUSTOM && !pointerIcon.equals(mCustomPointerIcon)) { mCustomPointerIcon = pointerIcon; InputManager.getInstance().setCustomPointerIcon(mCustomPointerIcon); } return true; } /** * Performs synthesis of new input events from unhandled input events. */ final class SyntheticInputStage extends InputStage { private final SyntheticTrackballHandler mTrackball = new SyntheticTrackballHandler(); private final SyntheticJoystickHandler mJoystick = new SyntheticJoystickHandler(); private final SyntheticTouchNavigationHandler mTouchNavigation = new SyntheticTouchNavigationHandler(); private final SyntheticKeyboardHandler mKeyboard = new SyntheticKeyboardHandler(); public SyntheticInputStage() { super(null); } @Override protected int onProcess(QueuedInputEvent q) { q.mFlags |= QueuedInputEvent.FLAG_RESYNTHESIZED; if (q.mEvent instanceof MotionEvent) { final MotionEvent event = (MotionEvent)q.mEvent; final int source = event.getSource(); if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) { mTrackball.process(event); return FINISH_HANDLED; } else if ((source & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) { mJoystick.process(event); return FINISH_HANDLED; } else if ((source & InputDevice.SOURCE_TOUCH_NAVIGATION) == InputDevice.SOURCE_TOUCH_NAVIGATION) { mTouchNavigation.process(event); return FINISH_HANDLED; } } else if ((q.mFlags & QueuedInputEvent.FLAG_UNHANDLED) != 0) { mKeyboard.process((KeyEvent)q.mEvent); return FINISH_HANDLED; } return FORWARD; } @Override protected void onDeliverToNext(QueuedInputEvent q) { if ((q.mFlags & QueuedInputEvent.FLAG_RESYNTHESIZED) == 0) { // Cancel related synthetic events if any prior stage has handled the event. if (q.mEvent instanceof MotionEvent) { final MotionEvent event = (MotionEvent)q.mEvent; final int source = event.getSource(); if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) { mTrackball.cancel(event); } else if ((source & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) { mJoystick.cancel(event); } else if ((source & InputDevice.SOURCE_TOUCH_NAVIGATION) == InputDevice.SOURCE_TOUCH_NAVIGATION) { mTouchNavigation.cancel(event); } } } super.onDeliverToNext(q); } } /** * Creates dpad events from unhandled trackball movements. */ final class SyntheticTrackballHandler { private final TrackballAxis mX = new TrackballAxis(); private final TrackballAxis mY = new TrackballAxis(); private long mLastTime; public void process(MotionEvent event) { // Translate the trackball event into DPAD keys and try to deliver those. long curTime = SystemClock.uptimeMillis(); if ((mLastTime + MAX_TRACKBALL_DELAY) < curTime) { // It has been too long since the last movement, // so restart at the beginning. mX.reset(0); mY.reset(0); mLastTime = curTime; } final int action = event.getAction(); final int metaState = event.getMetaState(); switch (action) { case MotionEvent.ACTION_DOWN: mX.reset(2); mY.reset(2); enqueueInputEvent(new KeyEvent(curTime, curTime, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_CENTER, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_FALLBACK, InputDevice.SOURCE_KEYBOARD)); break; case MotionEvent.ACTION_UP: mX.reset(2); mY.reset(2); enqueueInputEvent(new KeyEvent(curTime, curTime, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DPAD_CENTER, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_FALLBACK, InputDevice.SOURCE_KEYBOARD)); break; } if (DEBUG_TRACKBALL) Log.v(mTag, "TB X=" + mX.position + " step=" + mX.step + " dir=" + mX.dir + " acc=" + mX.acceleration + " move=" + event.getX() + " / Y=" + mY.position + " step=" + mY.step + " dir=" + mY.dir + " acc=" + mY.acceleration + " move=" + event.getY()); final float xOff = mX.collect(event.getX(), event.getEventTime(), "X"); final float yOff = mY.collect(event.getY(), event.getEventTime(), "Y"); // Generate DPAD events based on the trackball movement. // We pick the axis that has moved the most as the direction of // the DPAD. When we generate DPAD events for one axis, then the // other axis is reset -- we don't want to perform DPAD jumps due // to slight movements in the trackball when making major movements // along the other axis. int keycode = 0; int movement = 0; float accel = 1; if (xOff > yOff) { movement = mX.generate(); if (movement != 0) { keycode = movement > 0 ? KeyEvent.KEYCODE_DPAD_RIGHT : KeyEvent.KEYCODE_DPAD_LEFT; accel = mX.acceleration; mY.reset(2); } } else if (yOff > 0) { movement = mY.generate(); if (movement != 0) { keycode = movement > 0 ? KeyEvent.KEYCODE_DPAD_DOWN : KeyEvent.KEYCODE_DPAD_UP; accel = mY.acceleration; mX.reset(2); } } if (keycode != 0) { if (movement < 0) movement = -movement; int accelMovement = (int)(movement * accel); if (DEBUG_TRACKBALL) Log.v(mTag, "Move: movement=" + movement + " accelMovement=" + accelMovement + " accel=" + accel); if (accelMovement > movement) { if (DEBUG_TRACKBALL) Log.v(mTag, "Delivering fake DPAD: " + keycode); movement--; int repeatCount = accelMovement - movement; enqueueInputEvent(new KeyEvent(curTime, curTime, KeyEvent.ACTION_MULTIPLE, keycode, repeatCount, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_FALLBACK, InputDevice.SOURCE_KEYBOARD)); } while (movement > 0) { if (DEBUG_TRACKBALL) Log.v(mTag, "Delivering fake DPAD: " + keycode); movement--; curTime = SystemClock.uptimeMillis(); enqueueInputEvent(new KeyEvent(curTime, curTime, KeyEvent.ACTION_DOWN, keycode, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_FALLBACK, InputDevice.SOURCE_KEYBOARD)); enqueueInputEvent(new KeyEvent(curTime, curTime, KeyEvent.ACTION_UP, keycode, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_FALLBACK, InputDevice.SOURCE_KEYBOARD)); } mLastTime = curTime; } } public void cancel(MotionEvent event) { mLastTime = Integer.MIN_VALUE; // If we reach this, we consumed a trackball event. // Because we will not translate the trackball event into a key event, // touch mode will not exit, so we exit touch mode here. if (mView != null && mAdded) { ensureTouchMode(false); } } } /** * Maintains state information for a single trackball axis, generating * discrete (DPAD) movements based on raw trackball motion. */ static final class TrackballAxis { /** * The maximum amount of acceleration we will apply. */ static final float MAX_ACCELERATION = 20; /** * The maximum amount of time (in milliseconds) between events in order * for us to consider the user to be doing fast trackball movements, * and thus apply an acceleration. */ static final long FAST_MOVE_TIME = 150; /** * Scaling factor to the time (in milliseconds) between events to how * much to multiple/divide the current acceleration. When movement * is < FAST_MOVE_TIME this multiplies the acceleration; when > * FAST_MOVE_TIME it divides it. */ static final float ACCEL_MOVE_SCALING_FACTOR = (1.0f/40); static final float FIRST_MOVEMENT_THRESHOLD = 0.5f; static final float SECOND_CUMULATIVE_MOVEMENT_THRESHOLD = 2.0f; static final float SUBSEQUENT_INCREMENTAL_MOVEMENT_THRESHOLD = 1.0f; float position; float acceleration = 1; long lastMoveTime = 0; int step; int dir; int nonAccelMovement; void reset(int _step) { position = 0; acceleration = 1; lastMoveTime = 0; step = _step; dir = 0; } /** * Add trackball movement into the state. If the direction of movement * has been reversed, the state is reset before adding the * movement (so that you don't have to compensate for any previously * collected movement before see the result of the movement in the * new direction). * * @return Returns the absolute value of the amount of movement * collected so far. */ float collect(float off, long time, String axis) { long normTime; if (off > 0) { normTime = (long)(off * FAST_MOVE_TIME); if (dir < 0) { if (DEBUG_TRACKBALL) Log.v(TAG, axis + " reversed to positive!"); position = 0; step = 0; acceleration = 1; lastMoveTime = 0; } dir = 1; } else if (off < 0) { normTime = (long)((-off) * FAST_MOVE_TIME); if (dir > 0) { if (DEBUG_TRACKBALL) Log.v(TAG, axis + " reversed to negative!"); position = 0; step = 0; acceleration = 1; lastMoveTime = 0; } dir = -1; } else { normTime = 0; } // The number of milliseconds between each movement that is // considered "normal" and will not result in any acceleration // or deceleration, scaled by the offset we have here. if (normTime > 0) { long delta = time - lastMoveTime; lastMoveTime = time; float acc = acceleration; if (delta < normTime) { // The user is scrolling rapidly, so increase acceleration. float scale = (normTime-delta) * ACCEL_MOVE_SCALING_FACTOR; if (scale > 1) acc *= scale; if (DEBUG_TRACKBALL) Log.v(TAG, axis + " accelerate: off=" + off + " normTime=" + normTime + " delta=" + delta + " scale=" + scale + " acc=" + acc); acceleration = acc < MAX_ACCELERATION ? acc : MAX_ACCELERATION; } else { // The user is scrolling slowly, so decrease acceleration. float scale = (delta-normTime) * ACCEL_MOVE_SCALING_FACTOR; if (scale > 1) acc /= scale; if (DEBUG_TRACKBALL) Log.v(TAG, axis + " deccelerate: off=" + off + " normTime=" + normTime + " delta=" + delta + " scale=" + scale + " acc=" + acc); acceleration = acc > 1 ? acc : 1; } } position += off; return Math.abs(position); } /** * Generate the number of discrete movement events appropriate for * the currently collected trackball movement. * * @return Returns the number of discrete movements, either positive * or negative, or 0 if there is not enough trackball movement yet * for a discrete movement. */ int generate() { int movement = 0; nonAccelMovement = 0; do { final int dir = position >= 0 ? 1 : -1; switch (step) { // If we are going to execute the first step, then we want // to do this as soon as possible instead of waiting for // a full movement, in order to make things look responsive. case 0: if (Math.abs(position) < FIRST_MOVEMENT_THRESHOLD) { return movement; } movement += dir; nonAccelMovement += dir; step = 1; break; // If we have generated the first movement, then we need // to wait for the second complete trackball motion before // generating the second discrete movement. case 1: if (Math.abs(position) < SECOND_CUMULATIVE_MOVEMENT_THRESHOLD) { return movement; } movement += dir; nonAccelMovement += dir; position -= SECOND_CUMULATIVE_MOVEMENT_THRESHOLD * dir; step = 2; break; // After the first two, we generate discrete movements // consistently with the trackball, applying an acceleration // if the trackball is moving quickly. This is a simple // acceleration on top of what we already compute based // on how quickly the wheel is being turned, to apply // a longer increasing acceleration to continuous movement // in one direction. default: if (Math.abs(position) < SUBSEQUENT_INCREMENTAL_MOVEMENT_THRESHOLD) { return movement; } movement += dir; position -= dir * SUBSEQUENT_INCREMENTAL_MOVEMENT_THRESHOLD; float acc = acceleration; acc *= 1.1f; acceleration = acc < MAX_ACCELERATION ? acc : acceleration; break; } } while (true); } } /** * Creates dpad events from unhandled joystick movements. */ final class SyntheticJoystickHandler extends Handler { private final static String TAG = "SyntheticJoystickHandler"; private final static int MSG_ENQUEUE_X_AXIS_KEY_REPEAT = 1; private final static int MSG_ENQUEUE_Y_AXIS_KEY_REPEAT = 2; private int mLastXDirection; private int mLastYDirection; private int mLastXKeyCode; private int mLastYKeyCode; public SyntheticJoystickHandler() { super(true); } @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_ENQUEUE_X_AXIS_KEY_REPEAT: case MSG_ENQUEUE_Y_AXIS_KEY_REPEAT: { KeyEvent oldEvent = (KeyEvent)msg.obj; KeyEvent e = KeyEvent.changeTimeRepeat(oldEvent, SystemClock.uptimeMillis(), oldEvent.getRepeatCount() + 1); if (mAttachInfo.mHasWindowFocus) { enqueueInputEvent(e); Message m = obtainMessage(msg.what, e); m.setAsynchronous(true); sendMessageDelayed(m, ViewConfiguration.getKeyRepeatDelay()); } } break; } } public void process(MotionEvent event) { switch(event.getActionMasked()) { case MotionEvent.ACTION_CANCEL: cancel(event); break; case MotionEvent.ACTION_MOVE: update(event, true); break; default: Log.w(mTag, "Unexpected action: " + event.getActionMasked()); } } private void cancel(MotionEvent event) { removeMessages(MSG_ENQUEUE_X_AXIS_KEY_REPEAT); removeMessages(MSG_ENQUEUE_Y_AXIS_KEY_REPEAT); update(event, false); } private void update(MotionEvent event, boolean synthesizeNewKeys) { final long time = event.getEventTime(); final int metaState = event.getMetaState(); final int deviceId = event.getDeviceId(); final int source = event.getSource(); int xDirection = joystickAxisValueToDirection( event.getAxisValue(MotionEvent.AXIS_HAT_X)); if (xDirection == 0) { xDirection = joystickAxisValueToDirection(event.getX()); } int yDirection = joystickAxisValueToDirection( event.getAxisValue(MotionEvent.AXIS_HAT_Y)); if (yDirection == 0) { yDirection = joystickAxisValueToDirection(event.getY()); } if (xDirection != mLastXDirection) { if (mLastXKeyCode != 0) { removeMessages(MSG_ENQUEUE_X_AXIS_KEY_REPEAT); enqueueInputEvent(new KeyEvent(time, time, KeyEvent.ACTION_UP, mLastXKeyCode, 0, metaState, deviceId, 0, KeyEvent.FLAG_FALLBACK, source)); mLastXKeyCode = 0; } mLastXDirection = xDirection; if (xDirection != 0 && synthesizeNewKeys) { mLastXKeyCode = xDirection > 0 ? KeyEvent.KEYCODE_DPAD_RIGHT : KeyEvent.KEYCODE_DPAD_LEFT; final KeyEvent e = new KeyEvent(time, time, KeyEvent.ACTION_DOWN, mLastXKeyCode, 0, metaState, deviceId, 0, KeyEvent.FLAG_FALLBACK, source); enqueueInputEvent(e); Message m = obtainMessage(MSG_ENQUEUE_X_AXIS_KEY_REPEAT, e); m.setAsynchronous(true); sendMessageDelayed(m, ViewConfiguration.getKeyRepeatTimeout()); } } if (yDirection != mLastYDirection) { if (mLastYKeyCode != 0) { removeMessages(MSG_ENQUEUE_Y_AXIS_KEY_REPEAT); enqueueInputEvent(new KeyEvent(time, time, KeyEvent.ACTION_UP, mLastYKeyCode, 0, metaState, deviceId, 0, KeyEvent.FLAG_FALLBACK, source)); mLastYKeyCode = 0; } mLastYDirection = yDirection; if (yDirection != 0 && synthesizeNewKeys) { mLastYKeyCode = yDirection > 0 ? KeyEvent.KEYCODE_DPAD_DOWN : KeyEvent.KEYCODE_DPAD_UP; final KeyEvent e = new KeyEvent(time, time, KeyEvent.ACTION_DOWN, mLastYKeyCode, 0, metaState, deviceId, 0, KeyEvent.FLAG_FALLBACK, source); enqueueInputEvent(e); Message m = obtainMessage(MSG_ENQUEUE_Y_AXIS_KEY_REPEAT, e); m.setAsynchronous(true); sendMessageDelayed(m, ViewConfiguration.getKeyRepeatTimeout()); } } } private int joystickAxisValueToDirection(float value) { if (value >= 0.5f) { return 1; } else if (value <= -0.5f) { return -1; } else { return 0; } } } /** * Creates dpad events from unhandled touch navigation movements. */ final class SyntheticTouchNavigationHandler extends Handler { private static final String LOCAL_TAG = "SyntheticTouchNavigationHandler"; private static final boolean LOCAL_DEBUG = false; // Assumed nominal width and height in millimeters of a touch navigation pad, // if no resolution information is available from the input system. private static final float DEFAULT_WIDTH_MILLIMETERS = 48; private static final float DEFAULT_HEIGHT_MILLIMETERS = 48; /* TODO: These constants should eventually be moved to ViewConfiguration. */ // The nominal distance traveled to move by one unit. private static final int TICK_DISTANCE_MILLIMETERS = 12; // Minimum and maximum fling velocity in ticks per second. // The minimum velocity should be set such that we perform enough ticks per // second that the fling appears to be fluid. For example, if we set the minimum // to 2 ticks per second, then there may be up to half a second delay between the next // to last and last ticks which is noticeably discrete and jerky. This value should // probably not be set to anything less than about 4. // If fling accuracy is a problem then consider tuning the tick distance instead. private static final float MIN_FLING_VELOCITY_TICKS_PER_SECOND = 6f; private static final float MAX_FLING_VELOCITY_TICKS_PER_SECOND = 20f; // Fling velocity decay factor applied after each new key is emitted. // This parameter controls the deceleration and overall duration of the fling. // The fling stops automatically when its velocity drops below the minimum // fling velocity defined above. private static final float FLING_TICK_DECAY = 0.8f; /* The input device that we are tracking. */ private int mCurrentDeviceId = -1; private int mCurrentSource; private boolean mCurrentDeviceSupported; /* Configuration for the current input device. */ // The scaled tick distance. A movement of this amount should generally translate // into a single dpad event in a given direction. private float mConfigTickDistance; // The minimum and maximum scaled fling velocity. private float mConfigMinFlingVelocity; private float mConfigMaxFlingVelocity; /* Tracking state. */ // The velocity tracker for detecting flings. private VelocityTracker mVelocityTracker; // The active pointer id, or -1 if none. private int mActivePointerId = -1; // Location where tracking started. private float mStartX; private float mStartY; // Most recently observed position. private float mLastX; private float mLastY; // Accumulated movement delta since the last direction key was sent. private float mAccumulatedX; private float mAccumulatedY; // Set to true if any movement was delivered to the app. // Implies that tap slop was exceeded. private boolean mConsumedMovement; // The most recently sent key down event. // The keycode remains set until the direction changes or a fling ends // so that repeated key events may be generated as required. private long mPendingKeyDownTime; private int mPendingKeyCode = KeyEvent.KEYCODE_UNKNOWN; private int mPendingKeyRepeatCount; private int mPendingKeyMetaState; // The current fling velocity while a fling is in progress. private boolean mFlinging; private float mFlingVelocity; public SyntheticTouchNavigationHandler() { super(true); } public void process(MotionEvent event) { // Update the current device information. final long time = event.getEventTime(); final int deviceId = event.getDeviceId(); final int source = event.getSource(); if (mCurrentDeviceId != deviceId || mCurrentSource != source) { finishKeys(time); finishTracking(time); mCurrentDeviceId = deviceId; mCurrentSource = source; mCurrentDeviceSupported = false; InputDevice device = event.getDevice(); if (device != null) { // In order to support an input device, we must know certain // characteristics about it, such as its size and resolution. InputDevice.MotionRange xRange = device.getMotionRange(MotionEvent.AXIS_X); InputDevice.MotionRange yRange = device.getMotionRange(MotionEvent.AXIS_Y); if (xRange != null && yRange != null) { mCurrentDeviceSupported = true; // Infer the resolution if it not actually known. float xRes = xRange.getResolution(); if (xRes <= 0) { xRes = xRange.getRange() / DEFAULT_WIDTH_MILLIMETERS; } float yRes = yRange.getResolution(); if (yRes <= 0) { yRes = yRange.getRange() / DEFAULT_HEIGHT_MILLIMETERS; } float nominalRes = (xRes + yRes) * 0.5f; // Precompute all of the configuration thresholds we will need. mConfigTickDistance = TICK_DISTANCE_MILLIMETERS * nominalRes; mConfigMinFlingVelocity = MIN_FLING_VELOCITY_TICKS_PER_SECOND * mConfigTickDistance; mConfigMaxFlingVelocity = MAX_FLING_VELOCITY_TICKS_PER_SECOND * mConfigTickDistance; if (LOCAL_DEBUG) { Log.d(LOCAL_TAG, "Configured device " + mCurrentDeviceId + " (" + Integer.toHexString(mCurrentSource) + "): " + ", mConfigTickDistance=" + mConfigTickDistance + ", mConfigMinFlingVelocity=" + mConfigMinFlingVelocity + ", mConfigMaxFlingVelocity=" + mConfigMaxFlingVelocity); } } } } if (!mCurrentDeviceSupported) { return; } // Handle the event. final int action = event.getActionMasked(); switch (action) { case MotionEvent.ACTION_DOWN: { boolean caughtFling = mFlinging; finishKeys(time); finishTracking(time); mActivePointerId = event.getPointerId(0); mVelocityTracker = VelocityTracker.obtain(); mVelocityTracker.addMovement(event); mStartX = event.getX(); mStartY = event.getY(); mLastX = mStartX; mLastY = mStartY; mAccumulatedX = 0; mAccumulatedY = 0; // If we caught a fling, then pretend that the tap slop has already // been exceeded to suppress taps whose only purpose is to stop the fling. mConsumedMovement = caughtFling; break; } case MotionEvent.ACTION_MOVE: case MotionEvent.ACTION_UP: { if (mActivePointerId < 0) { break; } final int index = event.findPointerIndex(mActivePointerId); if (index < 0) { finishKeys(time); finishTracking(time); break; } mVelocityTracker.addMovement(event); final float x = event.getX(index); final float y = event.getY(index); mAccumulatedX += x - mLastX; mAccumulatedY += y - mLastY; mLastX = x; mLastY = y; // Consume any accumulated movement so far. final int metaState = event.getMetaState(); consumeAccumulatedMovement(time, metaState); // Detect taps and flings. if (action == MotionEvent.ACTION_UP) { if (mConsumedMovement && mPendingKeyCode != KeyEvent.KEYCODE_UNKNOWN) { // It might be a fling. mVelocityTracker.computeCurrentVelocity(1000, mConfigMaxFlingVelocity); final float vx = mVelocityTracker.getXVelocity(mActivePointerId); final float vy = mVelocityTracker.getYVelocity(mActivePointerId); if (!startFling(time, vx, vy)) { finishKeys(time); } } finishTracking(time); } break; } case MotionEvent.ACTION_CANCEL: { finishKeys(time); finishTracking(time); break; } } } public void cancel(MotionEvent event) { if (mCurrentDeviceId == event.getDeviceId() && mCurrentSource == event.getSource()) { final long time = event.getEventTime(); finishKeys(time); finishTracking(time); } } private void finishKeys(long time) { cancelFling(); sendKeyUp(time); } private void finishTracking(long time) { if (mActivePointerId >= 0) { mActivePointerId = -1; mVelocityTracker.recycle(); mVelocityTracker = null; } } private void consumeAccumulatedMovement(long time, int metaState) { final float absX = Math.abs(mAccumulatedX); final float absY = Math.abs(mAccumulatedY); if (absX >= absY) { if (absX >= mConfigTickDistance) { mAccumulatedX = consumeAccumulatedMovement(time, metaState, mAccumulatedX, KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.KEYCODE_DPAD_RIGHT); mAccumulatedY = 0; mConsumedMovement = true; } } else { if (absY >= mConfigTickDistance) { mAccumulatedY = consumeAccumulatedMovement(time, metaState, mAccumulatedY, KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_DPAD_DOWN); mAccumulatedX = 0; mConsumedMovement = true; } } } private float consumeAccumulatedMovement(long time, int metaState, float accumulator, int negativeKeyCode, int positiveKeyCode) { while (accumulator <= -mConfigTickDistance) { sendKeyDownOrRepeat(time, negativeKeyCode, metaState); accumulator += mConfigTickDistance; } while (accumulator >= mConfigTickDistance) { sendKeyDownOrRepeat(time, positiveKeyCode, metaState); accumulator -= mConfigTickDistance; } return accumulator; } private void sendKeyDownOrRepeat(long time, int keyCode, int metaState) { if (mPendingKeyCode != keyCode) { sendKeyUp(time); mPendingKeyDownTime = time; mPendingKeyCode = keyCode; mPendingKeyRepeatCount = 0; } else { mPendingKeyRepeatCount += 1; } mPendingKeyMetaState = metaState; // Note: Normally we would pass FLAG_LONG_PRESS when the repeat count is 1 // but it doesn't quite make sense when simulating the events in this way. if (LOCAL_DEBUG) { Log.d(LOCAL_TAG, "Sending key down: keyCode=" + mPendingKeyCode + ", repeatCount=" + mPendingKeyRepeatCount + ", metaState=" + Integer.toHexString(mPendingKeyMetaState)); } enqueueInputEvent(new KeyEvent(mPendingKeyDownTime, time, KeyEvent.ACTION_DOWN, mPendingKeyCode, mPendingKeyRepeatCount, mPendingKeyMetaState, mCurrentDeviceId, KeyEvent.FLAG_FALLBACK, mCurrentSource)); } private void sendKeyUp(long time) { if (mPendingKeyCode != KeyEvent.KEYCODE_UNKNOWN) { if (LOCAL_DEBUG) { Log.d(LOCAL_TAG, "Sending key up: keyCode=" + mPendingKeyCode + ", metaState=" + Integer.toHexString(mPendingKeyMetaState)); } enqueueInputEvent(new KeyEvent(mPendingKeyDownTime, time, KeyEvent.ACTION_UP, mPendingKeyCode, 0, mPendingKeyMetaState, mCurrentDeviceId, 0, KeyEvent.FLAG_FALLBACK, mCurrentSource)); mPendingKeyCode = KeyEvent.KEYCODE_UNKNOWN; } } private boolean startFling(long time, float vx, float vy) { if (LOCAL_DEBUG) { Log.d(LOCAL_TAG, "Considering fling: vx=" + vx + ", vy=" + vy + ", min=" + mConfigMinFlingVelocity); } // Flings must be oriented in the same direction as the preceding movements. switch (mPendingKeyCode) { case KeyEvent.KEYCODE_DPAD_LEFT: if (-vx >= mConfigMinFlingVelocity && Math.abs(vy) < mConfigMinFlingVelocity) { mFlingVelocity = -vx; break; } return false; case KeyEvent.KEYCODE_DPAD_RIGHT: if (vx >= mConfigMinFlingVelocity && Math.abs(vy) < mConfigMinFlingVelocity) { mFlingVelocity = vx; break; } return false; case KeyEvent.KEYCODE_DPAD_UP: if (-vy >= mConfigMinFlingVelocity && Math.abs(vx) < mConfigMinFlingVelocity) { mFlingVelocity = -vy; break; } return false; case KeyEvent.KEYCODE_DPAD_DOWN: if (vy >= mConfigMinFlingVelocity && Math.abs(vx) < mConfigMinFlingVelocity) { mFlingVelocity = vy; break; } return false; } // Post the first fling event. mFlinging = postFling(time); return mFlinging; } private boolean postFling(long time) { // The idea here is to estimate the time when the pointer would have // traveled one tick distance unit given the current fling velocity. // This effect creates continuity of motion. if (mFlingVelocity >= mConfigMinFlingVelocity) { long delay = (long)(mConfigTickDistance / mFlingVelocity * 1000); postAtTime(mFlingRunnable, time + delay); if (LOCAL_DEBUG) { Log.d(LOCAL_TAG, "Posted fling: velocity=" + mFlingVelocity + ", delay=" + delay + ", keyCode=" + mPendingKeyCode); } return true; } return false; } private void cancelFling() { if (mFlinging) { removeCallbacks(mFlingRunnable); mFlinging = false; } } private final Runnable mFlingRunnable = new Runnable() { @Override public void run() { final long time = SystemClock.uptimeMillis(); sendKeyDownOrRepeat(time, mPendingKeyCode, mPendingKeyMetaState); mFlingVelocity *= FLING_TICK_DECAY; if (!postFling(time)) { mFlinging = false; finishKeys(time); } } }; } final class SyntheticKeyboardHandler { public void process(KeyEvent event) { if ((event.getFlags() & KeyEvent.FLAG_FALLBACK) != 0) { return; } final KeyCharacterMap kcm = event.getKeyCharacterMap(); final int keyCode = event.getKeyCode(); final int metaState = event.getMetaState(); // Check for fallback actions specified by the key character map. KeyCharacterMap.FallbackAction fallbackAction = kcm.getFallbackAction(keyCode, metaState); if (fallbackAction != null) { final int flags = event.getFlags() | KeyEvent.FLAG_FALLBACK; KeyEvent fallbackEvent = KeyEvent.obtain( event.getDownTime(), event.getEventTime(), event.getAction(), fallbackAction.keyCode, event.getRepeatCount(), fallbackAction.metaState, event.getDeviceId(), event.getScanCode(), flags, event.getSource(), null); fallbackAction.recycle(); enqueueInputEvent(fallbackEvent); } } } /** * 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. */ 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 the key is used for typing. * @param keyEvent The key event. * @return True if the key is used for typing. */ 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). * @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) { // Only relevant in touch mode. if (!mAttachInfo.mInTouchMode) { return false; } // 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; } // 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; } /* drag/drop */ void setLocalDragState(Object obj) { mLocalDragState = obj; } private void handleDragEvent(DragEvent event) { // From the root, only drag start/end/location are dispatched. entered/exited // are determined and dispatched by the viewgroup hierarchy, who then report // that back here for ultimate reporting back to the framework. if (mView != null && mAdded) { final int what = event.mAction; // Cache the drag description when the operation starts, then fill it in // on subsequent calls as a convenience if (what == DragEvent.ACTION_DRAG_STARTED) { mCurrentDragView = null; // Start the current-recipient tracking mDragDescription = event.mClipDescription; } else { event.mClipDescription = mDragDescription; } if (what == DragEvent.ACTION_DRAG_EXITED) { // A direct EXITED event means that the window manager knows we've just crossed // a window boundary, so the current drag target within this one must have // just been exited. Send the EXITED notification to the current drag view, if any. if (View.sCascadedDragDrop) { mView.dispatchDragEnterExitInPreN(event); } setDragFocus(null, event); } else { // For events with a [screen] location, translate into window coordinates if ((what == DragEvent.ACTION_DRAG_LOCATION) || (what == DragEvent.ACTION_DROP)) { mDragPoint.set(event.mX, event.mY); if (mTranslator != null) { mTranslator.translatePointInScreenToAppWindow(mDragPoint); } if (mCurScrollY != 0) { mDragPoint.offset(0, mCurScrollY); } event.mX = mDragPoint.x; event.mY = mDragPoint.y; } // Remember who the current drag target is pre-dispatch final View prevDragView = mCurrentDragView; // Now dispatch the drag/drop event boolean result = mView.dispatchDragEvent(event); if (what == DragEvent.ACTION_DRAG_LOCATION && !event.mEventHandlerWasCalled) { // If the LOCATION event wasn't delivered to any handler, no view now has a drag // focus. setDragFocus(null, event); } // If we changed apparent drag target, tell the OS about it if (prevDragView != mCurrentDragView) { try { if (prevDragView != null) { mWindowSession.dragRecipientExited(mWindow); } if (mCurrentDragView != null) { mWindowSession.dragRecipientEntered(mWindow); } } catch (RemoteException e) { Slog.e(mTag, "Unable to note drag target change"); } } // Report the drop result when we're done if (what == DragEvent.ACTION_DROP) { mDragDescription = null; try { Log.i(mTag, "Reporting drop result: " + result); mWindowSession.reportDropResult(mWindow, result); } catch (RemoteException e) { Log.e(mTag, "Unable to report drop result"); } } // When the drag operation ends, reset drag-related state if (what == DragEvent.ACTION_DRAG_ENDED) { mCurrentDragView = null; setLocalDragState(null); mAttachInfo.mDragToken = null; if (mAttachInfo.mDragSurface != null) { mAttachInfo.mDragSurface.release(); mAttachInfo.mDragSurface = null; } } } } event.recycle(); } public void handleDispatchSystemUiVisibilityChanged(SystemUiVisibilityInfo args) { if (mSeq != args.seq) { // The sequence has changed, so we need to update our value and make // sure to do a traversal afterward so the window manager is given our // most recent data. mSeq = args.seq; mAttachInfo.mForceReportNewAttributes = true; scheduleTraversals(); } if (mView == null) return; if (args.localChanges != 0) { mView.updateLocalSystemUiVisibility(args.localValue, args.localChanges); } int visibility = args.globalVisibility&View.SYSTEM_UI_CLEARABLE_FLAGS; if (visibility != mAttachInfo.mGlobalSystemUiVisibility) { mAttachInfo.mGlobalSystemUiVisibility = visibility; mView.dispatchSystemUiVisibilityChanged(visibility); } } public void handleDispatchWindowShown() { mAttachInfo.mTreeObserver.dispatchOnWindowShown(); } public void handleRequestKeyboardShortcuts(IResultReceiver receiver, int deviceId) { Bundle data = new Bundle(); ArrayList
* This is necessary to get updated bounds after a position change.
*
* @param event an accessibility event of type
* {@link AccessibilityEvent#TYPE_WINDOW_CONTENT_CHANGED}
*/
private void handleWindowContentChangedEvent(AccessibilityEvent event) {
final View focusedHost = mAccessibilityFocusedHost;
if (focusedHost == null || mAccessibilityFocusedVirtualView == null) {
// No virtual view focused, nothing to do here.
return;
}
final AccessibilityNodeProvider provider = focusedHost.getAccessibilityNodeProvider();
if (provider == null) {
// Error state: virtual view with no provider. Clear focus.
mAccessibilityFocusedHost = null;
mAccessibilityFocusedVirtualView = null;
focusedHost.clearAccessibilityFocusNoCallbacks(0);
return;
}
// We only care about change types that may affect the bounds of the
// focused virtual view.
final int changes = event.getContentChangeTypes();
if ((changes & AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE) == 0
&& changes != AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED) {
return;
}
final long eventSourceNodeId = event.getSourceNodeId();
final int changedViewId = AccessibilityNodeInfo.getAccessibilityViewId(eventSourceNodeId);
// Search up the tree for subtree containment.
boolean hostInSubtree = false;
View root = mAccessibilityFocusedHost;
while (root != null && !hostInSubtree) {
if (changedViewId == root.getAccessibilityViewId()) {
hostInSubtree = true;
} else {
final ViewParent parent = root.getParent();
if (parent instanceof View) {
root = (View) parent;
} else {
root = null;
}
}
}
// We care only about changes in subtrees containing the host view.
if (!hostInSubtree) {
return;
}
final long focusedSourceNodeId = mAccessibilityFocusedVirtualView.getSourceNodeId();
int focusedChildId = AccessibilityNodeInfo.getVirtualDescendantId(focusedSourceNodeId);
if (focusedChildId == AccessibilityNodeInfo.UNDEFINED_ITEM_ID) {
// TODO: Should we clear the focused virtual view?
focusedChildId = AccessibilityNodeProvider.HOST_VIEW_ID;
}
// Refresh the node for the focused virtual view.
final Rect oldBounds = mTempRect;
mAccessibilityFocusedVirtualView.getBoundsInScreen(oldBounds);
mAccessibilityFocusedVirtualView = provider.createAccessibilityNodeInfo(focusedChildId);
if (mAccessibilityFocusedVirtualView == null) {
// Error state: The node no longer exists. Clear focus.
mAccessibilityFocusedHost = null;
focusedHost.clearAccessibilityFocusNoCallbacks(0);
// This will probably fail, but try to keep the provider's internal
// state consistent by clearing focus.
provider.performAction(focusedChildId,
AccessibilityAction.ACTION_CLEAR_ACCESSIBILITY_FOCUS.getId(), null);
invalidateRectOnScreen(oldBounds);
} else {
// The node was refreshed, invalidate bounds if necessary.
final Rect newBounds = mAccessibilityFocusedVirtualView.getBoundsInScreen();
if (!oldBounds.equals(newBounds)) {
oldBounds.union(newBounds);
invalidateRectOnScreen(oldBounds);
}
}
}
@Override
public void notifySubtreeAccessibilityStateChanged(View child, View source, int changeType) {
postSendWindowContentChangedCallback(source, changeType);
}
@Override
public boolean canResolveLayoutDirection() {
return true;
}
@Override
public boolean isLayoutDirectionResolved() {
return true;
}
@Override
public int getLayoutDirection() {
return View.LAYOUT_DIRECTION_RESOLVED_DEFAULT;
}
@Override
public boolean canResolveTextDirection() {
return true;
}
@Override
public boolean isTextDirectionResolved() {
return true;
}
@Override
public int getTextDirection() {
return View.TEXT_DIRECTION_RESOLVED_DEFAULT;
}
@Override
public boolean canResolveTextAlignment() {
return true;
}
@Override
public boolean isTextAlignmentResolved() {
return true;
}
@Override
public int getTextAlignment() {
return View.TEXT_ALIGNMENT_RESOLVED_DEFAULT;
}
private View getCommonPredecessor(View first, View second) {
if (mTempHashSet == null) {
mTempHashSet = new HashSet
* This method is only supposed to be used to speed up the interaction from SystemUI and window
* manager when waiting for the first frame to be drawn when turning on the screen. DO NOT USE
* unless you fully understand this interaction.
* @hide
*/
public void setReportNextDraw() {
mReportNextDraw = true;
invalidate();
}
void changeCanvasOpacity(boolean opaque) {
Log.d(mTag, "changeCanvasOpacity: opaque=" + opaque);
if (mAttachInfo.mHardwareRenderer != null) {
mAttachInfo.mHardwareRenderer.setOpaque(opaque);
}
}
class TakenSurfaceHolder extends BaseSurfaceHolder {
@Override
public boolean onAllowLockCanvas() {
return mDrawingAllowed;
}
@Override
public void onRelayoutContainer() {
// Not currently interesting -- from changing between fixed and layout size.
}
@Override
public void setFormat(int format) {
((RootViewSurfaceTaker)mView).setSurfaceFormat(format);
}
@Override
public void setType(int type) {
((RootViewSurfaceTaker)mView).setSurfaceType(type);
}
@Override
public void onUpdateSurface() {
// We take care of format and type changes on our own.
throw new IllegalStateException("Shouldn't be here");
}
@Override
public boolean isCreating() {
return mIsCreating;
}
@Override
public void setFixedSize(int width, int height) {
throw new UnsupportedOperationException(
"Currently only support sizing from layout");
}
@Override
public void setKeepScreenOn(boolean screenOn) {
((RootViewSurfaceTaker)mView).setSurfaceKeepScreenOn(screenOn);
}
}
static class W extends IWindow.Stub {
private final WeakReference