diff options
Diffstat (limited to 'core/java/android/view')
27 files changed, 1747 insertions, 909 deletions
diff --git a/core/java/android/view/FocusFinder.java b/core/java/android/view/FocusFinder.java index 4048763..15fb839 100644 --- a/core/java/android/view/FocusFinder.java +++ b/core/java/android/view/FocusFinder.java @@ -397,7 +397,7 @@ public class FocusFinder { int numTouchables = touchables.size(); - int edgeSlop = ViewConfiguration.getEdgeSlop(); + int edgeSlop = ViewConfiguration.get(root.mContext).getScaledEdgeSlop(); Rect closestBounds = new Rect(); Rect touchableBounds = mOtherRect; diff --git a/core/java/android/view/GestureDetector.java b/core/java/android/view/GestureDetector.java index fc9af05..a472689 100644 --- a/core/java/android/view/GestureDetector.java +++ b/core/java/android/view/GestureDetector.java @@ -18,6 +18,7 @@ package android.view; import android.os.Handler; import android.os.Message; +import android.content.Context; /** * Detects various gestures and events using the supplied {@link MotionEvent}s. @@ -34,7 +35,6 @@ import android.os.Message; * </ul> */ public class GestureDetector { - /** * The listener that is used to notify when gestures occur. * If you want to listen for all the different gestures then implement @@ -113,6 +113,14 @@ public class GestureDetector { } /** + * @hide pending API council + */ + public interface OnDoubleTapListener { + boolean onSingleTapConfirmed(MotionEvent e); + boolean onDoubleTapEvent(MotionEvent e); + } + + /** * A convenience class to extend when you only want to listen for a * subset of all the gestures. This implements all methods in the * {@link OnGestureListener} but does nothing and return {@code false} @@ -144,22 +152,40 @@ public class GestureDetector { } } - private static final int TOUCH_SLOP_SQUARE = ViewConfiguration.getTouchSlop() - * ViewConfiguration.getTouchSlop(); - + // TODO: ViewConfiguration + private int mBiggerTouchSlopSquare = 20 * 20; + + private int mTouchSlopSquare; + private int mDoubleTapSlopSquare; + private int mMinimumFlingVelocity; + + private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout(); + private static final int TAP_TIMEOUT = ViewConfiguration.getTapTimeout(); + // TODO make new double-tap timeout, and define its events (i.e. either time + // between down-down or time between up-down) + private static final int DOUBLE_TAP_TIMEOUT = ViewConfiguration.getJumpTapTimeout(); + // constants for Message.what used by GestureHandler below private static final int SHOW_PRESS = 1; private static final int LONG_PRESS = 2; + private static final int TAP = 3; private final Handler mHandler; private final OnGestureListener mListener; + private OnDoubleTapListener mDoubleTapListener; private boolean mInLongPress; private boolean mAlwaysInTapRegion; + private boolean mAlwaysInBiggerTapRegion; private MotionEvent mCurrentDownEvent; - private MotionEvent mCurrentUpEvent; - + + /** + * True when the user is still touching for the second tap (down, move, and + * up events). Can only be true if there is a double tap listener attached. + */ + private boolean mIsDoubleTapping; + private float mLastMotionY; private float mLastMotionX; @@ -189,6 +215,12 @@ public class GestureDetector { case LONG_PRESS: dispatchLongPress(); break; + + case TAP: + if (mDoubleTapListener != null) { + mDoubleTapListener.onSingleTapConfirmed(mCurrentDownEvent); + } + break; default: throw new RuntimeException("Unknown message " + msg); //never @@ -203,16 +235,17 @@ public class GestureDetector { * * @param listener the listener invoked for all the callbacks, this must * not be null. - * @param handler the handler to use, this must - * not be null. + * @param handler the handler to use * * @throws NullPointerException if either {@code listener} or * {@code handler} is null. + * + * @deprecated Use {@link #GestureDetector(android.content.Context, + * android.view.GestureDetector.OnGestureListener, android.os.Handler)} instead. */ + @Deprecated public GestureDetector(OnGestureListener listener, Handler handler) { - mHandler = new GestureHandler(handler); - mListener = listener; - init(); + this(null, listener, handler); } /** @@ -222,19 +255,84 @@ public class GestureDetector { * * @param listener the listener invoked for all the callbacks, this must * not be null. + * * @throws NullPointerException if {@code listener} is null. + * + * @deprecated Use {@link #GestureDetector(android.content.Context, + * android.view.GestureDetector.OnGestureListener)} instead. */ + @Deprecated public GestureDetector(OnGestureListener listener) { - mHandler = new GestureHandler(); + this(null, listener, null); + } + + /** + * Creates a GestureDetector with the supplied listener. + * You may only use this constructor from a UI thread (this is the usual situation). + * @see android.os.Handler#Handler() + * + * @param context the application's context + * @param listener the listener invoked for all the callbacks, this must + * not be null. + * + * @throws NullPointerException if {@code listener} is null. + */ + public GestureDetector(Context context, OnGestureListener listener) { + this(context, listener, null); + } + + /** + * Creates a GestureDetector with the supplied listener. + * You may only use this constructor from a UI thread (this is the usual situation). + * @see android.os.Handler#Handler() + * + * @param context the application's context + * @param listener the listener invoked for all the callbacks, this must + * not be null. + * @param handler the handler to use + * + * @throws NullPointerException if {@code listener} is null. + */ + public GestureDetector(Context context, OnGestureListener listener, Handler handler) { + if (handler != null) { + mHandler = new GestureHandler(handler); + } else { + mHandler = new GestureHandler(); + } mListener = listener; - init(); + init(context); } - private void init() { + private void init(Context context) { if (mListener == null) { throw new NullPointerException("OnGestureListener must not be null"); } mIsLongpressEnabled = true; + + // Fallback to support pre-donuts releases + int touchSlop, doubleTapSlop; + if (context == null) { + //noinspection deprecation + touchSlop = ViewConfiguration.getTouchSlop(); + doubleTapSlop = ViewConfiguration.getDoubleTapSlop(); + //noinspection deprecation + mMinimumFlingVelocity = ViewConfiguration.getMinimumFlingVelocity(); + } else { + final ViewConfiguration configuration = ViewConfiguration.get(context); + touchSlop = configuration.getScaledTouchSlop(); + doubleTapSlop = configuration.getScaledDoubleTapSlop(); + mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity(); + } + mTouchSlopSquare = touchSlop * touchSlop; + mDoubleTapSlopSquare = doubleTapSlop * doubleTapSlop; + } + + /** + * @hide pending API council + * @param onDoubleTapListener + */ + public void setOnDoubleTapListener(OnDoubleTapListener onDoubleTapListener) { + mDoubleTapListener = onDoubleTapListener; } /** @@ -266,9 +364,6 @@ public class GestureDetector { * else false. */ public boolean onTouchEvent(MotionEvent ev) { - final long tapTime = ViewConfiguration.getTapTimeout(); - final long longpressTime = ViewConfiguration.getLongPressTimeout(); - final int touchSlop = ViewConfiguration.getTouchSlop(); final int action = ev.getAction(); final float y = ev.getY(); final float x = ev.getX(); @@ -282,19 +377,32 @@ public class GestureDetector { switch (action) { case MotionEvent.ACTION_DOWN: + if (mDoubleTapListener != null) { + mHandler.removeMessages(TAP); + if (mCurrentDownEvent != null && isConsideredDoubleTap(mCurrentDownEvent, ev)) { + // This is a second tap + mIsDoubleTapping = true; + handled = mDoubleTapListener.onDoubleTapEvent(ev); + } else { + // This is a first tap + mHandler.sendEmptyMessageDelayed(TAP, DOUBLE_TAP_TIMEOUT); + } + } + mLastMotionX = x; mLastMotionY = y; mCurrentDownEvent = MotionEvent.obtain(ev); mAlwaysInTapRegion = true; + mAlwaysInBiggerTapRegion = true; mInLongPress = false; - + if (mIsLongpressEnabled) { mHandler.removeMessages(LONG_PRESS); mHandler.sendEmptyMessageAtTime(LONG_PRESS, mCurrentDownEvent.getDownTime() - + tapTime + longpressTime); + + TAP_TIMEOUT + LONGPRESS_TIMEOUT); } - mHandler.sendEmptyMessageAtTime(SHOW_PRESS, mCurrentDownEvent.getDownTime() + tapTime); - handled = mListener.onDown(ev); + mHandler.sendEmptyMessageAtTime(SHOW_PRESS, mCurrentDownEvent.getDownTime() + TAP_TIMEOUT); + handled |= mListener.onDown(ev); break; case MotionEvent.ACTION_MOVE: @@ -303,11 +411,13 @@ public class GestureDetector { } final float scrollX = mLastMotionX - x; final float scrollY = mLastMotionY - y; - if (mAlwaysInTapRegion) { + if (mIsDoubleTapping) { + handled = mDoubleTapListener.onDoubleTapEvent(ev); + } else if (mAlwaysInTapRegion) { final int deltaX = (int) (x - mCurrentDownEvent.getX()); final int deltaY = (int) (y - mCurrentDownEvent.getY()); int distance = (deltaX * deltaX) + (deltaY * deltaY); - if (distance > TOUCH_SLOP_SQUARE) { + if (distance > mTouchSlopSquare) { handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY); mLastMotionX = x; mLastMotionY = y; @@ -315,6 +425,9 @@ public class GestureDetector { mHandler.removeMessages(SHOW_PRESS); mHandler.removeMessages(LONG_PRESS); } + if (distance > mBiggerTouchSlopSquare) { + mAlwaysInBiggerTapRegion = false; + } } else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) { handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY); mLastMotionX = x; @@ -323,8 +436,13 @@ public class GestureDetector { break; case MotionEvent.ACTION_UP: - mCurrentUpEvent = MotionEvent.obtain(ev); - if (mInLongPress) { + MotionEvent currentUpEvent = MotionEvent.obtain(ev); + if (mIsDoubleTapping) { + handled = mDoubleTapListener.onDoubleTapEvent(ev); + mIsDoubleTapping = false; + break; + } else if (mInLongPress) { + mHandler.removeMessages(TAP); mInLongPress = false; break; } @@ -338,9 +456,9 @@ public class GestureDetector { final float velocityY = velocityTracker.getYVelocity(); final float velocityX = velocityTracker.getXVelocity(); - if ((Math.abs(velocityY) > ViewConfiguration.getMinimumFlingVelocity()) - || (Math.abs(velocityX) > ViewConfiguration.getMinimumFlingVelocity())){ - handled = mListener.onFling(mCurrentDownEvent, mCurrentUpEvent, velocityX, velocityY); + if ((Math.abs(velocityY) > mMinimumFlingVelocity) + || (Math.abs(velocityX) > mMinimumFlingVelocity)){ + handled = mListener.onFling(mCurrentDownEvent, currentUpEvent, velocityX, velocityY); } } mVelocityTracker.recycle(); @@ -351,6 +469,7 @@ public class GestureDetector { case MotionEvent.ACTION_CANCEL: mHandler.removeMessages(SHOW_PRESS); mHandler.removeMessages(LONG_PRESS); + mHandler.removeMessages(TAP); mVelocityTracker.recycle(); mVelocityTracker = null; if (mInLongPress) { @@ -361,6 +480,20 @@ public class GestureDetector { return handled; } + private boolean isConsideredDoubleTap(MotionEvent firstDown, MotionEvent secondDown) { + if (!mAlwaysInBiggerTapRegion) { + return false; + } + + if (secondDown.getEventTime() - firstDown.getEventTime() > DOUBLE_TAP_TIMEOUT) { + return false; + } + + int deltaX = (int) firstDown.getX() - (int) secondDown.getX(); + int deltaY = (int) firstDown.getY() - (int) secondDown.getY(); + return (deltaX * deltaX + deltaY * deltaY < mDoubleTapSlopSquare); + } + private void dispatchLongPress() { mInLongPress = true; mListener.onLongPress(mCurrentDownEvent); diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl index 40251db..a856b24 100644 --- a/core/java/android/view/IWindowManager.aidl +++ b/core/java/android/view/IWindowManager.aidl @@ -104,6 +104,9 @@ interface IWindowManager int getKeycodeState(int sw); int getKeycodeStateForDevice(int devid, int sw); + // Report whether the hardware supports the given keys; returns true if successful + boolean hasKeys(in int[] keycodes, inout boolean[] keyExists); + // For testing void setInTouchMode(boolean showFocus); diff --git a/core/java/android/view/KeyCharacterMap.java b/core/java/android/view/KeyCharacterMap.java index 0347d50..25958aa 100644 --- a/core/java/android/view/KeyCharacterMap.java +++ b/core/java/android/view/KeyCharacterMap.java @@ -18,6 +18,8 @@ package android.view; import android.text.method.MetaKeyKeyListener; import android.util.SparseIntArray; +import android.os.RemoteException; +import android.os.ServiceManager; import android.os.SystemClock; import android.util.SparseArray; @@ -350,6 +352,28 @@ public class KeyCharacterMap return getKeyboardType_native(mPointer); } + /** + * Queries the framework about whether any physical keys exist on the + * device that are capable of producing the given key codes. + */ + public static boolean deviceHasKey(int keyCode) { + int[] codeArray = new int[1]; + codeArray[0] = keyCode; + boolean[] ret = deviceHasKeys(codeArray); + return ret[0]; + } + + public static boolean[] deviceHasKeys(int[] keyCodes) { + boolean[] ret = new boolean[keyCodes.length]; + IWindowManager wm = IWindowManager.Stub.asInterface(ServiceManager.getService("window")); + try { + wm.hasKeys(keyCodes, ret); + } catch (RemoteException e) { + // no fallback; just return the empty array + } + return ret; + } + private int mPointer; private int mKeyboardDevice; diff --git a/core/java/android/view/KeyEvent.java b/core/java/android/view/KeyEvent.java index 1575aad..d5434b6 100644 --- a/core/java/android/view/KeyEvent.java +++ b/core/java/android/view/KeyEvent.java @@ -92,7 +92,7 @@ public class KeyEvent implements Parcelable { public static final int KEYCODE_SYM = 63; public static final int KEYCODE_EXPLORER = 64; public static final int KEYCODE_ENVELOPE = 65; - public static final int KEYCODE_ENTER = 66; + public static final int KEYCODE_ENTER = 66; public static final int KEYCODE_DEL = 67; public static final int KEYCODE_GRAVE = 68; public static final int KEYCODE_MINUS = 69; @@ -144,8 +144,12 @@ public class KeyEvent implements Parcelable { public static final int ACTION_UP = 1; /** * {@link #getAction} value: multiple duplicate key events have - * occurred in a row. The {#link {@link #getRepeatCount()} method returns - * the number of duplicates. + * occurred in a row, or a complex string is being delivered. If the + * key code is not {#link {@link #KEYCODE_UNKNOWN} then the + * {#link {@link #getRepeatCount()} method returns the number of times + * the given key code should be executed. + * Otherwise, if the key code {@link #KEYCODE_UNKNOWN}, then + * this is a sequence of characters as returned by {@link #getCharacters}. */ public static final int ACTION_MULTIPLE = 2; @@ -248,6 +252,7 @@ public class KeyEvent implements Parcelable { private int mFlags; private long mDownTime; private long mEventTime; + private String mCharacters; public interface Callback { /** @@ -406,6 +411,28 @@ public class KeyEvent implements Parcelable { } /** + * Create a new key event for a string of characters. The key code, + * action, and repeat could will automatically be set to + * {@link #KEYCODE_UNKNOWN}, {@link #ACTION_MULTIPLE}, and 0 for you. + * + * @param time The time (in {@link android.os.SystemClock#uptimeMillis}) + * at which this event occured. + * @param characters The string of characters. + * @param device The device ID that generated the key event. + * @param flags The flags for this key event + */ + public KeyEvent(long time, String characters, int device, int flags) { + mDownTime = time; + mEventTime = time; + mCharacters = characters; + mAction = ACTION_MULTIPLE; + mKeyCode = KEYCODE_UNKNOWN; + mRepeatCount = 0; + mDeviceId = device; + mFlags = flags; + } + + /** * Copy an existing key event, modifying its time and repeat count. * * @param origEvent The existing event to be copied. @@ -423,6 +450,7 @@ public class KeyEvent implements Parcelable { mDeviceId = origEvent.mDeviceId; mScancode = origEvent.mScancode; mFlags = origEvent.mFlags; + mCharacters = origEvent.mCharacters; } /** @@ -441,6 +469,8 @@ public class KeyEvent implements Parcelable { mDeviceId = origEvent.mDeviceId; mScancode = origEvent.mScancode; mFlags = origEvent.mFlags; + // Don't copy mCharacters, since one way or the other we'll lose it + // when changing the action. } /** @@ -580,7 +610,7 @@ public class KeyEvent implements Parcelable { /** * Retrieve the key code of the key event. This is the physical key that - * was pressed -- not the Unicode character. + * was pressed, <em>not</em> the Unicode character. * * @return The key code of the event. */ @@ -589,6 +619,18 @@ public class KeyEvent implements Parcelable { } /** + * For the special case of a {@link #ACTION_MULTIPLE} event with key + * code of {@link #KEYCODE_UNKNOWN}, this is a raw string of characters + * associated with the event. In all other cases it is null. + * + * @return Returns a String of 1 or more characters associated with + * the event. + */ + public final String getCharacters() { + return mCharacters; + } + + /** * Retrieve the hardware key id of this key event. These values are not * reliable and vary from device to device. * @@ -772,16 +814,18 @@ public class KeyEvent implements Parcelable { if (receiver.onKeyMultiple(code, count, this)) { return true; } - mAction = ACTION_DOWN; - mRepeatCount = 0; - boolean handled = receiver.onKeyDown(code, this); - if (handled) { - mAction = ACTION_UP; - receiver.onKeyUp(code, this); + if (code != KeyEvent.KEYCODE_UNKNOWN) { + mAction = ACTION_DOWN; + mRepeatCount = 0; + boolean handled = receiver.onKeyDown(code, this); + if (handled) { + mAction = ACTION_UP; + receiver.onKeyUp(code, this); + } + mAction = ACTION_MULTIPLE; + mRepeatCount = count; + return handled; } - mAction = ACTION_MULTIPLE; - mRepeatCount = count; - return handled; } return false; } diff --git a/core/java/android/view/TouchDelegate.java b/core/java/android/view/TouchDelegate.java index 057df92..27b49db 100644 --- a/core/java/android/view/TouchDelegate.java +++ b/core/java/android/view/TouchDelegate.java @@ -77,7 +77,9 @@ public class TouchDelegate { * actual extent. */ public static final int TO_RIGHT = 8; - + + private int mSlop; + /** * Constructor * @@ -87,10 +89,10 @@ public class TouchDelegate { */ public TouchDelegate(Rect bounds, View delegateView) { mBounds = bounds; - - int slop = ViewConfiguration.getTouchSlop(); + + mSlop = ViewConfiguration.get(delegateView.getContext()).getScaledTouchSlop(); mSlopBounds = new Rect(bounds); - mSlopBounds.inset(-slop, -slop); + mSlopBounds.inset(-mSlop, -mSlop); mDelegateView = delegateView; } @@ -141,7 +143,7 @@ public class TouchDelegate { } else { // Offset event coordinates to be outside the target view (in case it does // something like tracking pressed state) - int slop = ViewConfiguration.getTouchSlop(); + int slop = mSlop; event.setLocation(-(slop * 2), -(slop * 2)); } handled = delegateView.dispatchTouchEvent(event); diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 85f482c..a51b564 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -60,12 +60,31 @@ import java.util.Arrays; /** * <p> - * The <code>View</code> class represents the basic UI building block. A view + * This class represents the basic building block for user interface components. A View * occupies a rectangular area on the screen and is responsible for drawing and - * event handling. <code>View</code> is the base class for <em>widgets</em>, - * used to create interactive graphical user interfaces. + * event handling. View is the base class for <em>widgets</em>, which are + * used to create interactive UI components (buttons, text fields, etc.). The + * {@link android.view.ViewGroup} subclass is the base class for <em>layouts</em>, which + * are invisible containers that hold other Views (or other ViewGroups) and define + * their layout properties. * </p> * + * <div class="special"> + * <p>For an introduction to using this class to develop your + * application's user interface, read the Developer Guide documentation on + * <strong><a href="{@docRoot}guide/topics/ui/index.html">User Interface</a></strong>. Special topics + * include: + * <br/><a href="{@docRoot}guide/topics/ui/declaring-layout.html">Declaring Layout</a> + * <br/><a href="{@docRoot}guide/topics/ui/menus.html">Creating Menus</a> + * <br/><a href="{@docRoot}guide/topics/ui/layout-objects.html">Common Layout Objects</a> + * <br/><a href="{@docRoot}guide/topics/ui/binding.html">Binding to Data with AdapterView</a> + * <br/><a href="{@docRoot}guide/topics/ui/ui-events.html">Handling UI Events</a> + * <br/><a href="{@docRoot}guide/topics/ui/themes.html">Applying Styles and Themes</a> + * <br/><a href="{@docRoot}guide/topics/ui/custom-components.html">Building Custom Components</a> + * <br/><a href="{@docRoot}guide/topics/ui/how-android-draws.html">How Android Draws Views</a>. + * </p> + * </div> + * * <a name="Using"></a> * <h3>Using Views</h3> * <p> @@ -1308,6 +1327,14 @@ public class View implements Drawable.Callback, KeyEvent.Callback { static final int HAS_BOUNDS = 0x00000010; /** {@hide} */ static final int DRAWN = 0x00000020; + /** + * When this flag is set, this view is running an animation on behalf of its + * children and should therefore not cancel invalidate requests, even if they + * lie outside of this view's bounds. + * + * {@hide} + */ + static final int DRAW_ANIMATION = 0x00000040; /** {@hide} */ static final int SKIP_DRAW = 0x00000080; /** {@hide} */ @@ -1353,8 +1380,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback { */ static final int SCROLL_CONTAINER_ADDED = 0x00100000; - // Note: flag 0x00000040 is available - /** * The parent this view is attached to. * {@hide} @@ -1559,6 +1584,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { private int mNextFocusDownId = View.NO_ID; private CheckForLongPress mPendingCheckForLongPress; + private UnsetPressedState mUnsetPressedState; /** * Whether the long press's action has been invoked. The tap's action is invoked on the @@ -1898,7 +1924,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback { initScrollCache(); mScrollCache.fadingEdgeLength = a.getDimensionPixelSize( - R.styleable.View_fadingEdgeLength, ViewConfiguration.getFadingEdgeLength()); + R.styleable.View_fadingEdgeLength, + ViewConfiguration.get(mContext).getScaledFadingEdgeLength()); } /** @@ -2013,36 +2040,38 @@ public class View implements Drawable.Callback, KeyEvent.Callback { mScrollCache.scrollBar = new ScrollBarDrawable(); } - mScrollCache.scrollBarSize = a.getDimensionPixelSize( + final ScrollabilityCache scrollabilityCache = mScrollCache; + + scrollabilityCache.scrollBarSize = a.getDimensionPixelSize( com.android.internal.R.styleable.View_scrollbarSize, - ViewConfiguration.getScrollBarSize()); + ViewConfiguration.get(mContext).getScaledScrollBarSize()); Drawable track = a.getDrawable(R.styleable.View_scrollbarTrackHorizontal); - mScrollCache.scrollBar.setHorizontalTrackDrawable(track); + scrollabilityCache.scrollBar.setHorizontalTrackDrawable(track); Drawable thumb = a.getDrawable(R.styleable.View_scrollbarThumbHorizontal); if (thumb != null) { - mScrollCache.scrollBar.setHorizontalThumbDrawable(thumb); + scrollabilityCache.scrollBar.setHorizontalThumbDrawable(thumb); } boolean alwaysDraw = a.getBoolean(R.styleable.View_scrollbarAlwaysDrawHorizontalTrack, false); if (alwaysDraw) { - mScrollCache.scrollBar.setAlwaysDrawHorizontalTrack(true); + scrollabilityCache.scrollBar.setAlwaysDrawHorizontalTrack(true); } track = a.getDrawable(R.styleable.View_scrollbarTrackVertical); - mScrollCache.scrollBar.setVerticalTrackDrawable(track); + scrollabilityCache.scrollBar.setVerticalTrackDrawable(track); thumb = a.getDrawable(R.styleable.View_scrollbarThumbVertical); if (thumb != null) { - mScrollCache.scrollBar.setVerticalThumbDrawable(thumb); + scrollabilityCache.scrollBar.setVerticalThumbDrawable(thumb); } alwaysDraw = a.getBoolean(R.styleable.View_scrollbarAlwaysDrawVerticalTrack, false); if (alwaysDraw) { - mScrollCache.scrollBar.setAlwaysDrawVerticalTrack(true); + scrollabilityCache.scrollBar.setAlwaysDrawVerticalTrack(true); } // Re-apply user/background padding so that scrollbar(s) get added @@ -2056,7 +2085,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { */ private void initScrollCache() { if (mScrollCache == null) { - mScrollCache = new ScrollabilityCache(); + mScrollCache = new ScrollabilityCache(ViewConfiguration.get(mContext)); } } @@ -2635,6 +2664,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { */ public void setVisibility(int visibility) { setFlags(visibility, VISIBILITY_MASK); + if (mBGDrawable != null) mBGDrawable.setVisible(visibility == VISIBLE, false); } /** @@ -3410,6 +3440,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { } void performCollectViewAttributes(int visibility) { + //noinspection PointlessBitwiseExpression if (((visibility | mViewFlags) & (VISIBILITY_MASK | KEEP_SCREEN_ON)) == (VISIBLE | KEEP_SCREEN_ON)) { mAttachInfo.mKeepScreenOn = true; @@ -3708,10 +3739,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback { } } - final UnsetPressedState unsetPressedState = new UnsetPressedState(); - if (!post(unsetPressedState)) { + if (mUnsetPressedState == null) { + mUnsetPressedState = new UnsetPressedState(); + } + + if (!post(mUnsetPressedState)) { // If the post failed, unpress right now - unsetPressedState.run(); + mUnsetPressedState.run(); } } break; @@ -3734,7 +3768,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { final int y = (int) event.getY(); // Be lenient about moving outside of buttons - int slop = ViewConfiguration.getTouchSlop(); + int slop = ViewConfiguration.get(mContext).getScaledTouchSlop(); if ((x < 0 - slop) || (x >= getWidth() + slop) || (y < 0 - slop) || (y >= getHeight() + slop)) { // Outside button @@ -4413,14 +4447,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * @see #invalidate() */ public void postInvalidate() { - // We try only with the AttachInfo because there's no point in invalidating - // if we are not attached to our window - if (mAttachInfo != null) { - Message msg = Message.obtain(); - msg.what = AttachInfo.INVALIDATE_MSG; - msg.obj = this; - mAttachInfo.mHandler.sendMessage(msg); - } + postInvalidateDelayed(0); } /** @@ -4436,16 +4463,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * @see #invalidate(Rect) */ public void postInvalidate(int left, int top, int right, int bottom) { - // We try only with the AttachInfo because there's no point in invalidating - // if we are not attached to our window - if (mAttachInfo != null) { - Message msg = Message.obtain(); - msg.what = AttachInfo.INVALIDATE_RECT_MSG; - msg.obj = this; - msg.arg1 = (left << 16) | (top & 0xFFFF); - msg.arg2 = (right << 16) | (bottom & 0xFFFF); - mAttachInfo.mHandler.sendMessage(msg); - } + postInvalidateDelayed(0, left, top, right, bottom); } /** @@ -4477,16 +4495,22 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * @param right The right coordinate of the rectangle to invalidate. * @param bottom The bottom coordinate of the rectangle to invalidate. */ - public void postInvalidateDelayed(long delayMilliseconds, int left, int top - , int right, int bottom) { + public void postInvalidateDelayed(long delayMilliseconds, int left, int top, + int right, int bottom) { + // We try only with the AttachInfo because there's no point in invalidating // if we are not attached to our window if (mAttachInfo != null) { - Message msg = Message.obtain(); + final AttachInfo.InvalidateInfo info = AttachInfo.InvalidateInfo.acquire(); + info.target = this; + info.left = left; + info.top = top; + info.right = right; + info.bottom = bottom; + + final Message msg = Message.obtain(); msg.what = AttachInfo.INVALIDATE_RECT_MSG; - msg.obj = this; - msg.arg1 = (left << 16) | (top & 0xFFFF); - msg.arg2 = (right << 16) | (bottom & 0xFFFF); + msg.obj = info; mAttachInfo.mHandler.sendMessageDelayed(msg, delayMilliseconds); } } @@ -4865,7 +4889,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback { final boolean drawHorizontalScrollBar = (viewFlags & SCROLLBARS_HORIZONTAL) == SCROLLBARS_HORIZONTAL; final boolean drawVerticalScrollBar = - (viewFlags & SCROLLBARS_VERTICAL) == SCROLLBARS_VERTICAL; + (viewFlags & SCROLLBARS_VERTICAL) == SCROLLBARS_VERTICAL + && !isVerticalScrollBarHidden(); if (drawVerticalScrollBar || drawHorizontalScrollBar) { final int width = mRight - mLeft; @@ -4887,6 +4912,16 @@ public class View implements Drawable.Callback, KeyEvent.Callback { } } } + + /** + * Override this if the vertical scrollbar needs to be hidden in a subclass, like when + * FastScroller is visible. + * @return whether to temporarily hide the vertical scrollbar + * @hide + */ + protected boolean isVerticalScrollBarHidden() { + return false; + } /** * <p>Draw the horizontal scrollbar if @@ -5022,6 +5057,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { if (mPendingCheckForLongPress != null) { removeCallbacks(mPendingCheckForLongPress); } + destroyDrawingCache(); } /** @@ -5408,7 +5444,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { if (width <= 0 || height <= 0 || (width * height * (opaque ? 2 : 4) >= // Projected bitmap size in bytes - ViewConfiguration.getMaximumDrawingCacheSize())) { + ViewConfiguration.get(mContext).getScaledMaximumDrawingCacheSize())) { if (mDrawingCache != null) { mDrawingCache.recycle(); } @@ -5485,9 +5521,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback { final int restoreCount = canvas.save(); canvas.translate(-mScrollX, -mScrollY); + mPrivateFlags |= DRAWN; + // Fast path for layouts with no backgrounds if ((mPrivateFlags & SKIP_DRAW) == SKIP_DRAW) { - mPrivateFlags |= DRAWN; if (ViewDebug.TRACE_HIERARCHY) { ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW); } @@ -5616,6 +5653,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback { ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW); } + mPrivateFlags |= DRAWN; + /* * Draw traversal performs several drawing steps which must be executed * in the appropriate order: @@ -5656,7 +5695,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback { boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0; if (!verticalEdges && !horizontalEdges) { // Step 3, draw the content - mPrivateFlags |= DRAWN; onDraw(canvas); // Step 4, draw the children @@ -5760,7 +5798,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback { } // Step 3, draw the content - mPrivateFlags |= DRAWN; onDraw(canvas); // Step 4, draw the children @@ -7671,6 +7708,67 @@ public class View implements Drawable.Callback, KeyEvent.Callback { void playSoundEffect(int effectId); } + /** + * InvalidateInfo is used to post invalidate(int, int, int, int) messages + * to a Handler. This class contains the target (View) to invalidate and + * the coordinates of the dirty rectangle. + * + * For performance purposes, this class also implements a pool of up to + * POOL_LIMIT objects that get reused. This reduces memory allocations + * whenever possible. + * + * The pool is implemented as a linked list of InvalidateInfo object with + * the root pointing to the next available InvalidateInfo. If the root + * is null (i.e. when all instances from the pool have been acquired), + * then a new InvalidateInfo is created and returned to the caller. + * + * An InvalidateInfo is sent back to the pool by calling its release() + * method. If the pool is full the object is simply discarded. + * + * This implementation follows the object pool pattern used in the + * MotionEvent class. + */ + static class InvalidateInfo { + private static final int POOL_LIMIT = 10; + private static final Object sLock = new Object(); + + private static int sAcquiredCount = 0; + private static InvalidateInfo sRoot; + + private InvalidateInfo next; + + View target; + + int left; + int top; + int right; + int bottom; + + static InvalidateInfo acquire() { + synchronized (sLock) { + if (sRoot == null) { + return new InvalidateInfo(); + } + + InvalidateInfo info = sRoot; + sRoot = info.next; + sAcquiredCount--; + + return info; + } + } + + void release() { + synchronized (sLock) { + if (sAcquiredCount < POOL_LIMIT) { + sAcquiredCount++; + next = sRoot; + sRoot = this; + } + } + } + } + final IWindowSession mSession; final IWindow mWindow; @@ -7839,18 +7937,21 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * instances of View.</p> */ private static class ScrollabilityCache { - public int fadingEdgeLength = ViewConfiguration.getFadingEdgeLength(); + public int fadingEdgeLength; - public int scrollBarSize = ViewConfiguration.getScrollBarSize(); + public int scrollBarSize; public ScrollBarDrawable scrollBar; public final Paint paint; public final Matrix matrix; public Shader shader; - private int mLastColor = 0; + private int mLastColor; + + public ScrollabilityCache(ViewConfiguration configuration) { + fadingEdgeLength = configuration.getScaledFadingEdgeLength(); + scrollBarSize = configuration.getScaledScrollBarSize(); - public ScrollabilityCache() { paint = new Paint(); matrix = new Matrix(); // use use a height of 1, and then wack the matrix each time we diff --git a/core/java/android/view/ViewConfiguration.java b/core/java/android/view/ViewConfiguration.java index b7110ce..7153ea1 100644 --- a/core/java/android/view/ViewConfiguration.java +++ b/core/java/android/view/ViewConfiguration.java @@ -16,17 +16,19 @@ package android.view; +import android.content.Context; +import android.util.DisplayMetrics; +import android.util.SparseArray; + /** * Contains methods to standard constants used in the UI for timeouts, sizes, and distances. - * */ public class ViewConfiguration { - /** * Defines the width of the horizontal scrollbar and the height of the vertical scrollbar in * pixels */ - private static final int SCROLL_BAR_SIZE = 6; + private static final int SCROLL_BAR_SIZE = 10; /** * Defines the length of the fading edges in pixels @@ -83,6 +85,11 @@ public class ViewConfiguration { private static final int TOUCH_SLOP = 12; /** + * Distance between the first touch and second touch to still be considered a double tap + */ + private static final int DOUBLE_TAP_SLOP = 100; + + /** * Distance a touch needs to be outside of a window's bounds for it to * count as outside for purposes of dismissing the window. */ @@ -97,28 +104,124 @@ public class ViewConfiguration { * The maximum size of View's drawing cache, expressed in bytes. This size * should be at least equal to the size of the screen in ARGB888 format. */ - private static final int MAXIMUM_DRAWING_CACHE_SIZE = 320 * 480 * 4; // One HVGA screen, ARGB8888 + @Deprecated + private static final int MAXIMUM_DRAWING_CACHE_SIZE = 320 * 480 * 4; // HVGA screen, ARGB8888 /** * The coefficient of friction applied to flings/scrolls. */ private static float SCROLL_FRICTION = 0.015f; + private final int mEdgeSlop; + private final int mFadingEdgeLength; + private final int mMinimumFlingVelocity; + private final int mScrollbarSize; + private final int mTouchSlop; + private final int mDoubleTapSlop; + private final int mWindowTouchSlop; + private final int mMaximumDrawingCacheSize; + + private static final SparseArray<ViewConfiguration> sConfigurations = + new SparseArray<ViewConfiguration>(2); + + /** + * @deprecated Use {@link android.view.ViewConfiguration#get(android.content.Context)} instead. + */ + @Deprecated + public ViewConfiguration() { + mEdgeSlop = EDGE_SLOP; + mFadingEdgeLength = FADING_EDGE_LENGTH; + mMinimumFlingVelocity = MINIMUM_FLING_VELOCITY; + mScrollbarSize = SCROLL_BAR_SIZE; + mTouchSlop = TOUCH_SLOP; + mDoubleTapSlop = DOUBLE_TAP_SLOP; + mWindowTouchSlop = WINDOW_TOUCH_SLOP; + //noinspection deprecation + mMaximumDrawingCacheSize = MAXIMUM_DRAWING_CACHE_SIZE; + } + + /** + * Creates a new configuration for the specified context. The configuration depends on + * various parameters of the context, like the dimension of the display or the density + * of the display. + * + * @param context The application context used to initialize this view configuration. + * + * @see #get(android.content.Context) + * @see android.util.DisplayMetrics + */ + private ViewConfiguration(Context context) { + final DisplayMetrics metrics = context.getResources().getDisplayMetrics(); + final float density = metrics.density; + + mEdgeSlop = (int) (density * EDGE_SLOP + 0.5f); + mFadingEdgeLength = (int) (density * FADING_EDGE_LENGTH + 0.5f); + mMinimumFlingVelocity = (int) (density * MINIMUM_FLING_VELOCITY + 0.5f); + mScrollbarSize = (int) (density * SCROLL_BAR_SIZE + 0.5f); + mTouchSlop = (int) (density * TOUCH_SLOP + 0.5f); + mDoubleTapSlop = (int) (density * DOUBLE_TAP_SLOP + 0.5f); + mWindowTouchSlop = (int) (density * WINDOW_TOUCH_SLOP + 0.5f); + + // Size of the screen in bytes, in ARGB_8888 format + mMaximumDrawingCacheSize = 4 * metrics.widthPixels * metrics.heightPixels; + } + + /** + * Returns a configuration for the specified context. The configuration depends on + * various parameters of the context, like the dimension of the display or the + * density of the display. + * + * @param context The application context used to initialize the view configuration. + */ + public static ViewConfiguration get(Context context) { + final DisplayMetrics metrics = context.getResources().getDisplayMetrics(); + final int density = (int) (100.0f * metrics.density); + + ViewConfiguration configuration = sConfigurations.get(density); + if (configuration == null) { + configuration = new ViewConfiguration(context); + sConfigurations.put(density, configuration); + } + + return configuration; + } + /** * @return The width of the horizontal scrollbar and the height of the vertical * scrollbar in pixels + * + * @deprecated Use {@link #getScaledScrollBarSize()} instead. */ + @Deprecated public static int getScrollBarSize() { return SCROLL_BAR_SIZE; } /** + * @return The width of the horizontal scrollbar and the height of the vertical + * scrollbar in pixels + */ + public int getScaledScrollBarSize() { + return mScrollbarSize; + } + + /** * @return Defines the length of the fading edges in pixels + * + * @deprecated Use {@link #getScaledFadingEdgeLength()} instead. */ + @Deprecated public static int getFadingEdgeLength() { return FADING_EDGE_LENGTH; } - + + /** + * @return Defines the length of the fading edges in pixels + */ + public int getScaledFadingEdgeLength() { + return mFadingEdgeLength; + } + /** * @return Defines the duration in milliseconds of the pressed state in child * components. @@ -156,44 +259,121 @@ public class ViewConfiguration { /** * @return Inset in pixels to look for touchable content when the user touches the edge of the * screen + * + * @deprecated Use {@link #getScaledEdgeSlop()} instead. */ + @Deprecated public static int getEdgeSlop() { return EDGE_SLOP; } - + + /** + * @return Inset in pixels to look for touchable content when the user touches the edge of the + * screen + */ + public int getScaledEdgeSlop() { + return mEdgeSlop; + } + /** * @return Distance a touch can wander before we think the user is scrolling in pixels + * + * @deprecated Use {@link #getScaledTouchSlop()} instead. */ + @Deprecated public static int getTouchSlop() { return TOUCH_SLOP; } + + /** + * @return Distance a touch can wander before we think the user is scrolling in pixels + */ + public int getScaledTouchSlop() { + return mTouchSlop; + } + + /** + * @return Distance between the first touch and second touch to still be + * considered a double tap + * @deprecated Use {@link #getScaledDoubleTapSlop()} instead. + * @hide The only client of this should be GestureDetector, which needs this + * for clients that still use its deprecated constructor. + */ + @Deprecated + public static int getDoubleTapSlop() { + return DOUBLE_TAP_SLOP; + } /** + * @return Distance between the first touch and second touch to still be + * considered a double tap + * @hide pending API council + */ + public int getScaledDoubleTapSlop() { + return mDoubleTapSlop; + } + + /** * @return Distance a touch must be outside the bounds of a window for it * to be counted as outside the window for purposes of dismissing that * window. + * + * @deprecated Use {@link #getScaledWindowTouchSlop()} instead. */ + @Deprecated public static int getWindowTouchSlop() { return WINDOW_TOUCH_SLOP; } + + /** + * @return Distance a touch must be outside the bounds of a window for it + * to be counted as outside the window for purposes of dismissing that + * window. + */ + public int getScaledWindowTouchSlop() { + return mWindowTouchSlop; + } /** - * Minimum velocity to initiate a fling, as measured in pixels per second + * @return Minimum velocity to initiate a fling, as measured in pixels per second. + * + * @deprecated Use {@link #getScaledMinimumFlingVelocity()} instead. */ - public static int getMinimumFlingVelocity() { - return MINIMUM_FLING_VELOCITY; + @Deprecated + public static int getMinimumFlingVelocity() { + return MINIMUM_FLING_VELOCITY; + } + + /** + * @return Minimum velocity to initiate a fling, as measured in pixels per second. + */ + public int getScaledMinimumFlingVelocity() { + return mMinimumFlingVelocity; } /** * The maximum drawing cache size expressed in bytes. * * @return the maximum size of View's drawing cache expressed in bytes + * + * @deprecated Use {@link #getScaledMaximumDrawingCacheSize()} instead. */ + @Deprecated public static int getMaximumDrawingCacheSize() { + //noinspection deprecation return MAXIMUM_DRAWING_CACHE_SIZE; } /** + * The maximum drawing cache size expressed in bytes. + * + * @return the maximum size of View's drawing cache expressed in bytes + */ + public int getScaledMaximumDrawingCacheSize() { + return mMaximumDrawingCacheSize; + } + + /** * The amount of time that the zoom controls should be * displayed on the screen expressed in milliseconds. * diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index e26a19e..c758662 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -25,6 +25,7 @@ import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.Region; +import android.graphics.RectF; import android.os.Parcelable; import android.util.AttributeSet; import android.util.EventLog; @@ -74,6 +75,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager // The current transformation to apply on the child being drawn private Transformation mChildTransformation; + private RectF mInvalidateRegion; // Target of Motion events private View mMotionTarget; @@ -1199,6 +1201,8 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } + // We will draw our child's animation, let's reset the flag + mPrivateFlags &= ~DRAW_ANIMATION; mGroupFlags &= ~FLAG_INVALIDATE_REQUIRED; boolean more = false; @@ -1328,8 +1332,15 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager boolean concatMatrix = false; if (a != null) { - if (!a.isInitialized()) { + if (mInvalidateRegion == null) { + mInvalidateRegion = new RectF(); + } + final RectF region = mInvalidateRegion; + + final boolean initialized = a.isInitialized(); + if (!initialized) { a.initialize(cr - cl, cb - ct, getWidth(), getHeight()); + a.initializeInvalidateRegion(cl, ct, cr, cb); child.onAnimationStart(); } @@ -1347,10 +1358,22 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager FLAG_OPTIMIZE_INVALIDATE) { mGroupFlags |= FLAG_INVALIDATE_REQUIRED; } else if ((flags & FLAG_INVALIDATE_REQUIRED) == 0) { + // The child need to draw an animation, potentially offscreen, so + // make sure we do not cancel invalidate requests + mPrivateFlags |= DRAW_ANIMATION; invalidate(cl, ct, cr, cb); } } else { - mGroupFlags |= FLAG_INVALIDATE_REQUIRED; + a.getInvalidateRegion(cl, ct, cr, cb, region, transformToApply); + + // The child need to draw an animation, potentially offscreen, so + // make sure we do not cancel invalidate requests + mPrivateFlags |= DRAW_ANIMATION; + // Enlarge the invalidate region to account for rounding errors + // in Animation#getInvalidateRegion(); Using 0.5f is unfortunately + // not enough for some types of animations (e.g. scale down.) + invalidate((int) (region.left - 1.0f), (int) (region.top - 1.0f), + (int) (region.right + 1.0f), (int) (region.bottom + 1.0f)); } } } else if ((flags & FLAG_SUPPORT_STATIC_TRANSFORMATIONS) == @@ -1367,7 +1390,8 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } } - if (!concatMatrix && canvas.quickReject(cl, ct, cr, cb, Canvas.EdgeType.BW)) { + if (!concatMatrix && canvas.quickReject(cl, ct, cr, cb, Canvas.EdgeType.BW) && + (child.mPrivateFlags & DRAW_ANIMATION) == 0) { return more; } @@ -1435,10 +1459,13 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } } + // Clear the flag as early as possible to allow draw() implementations + // to call invalidate() successfully when doing animations + child.mPrivateFlags |= DRAWN; + if (hasNoCache) { // Fast path for layouts with no backgrounds if ((child.mPrivateFlags & SKIP_DRAW) == SKIP_DRAW) { - child.mPrivateFlags |= DRAWN; if (ViewDebug.TRACE_HIERARCHY) { ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW); } @@ -1455,7 +1482,6 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager cachePaint.setAlpha(255); mGroupFlags &= ~FLAG_ALPHA_LOWER_THAN_ONE; } - child.mPrivateFlags |= DRAWN; if (ViewRoot.PROFILE_DRAWING) { EventLog.writeEvent(60003, hashCode()); } @@ -1922,8 +1948,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager LayoutAnimationController.AnimationParameters animationParams = params.layoutAnimationParameters; if (animationParams == null) { - animationParams = - new LayoutAnimationController.AnimationParameters(); + animationParams = new LayoutAnimationController.AnimationParameters(); params.layoutAnimationParameters = animationParams; } @@ -2278,8 +2303,16 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager final int[] location = attachInfo.mInvalidateChildLocation; location[CHILD_LEFT_INDEX] = child.mLeft; location[CHILD_TOP_INDEX] = child.mTop; + + // If the child is drawing an animation, we want to copy this flag onto + // ourselves and the parent to make sure the invalidate request goes + // through + final boolean drawAnimation = (child.mPrivateFlags & DRAW_ANIMATION) == DRAW_ANIMATION; do { + if (drawAnimation && parent instanceof View) { + ((View) parent).mPrivateFlags |= DRAW_ANIMATION; + } parent = parent.invalidateChildInParent(location, dirty); } while (parent != null); } @@ -2307,7 +2340,8 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager final int left = mLeft; final int top = mTop; - if (dirty.intersect(0, 0, mRight - left, mBottom - top)) { + if (dirty.intersect(0, 0, mRight - left, mBottom - top) || + (mPrivateFlags & DRAW_ANIMATION) == DRAW_ANIMATION) { mPrivateFlags &= ~DRAWING_CACHE_VALID; location[CHILD_LEFT_INDEX] = left; diff --git a/core/java/android/view/ViewRoot.java b/core/java/android/view/ViewRoot.java index 9e0289a..4e46397 100644 --- a/core/java/android/view/ViewRoot.java +++ b/core/java/android/view/ViewRoot.java @@ -177,13 +177,14 @@ public final class ViewRoot extends Handler implements ViewParent, boolean mUseGL; boolean mGlWanted; + final ViewConfiguration mViewConfiguration; + /** * see {@link #playSoundEffect(int)} */ AudioManager mAudioManager; - public ViewRoot(Context context) { super(); @@ -224,6 +225,7 @@ public final class ViewRoot extends Handler implements ViewParent, mSurface = new Surface(); mAdded = false; mAttachInfo = new View.AttachInfo(sWindowSession, mWindow, this, this); + mViewConfiguration = ViewConfiguration.get(context); } @Override @@ -1101,6 +1103,7 @@ public final class ViewRoot extends Handler implements ViewParent, mAttachInfo.mDrawingTime = SystemClock.uptimeMillis(); canvas.translate(0, -yoff); + mView.mPrivateFlags |= View.DRAWN; mView.draw(canvas); canvas.translate(0, yoff); @@ -1139,6 +1142,8 @@ public final class ViewRoot extends Handler implements ViewParent, Canvas canvas; try { canvas = surface.lockCanvas(dirty); + // TODO: Do this in native + canvas.setDensityScale(mView.getResources().getDisplayMetrics().density); } catch (Surface.OutOfResourcesException e) { Log.e("ViewRoot", "OutOfResourcesException locking surface", e); // TODO: we should ask the window manager to do something! @@ -1175,6 +1180,7 @@ public final class ViewRoot extends Handler implements ViewParent, dirty.setEmpty(); mAttachInfo.mDrawingTime = SystemClock.uptimeMillis(); canvas.translate(0, -yoff); + mView.mPrivateFlags |= View.DRAWN; mView.draw(canvas); canvas.translate(0, yoff); @@ -1197,6 +1203,23 @@ public final class ViewRoot extends Handler implements ViewParent, if (LOCAL_LOGV) { Log.v("ViewRoot", "Surface " + surface + " unlockCanvasAndPost"); } + + } else if (mWidth == 0 || mHeight == 0) { + // This is a special case where a window dimension is 0 -- we + // normally wouldn't draw anything because we have an empty + // dirty rect, but the surface flinger may be waiting for us to + // draw the window before it stops freezing the screen, so we + // need to diddle it like this to keep it from getting stuck. + Canvas canvas; + try { + canvas = surface.lockCanvas(dirty); + } catch (Surface.OutOfResourcesException e) { + Log.e("ViewRoot", "OutOfResourcesException locking surface", e); + // TODO: we should ask the window manager to do something! + // for now we just do nothing + return; + } + surface.unlockCanvasAndPost(canvas); } if (scrolling) { @@ -1414,6 +1437,7 @@ public final class ViewRoot extends Handler implements ViewParent, public final static int FINISHED_EVENT = 1010; public final static int DISPATCH_KEY_FROM_IME = 1011; public final static int FINISH_INPUT_CONNECTION = 1012; + public final static int CHECK_FOCUS = 1013; @Override public void handleMessage(Message msg) { @@ -1422,11 +1446,9 @@ public final class ViewRoot extends Handler implements ViewParent, ((View) msg.obj).invalidate(); break; case View.AttachInfo.INVALIDATE_RECT_MSG: - int left = msg.arg1 >>> 16; - int top = msg.arg1 & 0xFFFF; - int right = msg.arg2 >>> 16; - int bottom = msg.arg2 & 0xFFFF; - ((View) msg.obj).invalidate(left, top, right, bottom); + final View.AttachInfo.InvalidateInfo info = (View.AttachInfo.InvalidateInfo) msg.obj; + info.target.invalidate(info.left, info.top, info.right, info.bottom); + info.release(); break; case DO_TRAVERSAL: if (mProfile) { @@ -1478,7 +1500,7 @@ public final class ViewRoot extends Handler implements ViewParent, event.offsetLocation(0, mCurScrollY); handled = mView.dispatchTouchEvent(event); if (!handled && isDown) { - int edgeSlop = ViewConfiguration.getEdgeSlop(); + int edgeSlop = mViewConfiguration.getScaledEdgeSlop(); final int edgeFlags = event.getEdgeFlags(); int direction = View.FOCUS_UP; @@ -1615,7 +1637,7 @@ public final class ViewRoot extends Handler implements ViewParent, dispatchDetachedFromWindow(); break; case DISPATCH_KEY_FROM_IME: - if (LOCAL_LOGV) Log.v( + if (true) Log.v( "ViewRoot", "Dispatching key " + msg.obj + " from IME to " + mView); deliverKeyEventToViewHierarchy((KeyEvent)msg.obj, false); @@ -1626,6 +1648,12 @@ public final class ViewRoot extends Handler implements ViewParent, imm.reportFinishInputConnection((InputConnection)msg.obj); } } break; + case CHECK_FOCUS: { + InputMethodManager imm = InputMethodManager.peekInstance(); + if (imm != null) { + imm.checkFocus((View)msg.obj); + } + } break; } } @@ -2042,8 +2070,10 @@ public final class ViewRoot extends Handler implements ViewParent, } private void deliverKeyEvent(KeyEvent event, boolean sendDone) { - boolean handled = false; - handled = mView.dispatchKeyEventPreIme(event); + // 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) : true; if (handled) { if (sendDone) { if (LOCAL_LOGV) Log.v( @@ -2061,7 +2091,7 @@ public final class ViewRoot extends Handler implements ViewParent, if (WindowManager.LayoutParams.mayUseInputMethod( mWindowAttributes.flags)) { InputMethodManager imm = InputMethodManager.peekInstance(); - if (imm != null && mView != null && imm.isActive()) { + if (imm != null && mView != null) { int seq = enqueuePendingEvent(event, sendDone); if (DEBUG_IMF) Log.v(TAG, "Sending key event to IME: seq=" + seq + " event=" + event); @@ -2748,7 +2778,6 @@ public final class ViewRoot extends Handler implements ViewParent, synchronized (mActions) { final ArrayList<HandlerAction> actions = mActions; - final int count = actions.size(); while (actions.remove(handlerAction)) { // Keep going @@ -2776,7 +2805,20 @@ public final class ViewRoot extends Handler implements ViewParent, @Override public boolean equals(Object o) { - return action.equals(o); + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + HandlerAction that = (HandlerAction) o; + + return !(action != null ? !action.equals(that.action) : that.action != null); + + } + + @Override + public int hashCode() { + int result = action != null ? action.hashCode() : 0; + result = 31 * result + (int) (delay ^ (delay >>> 32)); + return result; } } } diff --git a/core/java/android/view/VolumePanel.java b/core/java/android/view/VolumePanel.java index f4d0fde..a573983 100644 --- a/core/java/android/view/VolumePanel.java +++ b/core/java/android/view/VolumePanel.java @@ -16,18 +16,17 @@ package android.view; -import android.media.ToneGenerator; -import android.media.AudioManager; -import android.media.AudioService; -import android.media.AudioSystem; +import android.bluetooth.HeadsetBase; import android.content.Context; import android.content.Intent; -import android.content.IntentFilter; import android.content.res.Resources; +import android.media.AudioManager; +import android.media.AudioService; +import android.media.AudioSystem; +import android.media.ToneGenerator; import android.os.Handler; import android.os.Message; import android.os.Vibrator; -import android.text.TextUtils; import android.util.Config; import android.util.Log; import android.widget.ImageView; @@ -39,7 +38,7 @@ import android.widget.Toast; * Handle the volume up and down keys. * * This code really should be moved elsewhere. - * + * * @hide */ public class VolumePanel extends Handler @@ -54,7 +53,7 @@ public class VolumePanel extends Handler * PhoneWindow will implement this part. */ public static final int PLAY_SOUND_DELAY = 300; - + /** * The delay before vibrating. This small period exists so if the user is * moving to silent mode, it will not emit a short vibrate (it normally @@ -64,28 +63,30 @@ public class VolumePanel extends Handler public static final int VIBRATE_DELAY = 300; private static final int VIBRATE_DURATION = 300; - private static final int BEEP_DURATION = 150; + private static final int BEEP_DURATION = 150; private static final int MAX_VOLUME = 100; private static final int FREE_DELAY = 10000; - + private static final int MSG_VOLUME_CHANGED = 0; private static final int MSG_FREE_RESOURCES = 1; private static final int MSG_PLAY_SOUND = 2; private static final int MSG_STOP_SOUNDS = 3; private static final int MSG_VIBRATE = 4; - + private static final int RINGTONE_VOLUME_TEXT = com.android.internal.R.string.volume_ringtone; private static final int MUSIC_VOLUME_TEXT = com.android.internal.R.string.volume_music; private static final int INCALL_VOLUME_TEXT = com.android.internal.R.string.volume_call; private static final int ALARM_VOLUME_TEXT = com.android.internal.R.string.volume_alarm; private static final int UNKNOWN_VOLUME_TEXT = com.android.internal.R.string.volume_unknown; private static final int NOTIFICATION_VOLUME_TEXT = - com.android.internal.R.string.volume_notification; - + com.android.internal.R.string.volume_notification; + private static final int BLUETOOTH_INCALL_VOLUME_TEXT = + com.android.internal.R.string.volume_bluetooth_call; + protected Context mContext; private AudioManager mAudioManager; protected AudioService mAudioService; - + private final Toast mToast; private final View mView; private final TextView mMessage; @@ -117,13 +118,13 @@ public class VolumePanel extends Handler mToneGenerators = new ToneGenerator[AudioSystem.getNumStreamTypes()]; mVibrator = new Vibrator(); } - + public void postVolumeChanged(int streamType, int flags) { if (hasMessages(MSG_VOLUME_CHANGED)) return; removeMessages(MSG_FREE_RESOURCES); obtainMessage(MSG_VOLUME_CHANGED, streamType, flags).sendToTarget(); } - + /** * Override this if you have other work to do when the volume changes (for * example, vibrating, playing a sound, etc.). Make sure to call through to @@ -132,31 +133,31 @@ public class VolumePanel extends Handler protected void onVolumeChanged(int streamType, int flags) { if (LOGD) Log.d(TAG, "onVolumeChanged(streamType: " + streamType + ", flags: " + flags + ")"); - + if ((flags & AudioManager.FLAG_SHOW_UI) != 0) { onShowVolumeChanged(streamType, flags); } - + if ((flags & AudioManager.FLAG_PLAY_SOUND) != 0) { removeMessages(MSG_PLAY_SOUND); sendMessageDelayed(obtainMessage(MSG_PLAY_SOUND, streamType, flags), PLAY_SOUND_DELAY); } - + if ((flags & AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE) != 0) { removeMessages(MSG_PLAY_SOUND); removeMessages(MSG_VIBRATE); onStopSounds(); } - + removeMessages(MSG_FREE_RESOURCES); - sendMessageDelayed(obtainMessage(MSG_FREE_RESOURCES), FREE_DELAY); + sendMessageDelayed(obtainMessage(MSG_FREE_RESOURCES), FREE_DELAY); } protected void onShowVolumeChanged(int streamType, int flags) { int index = mAudioService.getStreamVolume(streamType); int message = UNKNOWN_VOLUME_TEXT; int additionalMessage = 0; - + if (LOGD) { Log.d(TAG, "onShowVolumeChanged(streamType: " + streamType + ", flags: " + flags + "), index: " + index); @@ -166,13 +167,13 @@ public class VolumePanel extends Handler int max = mAudioService.getStreamMaxVolume(streamType); switch (streamType) { - + case AudioManager.STREAM_RING: { message = RINGTONE_VOLUME_TEXT; setRingerIcon(index); break; } - + case AudioManager.STREAM_MUSIC: { message = MUSIC_VOLUME_TEXT; if (mAudioManager.isBluetoothA2dpOn()) { @@ -184,7 +185,7 @@ public class VolumePanel extends Handler } break; } - + case AudioManager.STREAM_VOICE_CALL: { /* * For in-call voice call volume, there is no inaudible volume. @@ -194,13 +195,7 @@ public class VolumePanel extends Handler index++; max++; message = INCALL_VOLUME_TEXT; - if (mAudioManager.isBluetoothScoOn()) { - additionalMessage = - com.android.internal.R.string.volume_call_hint_playing_through_bluetooth; - setLargeIcon(com.android.internal.R.drawable.ic_volume_bluetooth_in_call); - } else { - setSmallIcon(index); - } + setSmallIcon(index); break; } @@ -209,12 +204,25 @@ public class VolumePanel extends Handler setSmallIcon(index); break; } - + case AudioManager.STREAM_NOTIFICATION: { message = NOTIFICATION_VOLUME_TEXT; setSmallIcon(index); break; } + + case AudioManager.STREAM_BLUETOOTH_SCO: { + /* + * For in-call voice call volume, there is no inaudible volume. + * Rescale the UI control so the progress bar doesn't go all + * the way to zero and don't show the mute icon. + */ + index++; + max++; + message = BLUETOOTH_INCALL_VOLUME_TEXT; + setLargeIcon(com.android.internal.R.drawable.ic_volume_bluetooth_in_call); + break; + } } String messageString = Resources.getSystem().getString(message); @@ -228,25 +236,25 @@ public class VolumePanel extends Handler mAdditionalMessage.setVisibility(View.VISIBLE); mAdditionalMessage.setText(Resources.getSystem().getString(additionalMessage)); } - + if (max != mLevel.getMax()) { mLevel.setMax(max); } mLevel.setProgress(index); - + mToast.setView(mView); mToast.setDuration(Toast.LENGTH_SHORT); mToast.setGravity(Gravity.TOP, 0, 0); mToast.show(); - + // Do a little vibrate if applicable (only when going into vibrate mode) - if ((flags & AudioManager.FLAG_VIBRATE) != 0 && + if ((flags & AudioManager.FLAG_VIBRATE) != 0 && mAudioService.isStreamAffectedByRingerMode(streamType) && mAudioService.getRingerMode() == AudioManager.RINGER_MODE_VIBRATE && mAudioService.shouldVibrate(AudioManager.VIBRATE_TYPE_RINGER)) { sendMessageDelayed(obtainMessage(MSG_VIBRATE), VIBRATE_DELAY); } - + } protected void onPlaySound(int streamType, int flags) { @@ -256,7 +264,7 @@ public class VolumePanel extends Handler // Force stop right now onStopSounds(); } - + synchronized (this) { ToneGenerator toneGen = getOrCreateToneGenerator(streamType); toneGen.startTone(ToneGenerator.TONE_PROP_BEEP); @@ -266,7 +274,7 @@ public class VolumePanel extends Handler } protected void onStopSounds() { - + synchronized (this) { int numStreamTypes = AudioSystem.getNumStreamTypes(); for (int i = numStreamTypes - 1; i >= 0; i--) { @@ -277,17 +285,17 @@ public class VolumePanel extends Handler } } } - + protected void onVibrate() { - + // Make sure we ended up in vibrate ringer mode if (mAudioService.getRingerMode() != AudioManager.RINGER_MODE_VIBRATE) { return; } - + mVibrator.vibrate(VIBRATE_DURATION); } - + /** * Lock on this VolumePanel instance as long as you use the returned ToneGenerator. */ @@ -303,13 +311,13 @@ public class VolumePanel extends Handler /** * Makes the small icon visible, and hides the large icon. - * + * * @param index The volume index, where 0 means muted. */ private void setSmallIcon(int index) { mLargeStreamIcon.setVisibility(View.GONE); mSmallStreamIcon.setVisibility(View.VISIBLE); - + mSmallStreamIcon.setImageResource(index == 0 ? com.android.internal.R.drawable.ic_volume_off_small : com.android.internal.R.drawable.ic_volume_small); @@ -317,7 +325,7 @@ public class VolumePanel extends Handler /** * Makes the large image view visible with the given icon. - * + * * @param resId The icon to display. */ private void setLargeIcon(int resId) { @@ -329,7 +337,7 @@ public class VolumePanel extends Handler /** * Makes the ringer icon visible with an icon that is chosen * based on the current ringer mode. - * + * * @param index */ private void setRingerIcon(int index) { @@ -350,13 +358,13 @@ public class VolumePanel extends Handler } mLargeStreamIcon.setImageResource(icon); } - + protected void onFreeResources() { // We'll keep the views, just ditch the cached drawable and hence // bitmaps mSmallStreamIcon.setImageDrawable(null); mLargeStreamIcon.setImageDrawable(null); - + synchronized (this) { for (int i = mToneGenerators.length - 1; i >= 0; i--) { if (mToneGenerators[i] != null) { @@ -366,26 +374,26 @@ public class VolumePanel extends Handler } } } - + @Override public void handleMessage(Message msg) { switch (msg.what) { - + case MSG_VOLUME_CHANGED: { onVolumeChanged(msg.arg1, msg.arg2); break; } - + case MSG_FREE_RESOURCES: { onFreeResources(); break; } - + case MSG_STOP_SOUNDS: { onStopSounds(); break; } - + case MSG_PLAY_SOUND: { onPlaySound(msg.arg1, msg.arg2); break; @@ -395,8 +403,8 @@ public class VolumePanel extends Handler onVibrate(); break; } - + } } - + } diff --git a/core/java/android/view/Window.java b/core/java/android/view/Window.java index a68436b..428de67 100644 --- a/core/java/android/view/Window.java +++ b/core/java/android/view/Window.java @@ -525,6 +525,21 @@ public abstract class Window { } /** + * Specify custom animations to use for the window, as per + * {@link WindowManager.LayoutParams#windowAnimations + * WindowManager.LayoutParams.windowAnimations}. Providing anything besides + * 0 here will override the animations the window would + * normally retrieve from its theme. + */ + public void setWindowAnimations(int resId) { + final WindowManager.LayoutParams attrs = getAttributes(); + attrs.windowAnimations = resId; + if (mCallback != null) { + mCallback.onWindowAttributesChanged(attrs); + } + } + + /** * Specify an explicit soft input mode to use for the window, as per * {@link WindowManager.LayoutParams#softInputMode * WindowManager.LayoutParams.softInputMode}. Providing anything besides diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index 7e47ad1..d08a6fa 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -16,6 +16,7 @@ package android.view; +import android.content.pm.ActivityInfo; import android.graphics.PixelFormat; import android.os.IBinder; import android.os.Parcel; @@ -126,8 +127,6 @@ public interface WindowManager extends ViewManager { * @see #TYPE_APPLICATION_MEDIA * @see #TYPE_APPLICATION_SUB_PANEL * @see #TYPE_APPLICATION_ATTACHED_DIALOG - * @see #TYPE_INPUT_METHOD - * @see #TYPE_INPUT_METHOD_DIALOG * @see #TYPE_STATUS_BAR * @see #TYPE_SEARCH_BAR * @see #TYPE_PHONE @@ -645,6 +644,17 @@ public interface WindowManager extends ViewManager { */ public String packageName = null; + /** + * Specific orientation value for a window. + * May be any of the same values allowed + * for {@link android.content.pm.ActivityInfo#screenOrientation}. + * If not set, a default value of + * {@link android.content.pm.ActivityInfo#SCREEN_ORIENTATION_UNSPECIFIED} + * will be used. + */ + public int screenOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; + + public LayoutParams() { super(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT); type = TYPE_APPLICATION; @@ -722,6 +732,7 @@ public interface WindowManager extends ViewManager { out.writeStrongBinder(token); out.writeString(packageName); TextUtils.writeToParcel(mTitle, out, parcelableFlags); + out.writeInt(screenOrientation); } public static final Parcelable.Creator<LayoutParams> CREATOR @@ -755,6 +766,7 @@ public interface WindowManager extends ViewManager { token = in.readStrongBinder(); packageName = in.readString(); mTitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + screenOrientation = in.readInt(); } public static final int LAYOUT_CHANGED = 1<<0; @@ -767,6 +779,7 @@ public interface WindowManager extends ViewManager { public static final int ALPHA_CHANGED = 1<<7; public static final int MEMORY_TYPE_CHANGED = 1<<8; public static final int SOFT_INPUT_MODE_CHANGED = 1<<9; + public static final int SCREEN_ORIENTATION_CHANGED = 1<<10; public final int copyFrom(LayoutParams o) { int changes = 0; @@ -862,6 +875,10 @@ public interface WindowManager extends ViewManager { changes |= DIM_AMOUNT_CHANGED; } + if (screenOrientation != o.screenOrientation) { + screenOrientation = o.screenOrientation; + changes |= SCREEN_ORIENTATION_CHANGED; + } return changes; } @@ -907,6 +924,10 @@ public interface WindowManager extends ViewManager { sb.append(" wanim=0x"); sb.append(Integer.toHexString(windowAnimations)); } + if (screenOrientation != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) { + sb.append("or="); + sb.append(screenOrientation); + } sb.append('}'); return sb.toString(); } diff --git a/core/java/android/view/WindowManagerPolicy.java b/core/java/android/view/WindowManagerPolicy.java index 6af4915..542b35f 100644 --- a/core/java/android/view/WindowManagerPolicy.java +++ b/core/java/android/view/WindowManagerPolicy.java @@ -289,16 +289,18 @@ public interface WindowManagerPolicy { * Can be called by the policy to force a window to be hidden, * regardless of whether the client or window manager would like * it shown. Must be called with the window manager lock held. + * Returns true if {@link #showLw} was last called for the window. */ - public void hideLw(boolean doAnimation); + public boolean hideLw(boolean doAnimation); /** * Can be called to undo the effect of {@link #hideLw}, allowing a * window to be shown as long as the window manager and client would * also like it to be shown. Must be called with the window manager * lock held. + * Returns true if {@link #hideLw} was last called for the window. */ - public void showLw(boolean doAnimation); + public boolean showLw(boolean doAnimation); } /** No transition happening. */ @@ -735,10 +737,17 @@ public interface WindowManagerPolicy { * ActivityInfo.SCREEN_ORIENTATION_PORTRAIT}), return a surface * rotation. */ - public int rotationForOrientation(int orientation); + public int rotationForOrientation(int orientation, int lastRotation, + boolean displayEnabled); /** - * Called when the system is mostly done booting + * Called when the system is mostly done booting to dentermine whether + * the system should go into safe mode. + */ + public boolean detectSafeMode(); + + /** + * Called when the system is mostly done booting. */ public void systemReady(); diff --git a/core/java/android/view/animation/Animation.java b/core/java/android/view/animation/Animation.java index 9264398..c96b3e5 100644 --- a/core/java/android/view/animation/Animation.java +++ b/core/java/android/view/animation/Animation.java @@ -20,13 +20,14 @@ import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; import android.util.TypedValue; +import android.graphics.RectF; /** * Abstraction for an Animation that can be applied to Views, Surfaces, or * other objects. See the {@link android.view.animation animation package * description file}. */ -public abstract class Animation { +public abstract class Animation implements Cloneable { /** * Repeat the animation indefinitely. */ @@ -174,8 +175,12 @@ public abstract class Animation { */ private int mZAdjustment; - // Indicates what was the last value returned by getTransformation() private boolean mMore = true; + private boolean mOneMoreTime = true; + + RectF mPreviousRegion = new RectF(); + Transformation mTransformation = new Transformation(); + Transformation mPreviousTransformation = new Transformation(); /** * Creates a new animation with a duration of 0ms, the default interpolator, with @@ -217,16 +222,28 @@ public abstract class Animation { a.recycle(); } + @Override + protected Animation clone() throws CloneNotSupportedException { + final Animation animation = (Animation) super.clone(); + animation.mPreviousRegion = new RectF(); + animation.mTransformation = new Transformation(); + animation.mPreviousTransformation = new Transformation(); + return animation; + } + /** * Reset the initialization state of this animation. * * @see #initialize(int, int, int, int) */ public void reset() { + mPreviousRegion.setEmpty(); + mPreviousTransformation.clear(); mInitialized = false; mCycleFlip = false; mRepeated = 0; mMore = true; + mOneMoreTime = true; } /** @@ -255,10 +272,8 @@ public abstract class Animation { * @param parentHeight Height of the animated object's parent */ public void initialize(int width, int height, int parentWidth, int parentHeight) { + reset(); mInitialized = true; - mCycleFlip = false; - mRepeated = 0; - mMore = true; } /** @@ -707,6 +722,11 @@ public abstract class Animation { } } + if (!mMore && mOneMoreTime) { + mOneMoreTime = false; + return true; + } + return mMore; } @@ -765,7 +785,54 @@ public abstract class Animation { return value; } } - + + /** + * @param left + * @param top + * @param right + * @param bottom + * @param invalidate + * @param transformation + * + * @hide + */ + public void getInvalidateRegion(int left, int top, int right, int bottom, + RectF invalidate, Transformation transformation) { + + final RectF previousRegion = mPreviousRegion; + + invalidate.set(left, top, right, bottom); + transformation.getMatrix().mapRect(invalidate); + invalidate.union(previousRegion); + + previousRegion.set(left, top, right, bottom); + transformation.getMatrix().mapRect(previousRegion); + + final Transformation tempTransformation = mTransformation; + final Transformation previousTransformation = mPreviousTransformation; + + tempTransformation.set(transformation); + transformation.set(previousTransformation); + previousTransformation.set(tempTransformation); + } + + /** + * @param left + * @param top + * @param right + * @param bottom + * + * @hide + */ + public void initializeInvalidateRegion(int left, int top, int right, int bottom) { + final RectF region = mPreviousRegion; + region.set(left, top, right, bottom); + if (mFillBefore) { + final Transformation previousTransformation = mPreviousTransformation; + applyTransformation(0.0f, previousTransformation); + } + } + /** * Utility class to parse a string description of a size. */ diff --git a/core/java/android/view/animation/AnimationSet.java b/core/java/android/view/animation/AnimationSet.java index 688da70..7b56f00 100644 --- a/core/java/android/view/animation/AnimationSet.java +++ b/core/java/android/view/animation/AnimationSet.java @@ -19,6 +19,7 @@ package android.view.animation; import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; +import android.graphics.RectF; import java.util.ArrayList; import java.util.List; @@ -39,6 +40,7 @@ public class AnimationSet extends Animation { private static final int PROPERTY_SHARE_INTERPOLATOR_MASK = 0x10; private static final int PROPERTY_DURATION_MASK = 0x20; private static final int PROPERTY_MORPH_MATRIX_MASK = 0x40; + private static final int PROPERTY_CHANGE_BOUNDS_MASK = 0x80; private int mFlags = 0; @@ -82,6 +84,22 @@ public class AnimationSet extends Animation { init(); } + @Override + protected AnimationSet clone() throws CloneNotSupportedException { + final AnimationSet animation = (AnimationSet) super.clone(); + animation.mTempTransformation = new Transformation(); + animation.mAnimations = new ArrayList<Animation>(); + + final int count = mAnimations.size(); + final ArrayList<Animation> animations = mAnimations; + + for (int i = 0; i < count; i++) { + animation.mAnimations.add(animations.get(i).clone()); + } + + return animation; + } + private void setFlag(int mask, boolean value) { if (value) { mFlags |= mask; @@ -145,6 +163,11 @@ public class AnimationSet extends Animation { mFlags |= PROPERTY_MORPH_MATRIX_MASK; } + boolean changeBounds = (mFlags & PROPERTY_CHANGE_BOUNDS_MASK) == 0; + if (changeBounds && a.willChangeTransformationMatrix()) { + mFlags |= PROPERTY_CHANGE_BOUNDS_MASK; + } + if (mAnimations.size() == 1) { mDuration = a.getStartOffset() + a.getDuration(); mLastEnd = mStartOffset + mDuration; @@ -239,7 +262,54 @@ public class AnimationSet extends Animation { } return duration; } - + + /** + * @hide + */ + public void getInvalidateRegion(int left, int top, int right, int bottom, + RectF invalidate, Transformation transformation) { + + final RectF previousRegion = mPreviousRegion; + + invalidate.set(left, top, right, bottom); + transformation.getMatrix().mapRect(invalidate); + invalidate.union(previousRegion); + + previousRegion.set(left, top, right, bottom); + transformation.getMatrix().mapRect(previousRegion); + + final Transformation tempTransformation = mTransformation; + final Transformation previousTransformation = mPreviousTransformation; + + tempTransformation.set(transformation); + transformation.set(previousTransformation); + previousTransformation.set(tempTransformation); + } + + /** + * @hide + */ + public void initializeInvalidateRegion(int left, int top, int right, int bottom) { + final RectF region = mPreviousRegion; + region.set(left, top, right, bottom); + + if (mFillBefore) { + final int count = mAnimations.size(); + final ArrayList<Animation> animations = mAnimations; + final Transformation temp = mTempTransformation; + + final Transformation previousTransformation = mPreviousTransformation; + + for (int i = count - 1; i >= 0; --i) { + final Animation a = animations.get(i); + + temp.clear(); + a.applyTransformation(0.0f, temp); + previousTransformation.compose(temp); + } + } + } + /** * The transformation of an animation set is the concatenation of all of its * component animations. @@ -313,7 +383,7 @@ public class AnimationSet extends Animation { == PROPERTY_SHARE_INTERPOLATOR_MASK; boolean startOffsetSet = (mFlags & PROPERTY_START_OFFSET_MASK) == PROPERTY_START_OFFSET_MASK; - + if (shareInterpolator) { ensureInterpolator(); } @@ -327,7 +397,13 @@ public class AnimationSet extends Animation { final int repeatMode = mRepeatMode; final Interpolator interpolator = mInterpolator; final long startOffset = mStartOffset; - + + + long[] storedOffsets = mStoredOffsets; + if (storedOffsets == null || storedOffsets.length != count) { + storedOffsets = mStoredOffsets = new long[count]; + } + for (int i = 0; i < count; i++) { Animation a = children.get(i); if (durationSet) { @@ -346,42 +422,36 @@ public class AnimationSet extends Animation { a.setInterpolator(interpolator); } if (startOffsetSet) { - a.setStartOffset(startOffset); + long offset = a.getStartOffset(); + a.setStartOffset(offset + startOffset); + storedOffsets[i] = offset; } a.initialize(width, height, parentWidth, parentHeight); } } - /** - * @hide - * @param startOffset the startOffset to add to the children's startOffset - */ - void saveChildrenStartOffset(long startOffset) { - final ArrayList<Animation> children = mAnimations; - final int count = children.size(); - long[] storedOffsets = mStoredOffsets = new long[count]; - - for (int i = 0; i < count; i++) { - Animation animation = children.get(i); - long offset = animation.getStartOffset(); - animation.setStartOffset(offset + startOffset); - storedOffsets[i] = offset; - } + @Override + public void reset() { + super.reset(); + restoreChildrenStartOffset(); } /** * @hide */ void restoreChildrenStartOffset() { + final long[] offsets = mStoredOffsets; + if (offsets == null) return; + final ArrayList<Animation> children = mAnimations; final int count = children.size(); - final long[] offsets = mStoredOffsets; + for (int i = 0; i < count; i++) { children.get(i).setStartOffset(offsets[i]); } } - + /** * @return All the child animations in this AnimationSet. Note that * this may include other AnimationSets, which are not expanded. @@ -394,4 +464,9 @@ public class AnimationSet extends Animation { public boolean willChangeTransformationMatrix() { return (mFlags & PROPERTY_MORPH_MATRIX_MASK) == PROPERTY_MORPH_MATRIX_MASK; } + + @Override + public boolean willChangeBounds() { + return (mFlags & PROPERTY_CHANGE_BOUNDS_MASK) == PROPERTY_CHANGE_BOUNDS_MASK; + } } diff --git a/core/java/android/view/animation/LayoutAnimationController.java b/core/java/android/view/animation/LayoutAnimationController.java index 9cfa8d7..882e738 100644 --- a/core/java/android/view/animation/LayoutAnimationController.java +++ b/core/java/android/view/animation/LayoutAnimationController.java @@ -318,9 +318,16 @@ public class LayoutAnimationController { * @see #getDelayForView(android.view.View) */ public final Animation getAnimationForView(View view) { - final long delay = getDelayForView(view); + final long delay = getDelayForView(view) + mAnimation.getStartOffset(); mMaxDelay = Math.max(mMaxDelay, delay); - return new DelayedAnimation(delay, mAnimation); + + try { + final Animation animation = mAnimation.clone(); + animation.setStartOffset(delay); + return animation; + } catch (CloneNotSupportedException e) { + return null; + } } /** @@ -425,149 +432,4 @@ public class LayoutAnimationController { */ public int index; } - - /** - * Encapsulates an animation and delays its start offset by a specified - * amount. This allows to reuse the same base animation for various views - * and get the effect of running multiple instances of the animation at - * different times. - */ - private static class DelayedAnimation extends Animation { - private final long mDelay; - private final Animation mAnimation; - - /** - * Creates a new delayed animation that will delay the controller's - * animation by the specified delay in milliseconds. - * - * @param delay the delay in milliseconds by which to offset the - * @param animation the animation to delay - */ - private DelayedAnimation(long delay, Animation animation) { - mDelay = delay; - mAnimation = animation; - } - - @Override - public boolean isInitialized() { - return mAnimation.isInitialized(); - } - - @Override - public void initialize(int width, int height, int parentWidth, int parentHeight) { - mAnimation.initialize(width, height, parentWidth, parentHeight); - } - - @Override - public void reset() { - mAnimation.reset(); - } - - @Override - public boolean getTransformation(long currentTime, Transformation outTransformation) { - final long oldOffset = mAnimation.getStartOffset(); - final boolean isSet = mAnimation instanceof AnimationSet; - if (isSet) { - AnimationSet set = ((AnimationSet) mAnimation); - set.saveChildrenStartOffset(mDelay); - } - mAnimation.setStartOffset(oldOffset + mDelay); - - boolean result = mAnimation.getTransformation(currentTime, - outTransformation); - - if (isSet) { - AnimationSet set = ((AnimationSet) mAnimation); - set.restoreChildrenStartOffset(); - } - mAnimation.setStartOffset(oldOffset); - - return result; - } - - @Override - public void setStartTime(long startTimeMillis) { - mAnimation.setStartTime(startTimeMillis); - } - - @Override - public long getStartTime() { - return mAnimation.getStartTime(); - } - - @Override - public void setInterpolator(Interpolator i) { - mAnimation.setInterpolator(i); - } - - @Override - public void setStartOffset(long startOffset) { - mAnimation.setStartOffset(startOffset); - } - - @Override - public void setDuration(long durationMillis) { - mAnimation.setDuration(durationMillis); - } - - @Override - public void scaleCurrentDuration(float scale) { - mAnimation.scaleCurrentDuration(scale); - } - - @Override - public void setRepeatMode(int repeatMode) { - mAnimation.setRepeatMode(repeatMode); - } - - @Override - public void setFillBefore(boolean fillBefore) { - mAnimation.setFillBefore(fillBefore); - } - - @Override - public void setFillAfter(boolean fillAfter) { - mAnimation.setFillAfter(fillAfter); - } - - @Override - public Interpolator getInterpolator() { - return mAnimation.getInterpolator(); - } - - @Override - public long getDuration() { - return mAnimation.getDuration(); - } - - @Override - public long getStartOffset() { - return mAnimation.getStartOffset() + mDelay; - } - - @Override - public int getRepeatMode() { - return mAnimation.getRepeatMode(); - } - - @Override - public boolean getFillBefore() { - return mAnimation.getFillBefore(); - } - - @Override - public boolean getFillAfter() { - return mAnimation.getFillAfter(); - } - - @Override - public boolean willChangeTransformationMatrix() { - return mAnimation.willChangeTransformationMatrix(); - } - - @Override - public boolean willChangeBounds() { - return mAnimation.willChangeBounds(); - } - } } diff --git a/core/java/android/view/inputmethod/BaseInputConnection.java b/core/java/android/view/inputmethod/BaseInputConnection.java index a6ce293..56c6c92 100644 --- a/core/java/android/view/inputmethod/BaseInputConnection.java +++ b/core/java/android/view/inputmethod/BaseInputConnection.java @@ -17,35 +17,365 @@ package android.view.inputmethod; import android.content.Context; +import android.content.res.TypedArray; +import android.os.Bundle; import android.os.Handler; -import android.os.Message; +import android.os.SystemClock; +import android.text.Editable; +import android.text.NoCopySpan; +import android.text.Selection; +import android.text.Spannable; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.TextUtils; +import android.text.method.MetaKeyKeyListener; +import android.util.Log; +import android.util.LogPrinter; +import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.View; import android.view.ViewRoot; +class ComposingText implements NoCopySpan { +} + /** * Base class for implementors of the InputConnection interface, taking care - * of implementing common system-oriented parts of the functionality. + * of most of the common behavior for providing a connection to an Editable. + * Implementors of this class will want to be sure to implement + * {@link #getEditable} to provide access to their own editable object. */ -public abstract class BaseInputConnection implements InputConnection { +public class BaseInputConnection implements InputConnection { + private static final boolean DEBUG = false; + private static final String TAG = "BaseInputConnection"; + static final Object COMPOSING = new ComposingText(); + final InputMethodManager mIMM; final Handler mH; final View mTargetView; + final boolean mDummyMode; + + private Object[] mDefaultComposingSpans; + + Editable mEditable; + KeyCharacterMap mKeyCharacterMap; - BaseInputConnection(InputMethodManager mgr) { + BaseInputConnection(InputMethodManager mgr, boolean dummyMode) { mIMM = mgr; mTargetView = null; mH = null; + mDummyMode = dummyMode; } - public BaseInputConnection(View targetView) { + public BaseInputConnection(View targetView, boolean dummyMode) { mIMM = (InputMethodManager)targetView.getContext().getSystemService( Context.INPUT_METHOD_SERVICE); mH = targetView.getHandler(); mTargetView = targetView; + mDummyMode = dummyMode; + } + + public static final void removeComposingSpans(Spannable text) { + text.removeSpan(COMPOSING); + Object[] sps = text.getSpans(0, text.length(), Object.class); + if (sps != null) { + for (int i=sps.length-1; i>=0; i--) { + Object o = sps[i]; + if ((text.getSpanFlags(o)&Spanned.SPAN_COMPOSING) != 0) { + text.removeSpan(o); + } + } + } + } + + public static void setComposingSpans(Spannable text) { + final Object[] sps = text.getSpans(0, text.length(), Object.class); + if (sps != null) { + for (int i=sps.length-1; i>=0; i--) { + final Object o = sps[i]; + if (o == COMPOSING) { + text.removeSpan(o); + continue; + } + final int fl = text.getSpanFlags(o); + if ((fl&(Spanned.SPAN_COMPOSING|Spanned.SPAN_POINT_MARK_MASK)) + != (Spanned.SPAN_COMPOSING|Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)) { + text.setSpan(o, text.getSpanStart(o), text.getSpanEnd(o), + (fl&Spanned.SPAN_POINT_MARK_MASK) + | Spanned.SPAN_COMPOSING + | Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + } + + text.setSpan(COMPOSING, 0, text.length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | Spanned.SPAN_COMPOSING); + } + + public static int getComposingSpanStart(Spannable text) { + return text.getSpanStart(COMPOSING); + } + + public static int getComposingSpanEnd(Spannable text) { + return text.getSpanEnd(COMPOSING); + } + + /** + * Return the target of edit operations. The default implementation + * returns its own fake editable that is just used for composing text; + * subclasses that are real text editors should override this and + * supply their own. + */ + public Editable getEditable() { + if (mEditable == null) { + mEditable = Editable.Factory.getInstance().newEditable(""); + Selection.setSelection(mEditable, 0); + } + return mEditable; } /** + * Default implementation does nothing. + */ + public boolean beginBatchEdit() { + return false; + } + + /** + * Default implementation does nothing. + */ + public boolean endBatchEdit() { + return false; + } + + /** + * Default implementation uses + * {@link MetaKeyKeyListener#clearMetaKeyState(long, int) + * MetaKeyKeyListener.clearMetaKeyState(long, int)} to clear the state. + */ + public boolean clearMetaKeyStates(int states) { + final Editable content = getEditable(); + if (content == null) return false; + MetaKeyKeyListener.clearMetaKeyState(content, states); + return true; + } + + /** + * Default implementation does nothing. + */ + public boolean commitCompletion(CompletionInfo text) { + return false; + } + + /** + * Default implementation replaces any existing composing text with + * the given text. In addition, only if dummy mode, a key event is + * sent for the new text and the current editable buffer cleared. + */ + public boolean commitText(CharSequence text, int newCursorPosition) { + if (DEBUG) Log.v(TAG, "commitText " + text); + replaceText(text, newCursorPosition, false); + sendCurrentText(); + return true; + } + + /** + * The default implementation performs the deletion around the current + * selection position of the editable text. + */ + public boolean deleteSurroundingText(int leftLength, int rightLength) { + if (DEBUG) Log.v(TAG, "deleteSurroundingText " + leftLength + + " / " + rightLength); + final Editable content = getEditable(); + if (content == null) return false; + + beginBatchEdit(); + + int a = Selection.getSelectionStart(content); + int b = Selection.getSelectionEnd(content); + + if (a > b) { + int tmp = a; + a = b; + b = tmp; + } + + // ignore the composing text. + int ca = getComposingSpanStart(content); + int cb = getComposingSpanEnd(content); + if (cb < ca) { + int tmp = ca; + ca = cb; + cb = tmp; + } + if (ca != -1 && cb != -1) { + if (ca < a) a = ca; + if (cb > b) b = cb; + } + + int deleted = 0; + + if (leftLength > 0) { + int start = a - leftLength; + if (start < 0) start = 0; + content.delete(start, a); + deleted = a - start; + } + + if (rightLength > 0) { + b = b - deleted; + + int end = b + rightLength; + if (end > content.length()) end = content.length(); + + content.delete(b, end); + } + + endBatchEdit(); + + return true; + } + + /** + * The default implementation removes the composing state from the + * current editable text. In addition, only if dummy mode, a key event is + * sent for the new text and the current editable buffer cleared. + */ + public boolean finishComposingText() { + if (DEBUG) Log.v(TAG, "finishComposingText"); + final Editable content = getEditable(); + if (content != null) { + beginBatchEdit(); + removeComposingSpans(content); + endBatchEdit(); + sendCurrentText(); + } + return true; + } + + /** + * The default implementation uses TextUtils.getCapsMode to get the + * cursor caps mode for the current selection position in the editable + * text, unless in dummy mode in which case 0 is always returned. + */ + public int getCursorCapsMode(int reqModes) { + if (mDummyMode) return 0; + + final Editable content = getEditable(); + if (content == null) return 0; + + int a = Selection.getSelectionStart(content); + int b = Selection.getSelectionEnd(content); + + if (a > b) { + int tmp = a; + a = b; + b = tmp; + } + + return TextUtils.getCapsMode(content, a, reqModes); + } + + /** + * The default implementation always returns null. + */ + public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) { + return null; + } + + /** + * The default implementation returns the given amount of text from the + * current cursor position in the buffer. + */ + public CharSequence getTextBeforeCursor(int length, int flags) { + final Editable content = getEditable(); + if (content == null) return null; + + int a = Selection.getSelectionStart(content); + int b = Selection.getSelectionEnd(content); + + if (a > b) { + int tmp = a; + a = b; + b = tmp; + } + + if (length > a) { + length = a; + } + + if ((flags&GET_TEXT_WITH_STYLES) != 0) { + return content.subSequence(a - length, a); + } + return TextUtils.substring(content, a - length, a); + } + + /** + * The default implementation returns the given amount of text from the + * current cursor position in the buffer. + */ + public CharSequence getTextAfterCursor(int length, int flags) { + final Editable content = getEditable(); + if (content == null) return null; + + int a = Selection.getSelectionStart(content); + int b = Selection.getSelectionEnd(content); + + if (a > b) { + int tmp = a; + a = b; + b = tmp; + } + + if (b + length > content.length()) { + length = content.length() - b; + } + + + if ((flags&GET_TEXT_WITH_STYLES) != 0) { + return content.subSequence(b, b + length); + } + return TextUtils.substring(content, b, b + length); + } + + /** + * The default implementation does nothing. + */ + public boolean performContextMenuAction(int id) { + return false; + } + + /** + * The default implementation does nothing. + */ + public boolean performPrivateCommand(String action, Bundle data) { + return false; + } + + /** + * The default implementation places the given text into the editable, + * replacing any existing composing text. The new text is marked as + * in a composing state with the composing style. + */ + public boolean setComposingText(CharSequence text, int newCursorPosition) { + if (DEBUG) Log.v(TAG, "setComposingText " + text); + replaceText(text, newCursorPosition, true); + return true; + } + + /** + * The default implementation changes the selection position in the + * current editable text. + */ + public boolean setSelection(int start, int end) { + if (DEBUG) Log.v(TAG, "setSelection " + start + ", " + end); + final Editable content = getEditable(); + if (content == null) return false; + Selection.setSelection(content, start, end); + return true; + } + + /** * Provides standard implementation for sending a key event to the window * attached to the input connection's view. */ @@ -82,4 +412,144 @@ public abstract class BaseInputConnection implements InputConnection { mIMM.updateStatusIcon(resId, packageName); return true; } + + private void sendCurrentText() { + if (!mDummyMode) { + return; + } + + Editable content = getEditable(); + if (content != null) { + if (content.length() == 1) { + // If it's 1 character, we have a chance of being + // able to generate normal key events... + if (mKeyCharacterMap == null) { + mKeyCharacterMap = KeyCharacterMap.load( + KeyCharacterMap.BUILT_IN_KEYBOARD); + } + char[] chars = new char[1]; + content.getChars(0, 1, chars, 0); + KeyEvent[] events = mKeyCharacterMap.getEvents(chars); + if (events != null) { + for (int i=0; i<events.length; i++) { + if (DEBUG) Log.v(TAG, "Sending: " + events[i]); + sendKeyEvent(events[i]); + } + content.clear(); + return; + } + } + + // Otherwise, revert to the special key event containing + // the actual characters. + KeyEvent event = new KeyEvent(SystemClock.uptimeMillis(), + content.toString(), KeyCharacterMap.BUILT_IN_KEYBOARD, 0); + sendKeyEvent(event); + content.clear(); + } + } + + private void replaceText(CharSequence text, int newCursorPosition, + boolean composing) { + final Editable content = getEditable(); + if (content == null) { + return; + } + + beginBatchEdit(); + + // delete composing text set previously. + int a = getComposingSpanStart(content); + int b = getComposingSpanEnd(content); + + if (DEBUG) Log.v(TAG, "Composing span: " + a + " to " + b); + + if (b < a) { + int tmp = a; + a = b; + b = tmp; + } + + if (a != -1 && b != -1) { + removeComposingSpans(content); + } else { + a = Selection.getSelectionStart(content); + b = Selection.getSelectionEnd(content); + if (a >=0 && b>= 0 && a != b) { + if (b < a) { + int tmp = a; + a = b; + b = tmp; + } + } + } + + if (composing) { + Spannable sp = null; + if (!(text instanceof Spannable)) { + sp = new SpannableStringBuilder(text); + text = sp; + if (mDefaultComposingSpans == null) { + Context context; + if (mTargetView != null) { + context = mTargetView.getContext(); + } else if (mIMM.mServedView != null) { + context = mIMM.mServedView.getContext(); + } else { + context = null; + } + if (context != null) { + TypedArray ta = context.getTheme() + .obtainStyledAttributes(new int[] { + com.android.internal.R.attr.candidatesTextStyleSpans + }); + CharSequence style = ta.getText(0); + ta.recycle(); + if (style != null && style instanceof Spanned) { + mDefaultComposingSpans = ((Spanned)style).getSpans( + 0, style.length(), Object.class); + } + } + } + if (mDefaultComposingSpans != null) { + for (int i = 0; i < mDefaultComposingSpans.length; ++i) { + sp.setSpan(mDefaultComposingSpans[i], 0, sp.length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + } else { + sp = (Spannable)text; + } + setComposingSpans(sp); + } + + // Adjust newCursorPosition to be relative the start of the text. + newCursorPosition += a; + + if (DEBUG) Log.v(TAG, "Replacing from " + a + " to " + b + " with \"" + + text + "\", composing=" + composing + + ", type=" + text.getClass().getCanonicalName()); + + if (DEBUG) { + LogPrinter lp = new LogPrinter(Log.VERBOSE, TAG); + lp.println("Current text:"); + TextUtils.dumpSpans(content, lp, " "); + lp.println("Composing text:"); + TextUtils.dumpSpans(text, lp, " "); + } + + content.replace(a, b, text); + if (newCursorPosition < 0) newCursorPosition = 0; + if (newCursorPosition > content.length()) + newCursorPosition = content.length(); + Selection.setSelection(content, newCursorPosition); + + if (DEBUG) { + LogPrinter lp = new LogPrinter(Log.VERBOSE, TAG); + lp.println("Final text:"); + TextUtils.dumpSpans(content, lp, " "); + } + + endBatchEdit(); + } } diff --git a/core/java/android/view/inputmethod/DefaultInputMethod.java b/core/java/android/view/inputmethod/DefaultInputMethod.java deleted file mode 100644 index 073b01c..0000000 --- a/core/java/android/view/inputmethod/DefaultInputMethod.java +++ /dev/null @@ -1,239 +0,0 @@ -package android.view.inputmethod; - -import android.graphics.Rect; -import android.os.Bundle; -import android.os.IBinder; -import android.os.RemoteException; -import android.util.Log; -import android.view.KeyEvent; -import android.view.MotionEvent; - -import com.android.internal.view.IInputContext; -import com.android.internal.view.IInputMethod; -import com.android.internal.view.IInputMethodCallback; -import com.android.internal.view.IInputMethodSession; -import com.android.internal.view.InputConnectionWrapper; - -/** - * This is the default input method that runs in the same context of the - * application that requests text input. It does nothing but returns false for - * any key events, so that all key events will be processed by the key listener - * of the focused text box. - * {@hide} - */ -public class DefaultInputMethod implements InputMethod, InputMethodSession { - private static IInputMethod sInstance = new SimpleInputMethod( - new DefaultInputMethod()); - - private static InputMethodInfo sProperty = new InputMethodInfo( - "android.text.inputmethod", DefaultInputMethod.class.getName(), - "Default", "android.text.inputmethod.defaultImeSettings"); - - private InputConnection mInputConnection; - - public static IInputMethod getInstance() { - return sInstance; - } - - public static InputMethodInfo getMetaInfo() { - return sProperty; - } - - public void bindInput(InputBinding binding) { - mInputConnection = binding.getConnection(); - } - - public void unbindInput() { - } - - public void createSession(SessionCallback callback) { - callback.sessionCreated(this); - } - - public void setSessionEnabled(InputMethodSession session, boolean enabled) { - } - - public void revokeSession(InputMethodSession session) { - } - - public void finishInput() { - mInputConnection.hideStatusIcon(); - } - - public void displayCompletions(CompletionInfo[] completions) { - } - - public void updateExtractedText(int token, ExtractedText text) { - } - - public void updateSelection(int oldSelStart, int oldSelEnd, - int newSelStart, int newSelEnd, int candidatesStart, int candidatesEnd) { - } - - public void updateCursor(Rect newCursor) { - } - - public void dispatchKeyEvent(int seq, KeyEvent event, EventCallback callback) { - callback.finishedEvent(seq, false); - } - - public void dispatchTrackballEvent(int seq, MotionEvent event, EventCallback callback) { - callback.finishedEvent(seq, false); - } - - public void restartInput(EditorInfo attribute) { - } - - public void attachToken(IBinder token) { - } - - public void startInput(EditorInfo attribute) { - mInputConnection - .showStatusIcon("android", com.android.internal.R.drawable.ime_qwerty); - } - - public void appPrivateCommand(String action, Bundle data) { - } - - public void hideSoftInput() { - } - - public void showSoftInput(int flags) { - } -} - -// ---------------------------------------------------------------------- - -class SimpleInputMethod extends IInputMethod.Stub { - final InputMethod mInputMethod; - - static class Session extends IInputMethodSession.Stub { - final InputMethodSession mSession; - - Session(InputMethodSession session) { - mSession = session; - } - - public void finishInput() { - mSession.finishInput(); - } - - public void updateSelection(int oldSelStart, int oldSelEnd, - int newSelStart, int newSelEnd, int candidatesStart, int candidatesEnd) { - mSession.updateSelection(oldSelStart, oldSelEnd, - newSelStart, newSelEnd, candidatesStart, candidatesEnd); - } - - public void updateCursor(Rect newCursor) { - mSession.updateCursor(newCursor); - } - - static class InputMethodEventCallbackWrapper implements InputMethodSession.EventCallback { - final IInputMethodCallback mCb; - InputMethodEventCallbackWrapper(IInputMethodCallback cb) { - mCb = cb; - } - public void finishedEvent(int seq, boolean handled) { - try { - mCb.finishedEvent(seq, handled); - } catch (RemoteException e) { - } - } - } - - public void dispatchKeyEvent(int seq, KeyEvent event, IInputMethodCallback callback) { - mSession.dispatchKeyEvent(seq, event, - new InputMethodEventCallbackWrapper(callback)); - } - - public void dispatchTrackballEvent(int seq, MotionEvent event, IInputMethodCallback callback) { - mSession.dispatchTrackballEvent(seq, event, - new InputMethodEventCallbackWrapper(callback)); - } - - public void displayCompletions(CompletionInfo[] completions) { - mSession.displayCompletions(completions); - } - - public void updateExtractedText(int token, ExtractedText text) { - mSession.updateExtractedText(token, text); - } - - public void appPrivateCommand(String action, Bundle data) { - mSession.appPrivateCommand(action, data); - } - } - - public SimpleInputMethod(InputMethod inputMethod) { - mInputMethod = inputMethod; - } - - public InputMethod getInternalInputMethod() { - return mInputMethod; - } - - public void attachToken(IBinder token) { - mInputMethod.attachToken(token); - } - - public void bindInput(InputBinding binding) { - InputConnectionWrapper ic = new InputConnectionWrapper( - IInputContext.Stub.asInterface(binding.getConnectionToken())); - InputBinding nu = new InputBinding(ic, binding); - mInputMethod.bindInput(nu); - } - - public void unbindInput() { - mInputMethod.unbindInput(); - } - - public void restartInput(EditorInfo attribute) { - mInputMethod.restartInput(attribute); - } - - public void startInput(EditorInfo attribute) { - mInputMethod.startInput(attribute); - } - - static class InputMethodSessionCallbackWrapper implements InputMethod.SessionCallback { - final IInputMethodCallback mCb; - InputMethodSessionCallbackWrapper(IInputMethodCallback cb) { - mCb = cb; - } - - public void sessionCreated(InputMethodSession session) { - try { - mCb.sessionCreated(new Session(session)); - } catch (RemoteException e) { - } - } - } - - public void createSession(IInputMethodCallback callback) throws RemoteException { - mInputMethod.createSession(new InputMethodSessionCallbackWrapper(callback)); - } - - public void setSessionEnabled(IInputMethodSession session, boolean enabled) throws RemoteException { - try { - InputMethodSession ls = ((Session)session).mSession; - mInputMethod.setSessionEnabled(ls, enabled); - } catch (ClassCastException e) { - Log.w("SimpleInputMethod", "Incoming session not of correct type: " + session, e); - } - } - - public void revokeSession(IInputMethodSession session) throws RemoteException { - try { - InputMethodSession ls = ((Session)session).mSession; - mInputMethod.revokeSession(ls); - } catch (ClassCastException e) { - Log.w("SimpleInputMethod", "Incoming session not of correct type: " + session, e); - } - } - - public void showSoftInput(boolean blah) { - } - - public void hideSoftInput() { - } -} diff --git a/core/java/android/view/inputmethod/EditorInfo.java b/core/java/android/view/inputmethod/EditorInfo.java index c050691..b2f26d7 100644 --- a/core/java/android/view/inputmethod/EditorInfo.java +++ b/core/java/android/view/inputmethod/EditorInfo.java @@ -5,6 +5,7 @@ import android.os.Parcel; import android.os.Parcelable; import android.text.InputType; import android.text.TextUtils; +import android.util.Printer; /** * An EditorInfo describes several attributes of a text editing object @@ -21,7 +22,7 @@ public class EditorInfo implements InputType, Parcelable { * @see #TYPE_MASK_VARIATION * @see #TYPE_MASK_FLAGS */ - public int inputType = TYPE_CLASS_TEXT; + public int inputType = TYPE_NULL; /** * A string supplying additional information about the content type that @@ -71,6 +72,26 @@ public class EditorInfo implements InputType, Parcelable { public CharSequence label; /** + * Name of the package that owns this editor. + */ + public String packageName; + + /** + * Identifier for the editor's field. This is optional, and may be + * 0. By default it is filled in with the result of + * {@link android.view.View#getId() View.getId()} on the View that + * is being edited. + */ + public int fieldId; + + /** + * Additional name for the editor's field. This can supply additional + * name information for the field. By default it is null. The actual + * contents have no meaning. + */ + public String fieldName; + + /** * Any extra data to supply to the input method. This is for extended * communication with specific input methods; the name fields in the * bundle should be scoped (such as "com.mydomain.im.SOME_FIELD") so @@ -81,6 +102,24 @@ public class EditorInfo implements InputType, Parcelable { public Bundle extras; /** + * Write debug output of this object. + */ + public void dump(Printer pw, String prefix) { + pw.println(prefix + "inputType=0x" + Integer.toHexString(inputType) + + " privateContentType=" + privateContentType); + pw.println(prefix + "initialSelStart=" + initialSelStart + + " initialSelEnd=" + initialSelEnd + + " initialCapsMode=0x" + + Integer.toHexString(initialCapsMode)); + pw.println(prefix + "hintText=" + hintText + + " label=" + label); + pw.println(prefix + "packageName=" + packageName + + " fieldId=" + fieldId + + " fieldName=" + fieldName); + pw.println(prefix + "extras=" + extras); + } + + /** * Used to package this object into a {@link Parcel}. * * @param dest The {@link Parcel} to be written. @@ -94,6 +133,9 @@ public class EditorInfo implements InputType, Parcelable { dest.writeInt(initialCapsMode); TextUtils.writeToParcel(hintText, dest, flags); TextUtils.writeToParcel(label, dest, flags); + dest.writeString(packageName); + dest.writeInt(fieldId); + dest.writeString(fieldName); dest.writeBundle(extras); } @@ -110,6 +152,9 @@ public class EditorInfo implements InputType, Parcelable { res.initialCapsMode = source.readInt(); res.hintText = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); res.label = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); + res.packageName = source.readString(); + res.fieldId = source.readInt(); + res.fieldName = source.readString(); res.extras = source.readBundle(); return res; } diff --git a/core/java/android/view/inputmethod/ExtractedText.java b/core/java/android/view/inputmethod/ExtractedText.java index 0ca3c79..e5d3cae 100644 --- a/core/java/android/view/inputmethod/ExtractedText.java +++ b/core/java/android/view/inputmethod/ExtractedText.java @@ -19,6 +19,22 @@ public class ExtractedText implements Parcelable { public int startOffset; /** + * If the content is a report of a partial text change, this is the + * offset where the change starts and it runs until + * {@link #partialEndOffset}. If the content is the full text, this + * field is -1. + */ + public int partialStartOffset; + + /** + * If the content is a report of a partial text change, this is the offset + * where the change ends. Note that the actual text may be larger or + * smaller than the difference between this and {@link #partialEndOffset}, + * meaning a reduction or increase, respectively, in the total text. + */ + public int partialEndOffset; + + /** * The offset where the selection currently starts within the extracted * text. The real selection start position is at * <var>startOffset</var>+<var>selectionStart</var>. @@ -52,6 +68,8 @@ public class ExtractedText implements Parcelable { public void writeToParcel(Parcel dest, int flags) { TextUtils.writeToParcel(text, dest, flags); dest.writeInt(startOffset); + dest.writeInt(partialStartOffset); + dest.writeInt(partialEndOffset); dest.writeInt(selectionStart); dest.writeInt(selectionEnd); dest.writeInt(flags); @@ -65,6 +83,8 @@ public class ExtractedText implements Parcelable { ExtractedText res = new ExtractedText(); res.text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); res.startOffset = source.readInt(); + res.partialStartOffset = source.readInt(); + res.partialEndOffset = source.readInt(); res.selectionStart = source.readInt(); res.selectionEnd = source.readInt(); res.flags = source.readInt(); diff --git a/core/java/android/view/inputmethod/ExtractedTextRequest.java b/core/java/android/view/inputmethod/ExtractedTextRequest.java index d962329..e84b094 100644 --- a/core/java/android/view/inputmethod/ExtractedTextRequest.java +++ b/core/java/android/view/inputmethod/ExtractedTextRequest.java @@ -16,6 +16,13 @@ public class ExtractedTextRequest implements Parcelable { public int token; /** + * Additional request flags, having the same possible values as the + * flags parameter of {@link InputConnection#getTextBeforeCursor + * InputConnection.getTextBeforeCursor()}. + */ + public int flags; + + /** * Hint for the maximum number of lines to return. */ public int hintMaxLines; @@ -33,6 +40,7 @@ public class ExtractedTextRequest implements Parcelable { */ public void writeToParcel(Parcel dest, int flags) { dest.writeInt(token); + dest.writeInt(this.flags); dest.writeInt(hintMaxLines); dest.writeInt(hintMaxChars); } @@ -45,6 +53,7 @@ public class ExtractedTextRequest implements Parcelable { public ExtractedTextRequest createFromParcel(Parcel source) { ExtractedTextRequest res = new ExtractedTextRequest(); res.token = source.readInt(); + res.flags = source.readInt(); res.hintMaxLines = source.readInt(); res.hintMaxChars = source.readInt(); return res; diff --git a/core/java/android/view/inputmethod/InputConnection.java b/core/java/android/view/inputmethod/InputConnection.java index bd7b050..8c30d3f 100644 --- a/core/java/android/view/inputmethod/InputConnection.java +++ b/core/java/android/view/inputmethod/InputConnection.java @@ -32,6 +32,21 @@ import android.view.KeyEvent; */ public interface InputConnection { /** + * Flag for use with {@link #getTextAfterCursor} and + * {@link #getTextBeforeCursor} to have style information returned along + * with the text. If not set, you will receive only the raw text. If + * set, you may receive a complex CharSequence of both text and style + * spans. + */ + static final int GET_TEXT_WITH_STYLES = 0x0001; + + /** + * Flag for use with {@link #getExtractedText} to indicate you would + * like to receive updates when the extracted text changes. + */ + public static final int GET_EXTRACTED_TEXT_MONITOR = 0x0001; + + /** * Get <var>n</var> characters of text before the current cursor position. * * <p>This method may fail either if the input connection has become invalid @@ -40,11 +55,13 @@ public interface InputConnection { * In either case, a null is returned. * * @param n The expected length of the text. + * @param flags Supplies additional options controlling how the text is + * returned. May be either 0 or {@link #GET_TEXT_WITH_STYLES}. * * @return Returns the text before the cursor position; the length of the * returned text might be less than <var>n</var>. */ - public CharSequence getTextBeforeCursor(int n); + public CharSequence getTextBeforeCursor(int n, int flags); /** * Get <var>n</var> characters of text after the current cursor position. @@ -55,11 +72,13 @@ public interface InputConnection { * In either case, a null is returned. * * @param n The expected length of the text. + * @param flags Supplies additional options controlling how the text is + * returned. May be either 0 or {@link #GET_TEXT_WITH_STYLES}. * * @return Returns the text after the cursor position; the length of the * returned text might be less than <var>n</var>. */ - public CharSequence getTextAfterCursor(int n); + public CharSequence getTextAfterCursor(int n, int flags); /** * Retrieve the current capitalization mode in effect at the current @@ -82,8 +101,6 @@ public interface InputConnection { */ public int getCursorCapsMode(int reqModes); - public static final int EXTRACTED_TEXT_MONITOR = 0x0001; - /** * Retrieve the current text in the input connection's editor, and monitor * for any changes to it. This function returns with the current text, @@ -97,7 +114,7 @@ public interface InputConnection { * * @param request Description of how the text should be returned. * @param flags Additional options to control the client, either 0 or - * {@link #EXTRACTED_TEXT_MONITOR}. + * {@link #GET_EXTRACTED_TEXT_MONITOR}. * * @return Returns an ExtractedText object describing the state of the * text view and containing the extracted text itself. @@ -141,7 +158,7 @@ public interface InputConnection { /** * Have the text editor finish whatever composing text is currently - * active. This simple leaves the text as-is, removing any special + * active. This simply leaves the text as-is, removing any special * composing styling or other state that was around it. The cursor * position remains unchanged. */ @@ -177,6 +194,22 @@ public interface InputConnection { public boolean commitCompletion(CompletionInfo text); /** + * Set the selection of the text editor. To set the cursor position, + * start and end should have the same value. + */ + public boolean setSelection(int start, int end); + + /** + * Perform a context menu action on the field. The given id may be one of: + * {@link android.R.id#selectAll}, + * {@link android.R.id#startSelectingText}, {@link android.R.id#stopSelectingText}, + * {@link android.R.id#cut}, {@link android.R.id#copy}, + * {@link android.R.id#paste}, {@link android.R.id#copyUrl}, + * or {@link android.R.id#switchInputMethod} + */ + public boolean performContextMenuAction(int id); + + /** * Tell the editor that you are starting a batch of editor operations. * The editor will try to avoid sending you updates about its state * until {@link #endBatchEdit} is called. diff --git a/core/java/android/view/inputmethod/InputConnectionWrapper.java b/core/java/android/view/inputmethod/InputConnectionWrapper.java deleted file mode 100644 index f65b2a1..0000000 --- a/core/java/android/view/inputmethod/InputConnectionWrapper.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright (C) 2007-2008 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.inputmethod; - -import android.os.Bundle; -import android.view.KeyEvent; - -/** - * Wrapper around InputConnection interface, calling through to another - * implementation of it. - */ -public class InputConnectionWrapper implements InputConnection { - InputConnection mBase; - - /** - * Create a new wrapper around an existing InputConnection implementation. - */ - public InputConnectionWrapper(InputConnection base) { - mBase = base; - } - - /** - * Return the base InputConnection that this class is wrapping. - */ - InputConnection getBase() { - return mBase; - } - - public CharSequence getTextBeforeCursor(int n) { - return mBase.getTextBeforeCursor(n); - } - - public CharSequence getTextAfterCursor(int n) { - return mBase.getTextAfterCursor(n); - } - - public int getCursorCapsMode(int reqModes) { - return mBase.getCursorCapsMode(reqModes); - } - - public ExtractedText getExtractedText(ExtractedTextRequest request, - int flags) { - return mBase.getExtractedText(request, flags); - } - - public boolean deleteSurroundingText(int leftLength, int rightLength) { - return mBase.deleteSurroundingText(leftLength, rightLength); - } - - public boolean setComposingText(CharSequence text, int newCursorPosition) { - return mBase.setComposingText(text, newCursorPosition); - } - - public boolean finishComposingText() { - return mBase.finishComposingText(); - } - - public boolean commitText(CharSequence text, int newCursorPosition) { - return mBase.commitText(text, newCursorPosition); - } - - public boolean commitCompletion(CompletionInfo text) { - return mBase.commitCompletion(text); - } - - public boolean beginBatchEdit() { - return mBase.beginBatchEdit(); - } - - public boolean endBatchEdit() { - return mBase.endBatchEdit(); - } - - public boolean sendKeyEvent(KeyEvent event) { - return mBase.sendKeyEvent(event); - } - - public boolean clearMetaKeyStates(int states) { - return mBase.clearMetaKeyStates(states); - } - - public boolean performPrivateCommand(String action, Bundle data) { - return mBase.performPrivateCommand(action, data); - } - - public boolean showStatusIcon(String packageName, int resId) { - return mBase.showStatusIcon(packageName, resId); - } - - public boolean hideStatusIcon() { - return mBase.hideStatusIcon(); - } -} diff --git a/core/java/android/view/inputmethod/InputMethod.java b/core/java/android/view/inputmethod/InputMethod.java index c0e6590..740dca8 100644 --- a/core/java/android/view/inputmethod/InputMethod.java +++ b/core/java/android/view/inputmethod/InputMethod.java @@ -113,12 +113,15 @@ public interface InputMethod { * is ready for this input method to process received events and send result * text back to the application. * - * @param attribute The attribute of the text box (typically, a EditText) + * @param inputConnection Optional specific input connection for + * communicating with the text box; if null, you should use the generic + * bound input connection. + * @param info Information about the text box (typically, an EditText) * that requests input. * * @see EditorInfo */ - public void startInput(EditorInfo attribute); + public void startInput(InputConnection inputConnection, EditorInfo info); /** * This method is called when the state of this input method needs to be @@ -128,12 +131,15 @@ public interface InputMethod { * Typically, this method is called when the input focus is moved from one * text box to another. * + * @param inputConnection Optional specific input connection for + * communicating with the text box; if null, you should use the generic + * bound input connection. * @param attribute The attribute of the text box (typically, a EditText) * that requests input. * * @see EditorInfo */ - public void restartInput(EditorInfo attribute); + public void restartInput(InputConnection inputConnection, EditorInfo attribute); /** * Create a new {@link InputMethodSession} that can be handed to client @@ -173,6 +179,13 @@ public interface InputMethod { public static final int SHOW_EXPLICIT = 0x00001; /** + * Flag for {@link #showSoftInput(int)}: this show has been forced to + * happen by the user. If set, the input method should remain visible + * until deliberated dismissed by the user in its UI. + */ + public static final int SHOW_FORCED = 0x00002; + + /** * Request that any soft input part of the input method be shown to the user. * * @param flags Provide additional information about the show request. diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index a9a9594..99d5aa5 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -26,11 +26,14 @@ import android.os.Message; import android.os.RemoteException; import android.os.ServiceManager; import android.util.Log; +import android.util.PrintWriterPrinter; +import android.util.Printer; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.ViewRoot; +import com.android.internal.os.HandlerCaller; import com.android.internal.view.IInputConnectionWrapper; import com.android.internal.view.IInputContext; import com.android.internal.view.IInputMethodCallback; @@ -39,7 +42,11 @@ import com.android.internal.view.IInputMethodManager; import com.android.internal.view.IInputMethodSession; import com.android.internal.view.InputBindResult; +import java.io.FileDescriptor; +import java.io.PrintWriter; import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; /** * Central system API to the overall input method framework (IMF) architecture, @@ -199,8 +206,7 @@ public final class InputMethodManager { // global lock. final H mH; - // The currently active input connection. - final MutableInputConnectionWrapper mInputConnectionWrapper; + // Our generic input connection if the current target does not have its own. final IInputContext mIInputContext; /** @@ -208,11 +214,6 @@ public final class InputMethodManager { */ boolean mActive = false; - /** - * The current base input connection, used when mActive is true. - */ - InputConnection mCurrentInputConnection; - // ----------------------------------------------------------- /** @@ -270,7 +271,7 @@ public final class InputMethodManager { // ----------------------------------------------------------- - static final int MSG_CHECK_FOCUS = 1; + static final int MSG_DUMP = 1; class H extends Handler { H(Looper looper) { @@ -280,85 +281,55 @@ public final class InputMethodManager { @Override public void handleMessage(Message msg) { switch (msg.what) { - case MSG_CHECK_FOCUS: - checkFocus(); + case MSG_DUMP: { + HandlerCaller.SomeArgs args = (HandlerCaller.SomeArgs)msg.obj; + try { + doDump((FileDescriptor)args.arg1, + (PrintWriter)args.arg2, (String[])args.arg3); + } catch (RuntimeException e) { + ((PrintWriter)args.arg2).println("Exception: " + e); + } + synchronized (args.arg4) { + ((CountDownLatch)args.arg4).countDown(); + } return; + } } } } - static class NoOpInputConnection implements InputConnection { - - public boolean clearMetaKeyStates(int states) { - return false; - } - - public boolean beginBatchEdit() { - return false; - } - - public boolean endBatchEdit() { - return false; - } - - public boolean commitCompletion(CompletionInfo text) { - return false; - } - - public boolean commitText(CharSequence text, int newCursorPosition) { - return false; - } - - public boolean deleteSurroundingText(int leftLength, int rightLength) { - return false; - } - - public int getCursorCapsMode(int reqModes) { - return 0; - } - - public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) { - return null; - } - - public CharSequence getTextAfterCursor(int n) { - return null; + class ControlledInputConnectionWrapper extends IInputConnectionWrapper { + public ControlledInputConnectionWrapper(Looper mainLooper, InputConnection conn) { + super(mainLooper, conn); } - public CharSequence getTextBeforeCursor(int n) { - return null; - } - - public boolean hideStatusIcon() { - return false; - } - - public boolean performPrivateCommand(String action, Bundle data) { - return false; - } - - public boolean sendKeyEvent(KeyEvent event) { - return false; - } - - public boolean setComposingText(CharSequence text, int newCursorPosition) { - return false; - } - - public boolean finishComposingText() { - return false; - } - - public boolean showStatusIcon(String packageName, int resId) { - return false; + public boolean isActive() { + return mActive; } } - final NoOpInputConnection mNoOpInputConnection = new NoOpInputConnection(); - final IInputMethodClient.Stub mClient = new IInputMethodClient.Stub() { - public void setUsingInputMethod(boolean state) { + @Override protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) { + // No need to check for dump permission, since we only give this + // interface to the system. + CountDownLatch latch = new CountDownLatch(1); + HandlerCaller.SomeArgs sargs = new HandlerCaller.SomeArgs(); + sargs.arg1 = fd; + sargs.arg2 = fout; + sargs.arg3 = args; + sargs.arg4 = latch; + mH.sendMessage(mH.obtainMessage(MSG_DUMP, sargs)); + try { + if (!latch.await(5, TimeUnit.SECONDS)) { + fout.println("Timeout waiting for dump"); + } + } catch (InterruptedException e) { + fout.println("Interrupted waiting for dump"); + } + } + + public void setUsingInputMethod(boolean state) { } public void onBindMethod(InputBindResult res) { @@ -403,62 +374,17 @@ public final class InputMethodManager { public void setActive(boolean active) { mActive = active; - mInputConnectionWrapper.setBaseInputConnection(active - ? mCurrentInputConnection : mNoOpInputConnection); } }; - final InputConnection mDummyInputConnection = new BaseInputConnection(this) { - public boolean beginBatchEdit() { - return false; - } - public boolean endBatchEdit() { - return false; - } - public boolean commitText(CharSequence text, int newCursorPosition) { - return false; - } - public boolean commitCompletion(CompletionInfo text) { - return false; - } - public boolean deleteSurroundingText(int leftLength, int rightLength) { - return false; - } - public ExtractedText getExtractedText(ExtractedTextRequest request, - int flags) { - return null; - } - public CharSequence getTextAfterCursor(int n) { - return null; - } - public CharSequence getTextBeforeCursor(int n) { - return null; - } - public int getCursorCapsMode(int reqModes) { - return 0; - } - public boolean clearMetaKeyStates(int states) { - return false; - } - public boolean performPrivateCommand(String action, Bundle data) { - return false; - } - public boolean setComposingText(CharSequence text, int newCursorPosition) { - return false; - } - public boolean finishComposingText() { - return false; - } - }; + final InputConnection mDummyInputConnection = new BaseInputConnection(this, true); InputMethodManager(IInputMethodManager service, Looper looper) { mService = service; mMainLooper = looper; mH = new H(looper); - mInputConnectionWrapper = new MutableInputConnectionWrapper(mNoOpInputConnection); - mIInputContext = new IInputConnectionWrapper(looper, - mInputConnectionWrapper); - setCurrentInputConnection(mDummyInputConnection); + mIInputContext = new ControlledInputConnectionWrapper(looper, + mDummyInputConnection); if (mInstance == null) { mInstance = this; @@ -563,22 +489,12 @@ public final class InputMethodManager { } /** - * Record the desired input connection, but only set it if mActive is true. - */ - void setCurrentInputConnection(InputConnection connection) { - mCurrentInputConnection = connection; - mInputConnectionWrapper.setBaseInputConnection(mActive - ? connection : mNoOpInputConnection); - } - - /** * Reset all of the state associated with a served view being connected * to an input method */ void clearConnectionLocked() { mCurrentTextBoxAttribute = null; mServedInputConnection = null; - setCurrentInputConnection(mDummyInputConnection); } /** @@ -659,13 +575,20 @@ public final class InputMethodManager { } /** - * Flag for {@link #showSoftInput} to indicate that the this is an implicit + * Flag for {@link #showSoftInput} to indicate that this is an implicit * request to show the input window, not as the result of a direct request * by the user. The window may not be shown in this case. */ public static final int SHOW_IMPLICIT = 0x0001; /** + * Flag for {@link #showSoftInput} to indicate that the user has forced + * the input method open (such as by long-pressing menu) so it should + * not be closed until they explicitly do so. + */ + public static final int SHOW_FORCED = 0x0002; + + /** * Explicitly request that the current input method's soft input area be * shown to the user, if needed. Call this if the user interacts with * your view in such a way that they have expressed they would like to @@ -689,6 +612,14 @@ public final class InputMethodManager { } } + /** @hide */ + public void showSoftInputUnchecked(int flags) { + try { + mService.showSoftInput(mClient, flags); + } catch (RemoteException e) { + } + } + /** * Flag for {@link #hideSoftInputFromWindow} to indicate that the soft * input window should only be hidden if it was not explicitly shown @@ -697,6 +628,13 @@ public final class InputMethodManager { public static final int HIDE_IMPLICIT_ONLY = 0x0001; /** + * Flag for {@link #hideSoftInputFromWindow} to indicate that the soft + * input window should normally be hidden, unless it was originally + * shown with {@link #SHOW_FORCED}. + */ + public static final int HIDE_NOT_ALWAYS = 0x0002; + + /** * Request to hide the soft input window from the context of the window * that is currently accepting input. This should be called as a result * of the user doing some actually than fairly explicitly requests to @@ -779,6 +717,8 @@ public final class InputMethodManager { // do its stuff. // Life is good: let's hook everything up! EditorInfo tba = new EditorInfo(); + tba.packageName = view.getContext().getPackageName(); + tba.fieldId = view.getId(); InputConnection ic = view.onCreateInputConnection(tba); if (DEBUG) Log.v(TAG, "Starting input: tba=" + tba + " ic=" + ic); @@ -801,22 +741,23 @@ public final class InputMethodManager { mCurrentTextBoxAttribute = tba; mServedConnecting = false; mServedInputConnection = ic; + IInputContext servedContext; if (ic != null) { mCursorSelStart = tba.initialSelStart; mCursorSelEnd = tba.initialSelEnd; mCursorCandStart = -1; mCursorCandEnd = -1; mCursorRect.setEmpty(); - setCurrentInputConnection(ic); + servedContext = new ControlledInputConnectionWrapper(vh.getLooper(), ic); } else { - setCurrentInputConnection(mDummyInputConnection); + servedContext = null; } try { if (DEBUG) Log.v(TAG, "START INPUT: " + view + " ic=" + ic + " tba=" + tba + " initial=" + initial); - InputBindResult res = mService.startInput(mClient, tba, initial, - mCurMethod == null); + InputBindResult res = mService.startInput(mClient, + servedContext, tba, initial, mCurMethod == null); if (DEBUG) Log.v(TAG, "Starting input: Bind result=" + res); if (res != null) { if (res.id != null) { @@ -885,10 +826,20 @@ public final class InputMethodManager { + " winFocus=" + view.hasWindowFocus()); if (mServedView == view) { ic = mServedInputConnection; - if (view.hasWindowFocus()) { + // The following code would auto-hide the IME if we end up + // with no more views with focus. This can happen, however, + // whenever we go into touch mode, so it ends up hiding + // at times when we don't really want it to. For now it + // seems better to just turn it all off. + if (false && view.hasWindowFocus()) { mLastServedView = view; - mH.removeMessages(MSG_CHECK_FOCUS); - mH.sendEmptyMessage(MSG_CHECK_FOCUS); + Handler vh = view.getHandler(); + if (vh != null) { + // This will result in a call to checkFocus() below. + vh.removeMessages(ViewRoot.CHECK_FOCUS); + vh.sendMessage(vh.obtainMessage(ViewRoot.CHECK_FOCUS, + view)); + } } } } @@ -898,8 +849,14 @@ public final class InputMethodManager { } } - void checkFocus() { + /** + * @hide + */ + public void checkFocus(View view) { synchronized (mH) { + if (view != mLastServedView) { + return; + } if (DEBUG) Log.v(TAG, "checkFocus: view=" + mServedView + " last=" + mLastServedView); if (mServedView == mLastServedView) { @@ -915,7 +872,7 @@ public final class InputMethodManager { void closeCurrentInput() { try { - mService.hideSoftInput(mClient, 0); + mService.hideSoftInput(mClient, HIDE_NOT_ALWAYS); } catch (RemoteException e) { } } @@ -1006,6 +963,32 @@ public final class InputMethodManager { } /** + * Call {@link InputMethodSession#appPrivateCommand(String, Bundle) + * InputMethodSession.appPrivateCommand()} on the current Input Method. + * @param view Optional View that is sending the command, or null if + * you want to send the command regardless of the view that is attached + * to the input method. + * @param action Name of the command to be performed. This <em>must</em> + * be a scoped name, i.e. prefixed with a package name you own, so that + * different developers will not create conflicting commands. + * @param data Any data to include with the command. + */ + public void sendAppPrivateCommand(View view, String action, Bundle data) { + synchronized (mH) { + if ((view != null && mServedView != view) + || mCurrentTextBoxAttribute == null || mCurMethod == null) { + return; + } + try { + if (DEBUG) Log.v(TAG, "APP PRIVATE COMMAND " + action + ": " + data); + mCurMethod.appPrivateCommand(action, data); + } catch (RemoteException e) { + Log.w(TAG, "IME died: " + mCurId, e); + } + } + } + + /** * Force switch to a new input method component. This can only be called * from the currently active input method, as validated by the given token. * @param token Supplies the identifying token given to an input method @@ -1048,7 +1031,7 @@ public final class InputMethodManager { synchronized (mH) { if (DEBUG) Log.d(TAG, "dispatchKeyEvent"); - if (mCurMethod == null || mCurrentTextBoxAttribute == null) { + if (mCurMethod == null) { try { callback.finishedEvent(seq, false); } catch (RemoteException e) { @@ -1116,4 +1099,33 @@ public final class InputMethodManager { } } } + + void doDump(FileDescriptor fd, PrintWriter fout, String[] args) { + final Printer p = new PrintWriterPrinter(fout); + p.println("Input method client state for " + this + ":"); + + p.println(" mService=" + mService); + p.println(" mMainLooper=" + mMainLooper); + p.println(" mIInputContext=" + mIInputContext); + p.println(" mActive=" + mActive + + " mBindSequence=" + mBindSequence + + " mCurId=" + mCurId); + p.println(" mCurMethod=" + mCurMethod); + p.println(" mServedView=" + mServedView); + p.println(" mLastServedView=" + mLastServedView); + p.println(" mServedConnecting=" + mServedConnecting); + if (mCurrentTextBoxAttribute != null) { + p.println(" mCurrentTextBoxAttribute:"); + mCurrentTextBoxAttribute.dump(p, " "); + } else { + p.println(" mCurrentTextBoxAttribute: null"); + } + p.println(" mServedInputConnection=" + mServedInputConnection); + p.println(" mCompletions=" + mCompletions); + p.println(" mCursorRect=" + mCursorRect); + p.println(" mCursorSelStart=" + mCursorSelStart + + " mCursorSelEnd=" + mCursorSelEnd + + " mCursorCandStart=" + mCursorCandStart + + " mCursorCandEnd=" + mCursorCandEnd); + } } diff --git a/core/java/android/view/inputmethod/MutableInputConnectionWrapper.java b/core/java/android/view/inputmethod/MutableInputConnectionWrapper.java deleted file mode 100644 index 025a059..0000000 --- a/core/java/android/view/inputmethod/MutableInputConnectionWrapper.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (C) 2007-2008 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.inputmethod; - - -/** - * Special version of {@link InputConnectionWrapper} that allows the base - * input connection to be modified after it is initially set. - */ -public class MutableInputConnectionWrapper extends InputConnectionWrapper { - public MutableInputConnectionWrapper(InputConnection base) { - super(base); - } - - /** - * Change the base InputConnection for this wrapper. All calls will then be - * delegated to the base input connection. - * - * @param base The new base InputConnection for this wrapper. - */ - public void setBaseInputConnection(InputConnection base) { - mBase = base; - } -} |