diff --git a/api/current.xml b/api/current.xml index ee82e008a0911..38490f3113eb5 100644 --- a/api/current.xml +++ b/api/current.xml @@ -16089,6 +16089,17 @@ visibility="public" > + + + + + + + + + + + + + + + + + + + + + + + + @@ -144880,6 +144958,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + If the focused view didn't want this event, this method is called. * - *

The default implementation handles KEYCODE_BACK to stop the activity - * and go back, and other default key handling if configured with {@link #setDefaultKeyMode}. + *

The default implementation sets up state to call + * {@link #onKeyLongPress}, and does other default key handling + * if configured with {@link #setDefaultKeyMode}. * * @return Return true to prevent this event from being propagated * further, or false to indicate that you have not handled @@ -1762,16 +1763,19 @@ public class Activity extends ContextThemeWrapper * @see android.view.KeyEvent */ public boolean onKeyDown(int keyCode, KeyEvent event) { - if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) { - finish(); + if (keyCode == KeyEvent.KEYCODE_BACK) { + event.startTracking(); return true; } if (mDefaultKeyMode == DEFAULT_KEYS_DISABLE) { return false; } else if (mDefaultKeyMode == DEFAULT_KEYS_SHORTCUT) { - return getWindow().performPanelShortcut(Window.FEATURE_OPTIONS_PANEL, - keyCode, event, Menu.FLAG_ALWAYS_PERFORM_CLOSE); + if (getWindow().performPanelShortcut(Window.FEATURE_OPTIONS_PANEL, + keyCode, event, Menu.FLAG_ALWAYS_PERFORM_CLOSE)) { + return true; + } + return false; } else { // Common code for DEFAULT_KEYS_DIALER & DEFAULT_KEYS_SEARCH_* boolean clearSpannable = false; @@ -1780,8 +1784,8 @@ public class Activity extends ContextThemeWrapper clearSpannable = true; handled = false; } else { - handled = TextKeyListener.getInstance().onKeyDown(null, mDefaultKeySsb, - keyCode, event); + handled = TextKeyListener.getInstance().onKeyDown( + null, mDefaultKeySsb, keyCode, event); if (handled && mDefaultKeySsb.length() > 0) { // something useable has been typed - dispatch it now. @@ -1812,12 +1816,24 @@ public class Activity extends ContextThemeWrapper } } + /** + * Default implementation of {@link KeyEvent.Callback#onKeyLongPress(int, KeyEvent) + * KeyEvent.Callback.onKeyLongPress()}: always returns false (doesn't handle + * the event). + */ + public boolean onKeyLongPress(int keyCode, KeyEvent event) { + return false; + } + /** * Called when a key was released and not handled by any of the views * inside of the activity. So, for example, key presses while the cursor * is inside a TextView will not trigger the event (unless it is a navigation * to another object) because TextView handles its own key presses. * + *

The default implementation handles KEYCODE_BACK to stop the activity + * and go back. + * * @return Return true to prevent this event from being propagated * further, or false to indicate that you have not handled * this event and it should continue to be propagated. @@ -1825,6 +1841,11 @@ public class Activity extends ContextThemeWrapper * @see KeyEvent */ public boolean onKeyUp(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK && event.isTracking() + && !event.isCanceled()) { + onBackPressed(); + return true; + } return false; } @@ -1837,6 +1858,15 @@ public class Activity extends ContextThemeWrapper return false; } + /** + * Called when the activity has detected the user's press of the back + * key. The default implementation simply finishes the current activity, + * but you can override this to do whatever you want. + */ + public void onBackPressed() { + finish(); + } + /** * Called when a touch screen event was not handled by any of the views * under it. This is most useful to process touch events that happen @@ -1909,9 +1939,10 @@ public class Activity extends ContextThemeWrapper /** * Called when the current {@link Window} of the activity gains or loses * focus. This is the best indicator of whether this activity is visible - * to the user. + * to the user. The default implementation clears the key tracking + * state, so should always be called. * - *

Note that this provides information what global focus state, which + *

Note that this provides information about global focus state, which * is managed independently of activity lifecycles. As such, while focus * changes will generally have some relation to lifecycle changes (an * activity that is stopped will not generally get window focus), you @@ -1988,7 +2019,8 @@ public class Activity extends ContextThemeWrapper if (getWindow().superDispatchKeyEvent(event)) { return true; } - return event.dispatch(this); + return event.dispatch(this, mDecor != null + ? mDecor.getKeyDispatcherState() : null, this); } /** diff --git a/core/java/android/app/Dialog.java b/core/java/android/app/Dialog.java index 1b96af9f20418..58e8b32125475 100644 --- a/core/java/android/app/Dialog.java +++ b/core/java/android/app/Dialog.java @@ -481,30 +481,45 @@ public class Dialog implements DialogInterface, Window.Callback, * *

If the focused view didn't want this event, this method is called. * - *

The default implementation handles KEYCODE_BACK to close the - * dialog. + *

The default implementation consumed the KEYCODE_BACK to later + * handle it in {@link #onKeyUp}. * * @see #onKeyUp * @see android.view.KeyEvent */ public boolean onKeyDown(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK) { - if (mCancelable) { - cancel(); - } + event.startTracking(); return true; } return false; } + /** + * Default implementation of {@link KeyEvent.Callback#onKeyLongPress(int, KeyEvent) + * KeyEvent.Callback.onKeyLongPress()}: always returns false (doesn't handle + * the event). + */ + public boolean onKeyLongPress(int keyCode, KeyEvent event) { + return false; + } + /** * A key was released. * + *

The default implementation handles KEYCODE_BACK to close the + * dialog. + * * @see #onKeyDown * @see KeyEvent */ public boolean onKeyUp(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK && event.isTracking() + && !event.isCanceled()) { + onBackPressed(); + return true; + } return false; } @@ -517,6 +532,17 @@ public class Dialog implements DialogInterface, Window.Callback, return false; } + /** + * Called when the dialog has detected the user's press of the back + * key. The default implementation simply cancels the dialog (only if + * it is cancelable), but you can override this to do whatever you want. + */ + public void onBackPressed() { + if (mCancelable) { + cancel(); + } + } + /** * Called when a touch screen event was not handled by any of the views * under it. This is most useful to process touch events that happen outside @@ -599,7 +625,8 @@ public class Dialog implements DialogInterface, Window.Callback, if (mWindow.superDispatchKeyEvent(event)) { return true; } - return event.dispatch(this); + return event.dispatch(this, mDecor != null + ? mDecor.getKeyDispatcherState() : null, this); } /** diff --git a/core/java/android/app/SearchDialog.java b/core/java/android/app/SearchDialog.java index 75e4669575f98..62dc9b23d023a 100644 --- a/core/java/android/app/SearchDialog.java +++ b/core/java/android/app/SearchDialog.java @@ -745,11 +745,9 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS return true; } - if (keyCode == KeyEvent.KEYCODE_SEARCH && event.getRepeatCount() < 1) { - // If the search key is pressed, toggle between global and in-app search. If we are - // currently doing global search and there is no in-app search context to toggle to, - // just don't do anything. - return toggleGlobalSearch(); + if (keyCode == KeyEvent.KEYCODE_SEARCH) { + // Consume search key for later use. + return true; } // if it's an action specified by the searchable activity, launch the @@ -763,6 +761,29 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS return false; } + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (DBG) Log.d(LOG_TAG, "onKeyUp(" + keyCode + "," + event + ")"); + if (mSearchable == null) { + return false; + } + + // handle back key to go back to previous searchable, etc. + if (handleBackKey(keyCode, event)) { + return true; + } + + if (keyCode == KeyEvent.KEYCODE_SEARCH && event.isTracking() + && !event.isCanceled()) { + // If the search key is pressed, toggle between global and in-app search. If we are + // currently doing global search and there is no in-app search context to toggle to, + // just don't do anything. + return toggleGlobalSearch(); + } + + return false; + } + /** * Callback to watch the textedit field for empty/non-empty */ @@ -1500,21 +1521,24 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS * * @return true if there was a previous component that we could go back to. */ - private boolean backToPreviousComponent() { + private boolean backToPreviousComponent(boolean doIt) { ComponentName previous = popPreviousComponent(); if (previous == null) { return false; } - if (!show(previous, mAppSearchData, false)) { - Log.w(LOG_TAG, "Failed to switch to source " + previous); - return false; - } - // must touch text to trigger suggestions - // TODO: should this be the text as it was when the user left - // the source that we are now going back to? - String query = mSearchAutoComplete.getText().toString(); - setUserQuery(query); + if (doIt) { + if (!show(previous, mAppSearchData, false)) { + Log.w(LOG_TAG, "Failed to switch to source " + previous); + return false; + } + + // must touch text to trigger suggestions + // TODO: should this be the text as it was when the user left + // the source that we are now going back to? + String query = mSearchAutoComplete.getText().toString(); + setUserQuery(query); + } return true; } @@ -1660,6 +1684,7 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS public static class SearchAutoComplete extends AutoCompleteTextView { private int mThreshold; + private int mLastKeyDown; private SearchDialog mSearchDialog; public SearchAutoComplete(Context context) { @@ -1740,27 +1765,44 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS */ @Override public boolean onKeyPreIme(int keyCode, KeyEvent event) { + mLastKeyDown = keyCode; if (mSearchDialog.mSearchable == null) { return false; } - if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_DOWN) { - if (mSearchDialog.backToPreviousComponent()) { - return true; + if (keyCode == KeyEvent.KEYCODE_BACK) { + if (event.getAction() == KeyEvent.ACTION_DOWN + && event.getRepeatCount() == 0) { + // We releae the back key, might we want to do + // something before the IME? + if (mSearchDialog.backToPreviousComponent(false)) { + return true; + } + if (isInputMethodNotNeeded() || + (isEmpty() && getDropDownChildCount() >= getAdapterCount())) { + return true; + } + mLastKeyDown = 0; + return false; // will dismiss soft keyboard if necessary + } else if (event.getAction() == KeyEvent.ACTION_UP + && mLastKeyDown == keyCode && !event.isCanceled()) { + if (mSearchDialog.backToPreviousComponent(true)) { + return true; + } + // If the drop-down obscures the keyboard, the user wouldn't see anything + // happening when pressing back, so we dismiss the entire dialog instead. + // + // also: if there is no text entered, we also want to dismiss the whole dialog, + // not just the soft keyboard. the exception to this is if there are shortcuts + // that aren't displayed (e.g are being obscured by the soft keyboard); in that + // case we want to dismiss the soft keyboard so the user can see the rest of the + // shortcuts. + if (isInputMethodNotNeeded() || + (isEmpty() && getDropDownChildCount() >= getAdapterCount())) { + mSearchDialog.cancel(); + return true; + } + return false; // will dismiss soft keyboard if necessary } - // If the drop-down obscures the keyboard, the user wouldn't see anything - // happening when pressing back, so we dismiss the entire dialog instead. - // - // also: if there is no text entered, we also want to dismiss the whole dialog, - // not just the soft keyboard. the exception to this is if there are shortcuts - // that aren't displayed (e.g are being obscured by the soft keyboard); in that - // case we want to dismiss the soft keyboard so the user can see the rest of the - // shortcuts. - if (isInputMethodNotNeeded() || - (isEmpty() && getDropDownChildCount() >= getAdapterCount())) { - mSearchDialog.cancel(); - return true; - } - return false; // will dismiss soft keyboard if necessary } return false; } @@ -1772,11 +1814,18 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS } protected boolean handleBackKey(int keyCode, KeyEvent event) { - if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_DOWN) { - if (backToPreviousComponent()) { + if (keyCode == KeyEvent.KEYCODE_BACK) { + if (event.getAction() == KeyEvent.ACTION_DOWN) { + // Consume the event, to get an up at which point we execute. return true; } - cancel(); + if (event.getAction() == KeyEvent.ACTION_UP && event.isTracking() + && !event.isCanceled()) { + if (backToPreviousComponent(true)) { + return true; + } + cancel(); + } return true; } return false; diff --git a/core/java/android/inputmethodservice/AbstractInputMethodService.java b/core/java/android/inputmethodservice/AbstractInputMethodService.java index eedcc354e6133..36196536f689d 100644 --- a/core/java/android/inputmethodservice/AbstractInputMethodService.java +++ b/core/java/android/inputmethodservice/AbstractInputMethodService.java @@ -45,6 +45,9 @@ public abstract class AbstractInputMethodService extends Service implements KeyEvent.Callback { private InputMethod mInputMethod; + final KeyEvent.DispatcherState mDispatcherState + = new KeyEvent.DispatcherState(); + /** * Base class for derived classes to implement their {@link InputMethod} * interface. This takes care of basic maintenance of the input method, @@ -129,7 +132,8 @@ public abstract class AbstractInputMethodService extends Service * callbacks on the service, and tell the client when this is done. */ public void dispatchKeyEvent(int seq, KeyEvent event, EventCallback callback) { - boolean handled = event.dispatch(AbstractInputMethodService.this); + boolean handled = event.dispatch(AbstractInputMethodService.this, + mDispatcherState, this); if (callback != null) { callback.finishedEvent(seq, handled); } @@ -147,6 +151,16 @@ public abstract class AbstractInputMethodService extends Service } } + /** + * Return the global {@link KeyEvent.DispatcherState KeyEvent.DispatcherState} + * for used for processing events from the target application. + * Normally you will not need to use this directly, but + * just use the standard high-level event callbacks like {@link #onKeyDown}. + */ + public KeyEvent.DispatcherState getKeyDispatcherState() { + return mDispatcherState; + } + /** * Called by the framework during initialization, when the InputMethod * interface for this service needs to be created. diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java index 1f640ea12bb00..5499bbafb0ff9 100644 --- a/core/java/android/inputmethodservice/InputMethodService.java +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -554,7 +554,7 @@ public class InputMethodService extends AbstractInputMethodService { mImm = (InputMethodManager)getSystemService(INPUT_METHOD_SERVICE); mInflater = (LayoutInflater)getSystemService( Context.LAYOUT_INFLATER_SERVICE); - mWindow = new SoftInputWindow(this, mTheme); + mWindow = new SoftInputWindow(this, mTheme, mDispatcherState); initViews(); mWindow.getWindow().setLayout(FILL_PARENT, WRAP_CONTENT); } @@ -1557,6 +1557,28 @@ public class InputMethodService extends AbstractInputMethodService { mImm.showSoftInputFromInputMethod(mToken, flags); } + private boolean handleBack(boolean doIt) { + if (mShowInputRequested) { + // If the soft input area is shown, back closes it and we + // consume the back key. + if (doIt) requestHideSelf(0); + return true; + } else if (mWindowVisible) { + if (mCandidatesVisibility == View.VISIBLE) { + // If we are showing candidates even if no input area, then + // hide them. + if (doIt) setCandidatesViewShown(false); + } else { + // If we have the window visible for some other reason -- + // most likely to show candidates -- then just get rid + // of it. This really shouldn't happen, but just in case... + if (doIt) hideWindow(); + } + return true; + } + return false; + } + /** * Override this to intercept key down events before they are processed by the * application. If you return true, the application will not itself @@ -1564,37 +1586,32 @@ public class InputMethodService extends AbstractInputMethodService { * will occur as if the IME had not seen the event at all. * *

The default implementation intercepts {@link KeyEvent#KEYCODE_BACK - * KeyEvent.KEYCODE_BACK} to hide the current IME UI if it is shown. In - * additional, in fullscreen mode only, it will consume DPAD movement + * KeyEvent.KEYCODE_BACK} if the IME is currently shown, to + * possibly hide it when the key goes up (if not canceled or long pressed). In + * addition, in fullscreen mode only, it will consume DPAD movement * events to move the cursor in the extracted text view, not allowing * them to perform navigation in the underlying application. */ public boolean onKeyDown(int keyCode, KeyEvent event) { - if (event.getKeyCode() == KeyEvent.KEYCODE_BACK - && event.getRepeatCount() == 0) { - if (mShowInputRequested) { - // If the soft input area is shown, back closes it and we - // consume the back key. - requestHideSelf(0); + if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { + if (handleBack(false)) { + event.startTracking(); return true; - } else if (mWindowVisible) { - if (mCandidatesVisibility == View.VISIBLE) { - // If we are showing candidates even if no input area, then - // hide them. - setCandidatesViewShown(false); - return true; - } else { - // If we have the window visible for some other reason -- - // most likely to show candidates -- then just get rid - // of it. This really shouldn't happen, but just in case... - hideWindow(); - return true; - } } + return false; } return doMovementKey(keyCode, event, MOVEMENT_DOWN); } + /** + * Default implementation of {@link KeyEvent.Callback#onKeyLongPress(int, KeyEvent) + * KeyEvent.Callback.onKeyLongPress()}: always returns false (doesn't handle + * the event). + */ + public boolean onKeyLongPress(int keyCode, KeyEvent event) { + return false; + } + /** * Override this to intercept special key multiple events before they are * processed by the @@ -1617,12 +1634,18 @@ public class InputMethodService extends AbstractInputMethodService { * process the event. If you return true, the normal application processing * will occur as if the IME had not seen the event at all. * - *

The default implementation always returns false, except when - * in fullscreen mode, where it will consume DPAD movement + *

The default implementation intercepts {@link KeyEvent#KEYCODE_BACK + * KeyEvent.KEYCODE_BACK} to hide the current IME UI if it is shown. In + * addition, in fullscreen mode only, it will consume DPAD movement * events to move the cursor in the extracted text view, not allowing * them to perform navigation in the underlying application. */ public boolean onKeyUp(int keyCode, KeyEvent event) { + if (event.getKeyCode() == KeyEvent.KEYCODE_BACK && event.isTracking() + && !event.isCanceled()) { + return handleBack(true); + } + return doMovementKey(keyCode, event, MOVEMENT_UP); } diff --git a/core/java/android/inputmethodservice/SoftInputWindow.java b/core/java/android/inputmethodservice/SoftInputWindow.java index d91ace6b778f6..6a54846fddbe8 100644 --- a/core/java/android/inputmethodservice/SoftInputWindow.java +++ b/core/java/android/inputmethodservice/SoftInputWindow.java @@ -21,6 +21,7 @@ import android.content.Context; import android.content.pm.ActivityInfo; import android.os.IBinder; import android.view.Gravity; +import android.view.KeyEvent; import android.view.WindowManager; /** @@ -30,7 +31,8 @@ import android.view.WindowManager; * always visible. */ class SoftInputWindow extends Dialog { - + final KeyEvent.DispatcherState mDispatcherState; + public void setToken(IBinder token) { WindowManager.LayoutParams lp = getWindow().getAttributes(); lp.token = token; @@ -49,11 +51,19 @@ class SoftInputWindow extends Dialog { * using styles. This theme is applied on top of the current theme in * context. If 0, the default dialog theme will be used. */ - public SoftInputWindow(Context context, int theme) { + public SoftInputWindow(Context context, int theme, + KeyEvent.DispatcherState dispatcherState) { super(context, theme); + mDispatcherState = dispatcherState; initDockWindow(); } + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + mDispatcherState.reset(); + } + /** * Get the size of the DockWindow. * diff --git a/core/java/android/view/KeyEvent.java b/core/java/android/view/KeyEvent.java index f9b16fc9f1a55..daa4b295a0f42 100644 --- a/core/java/android/view/KeyEvent.java +++ b/core/java/android/view/KeyEvent.java @@ -18,6 +18,7 @@ package android.view; import android.os.Parcel; import android.os.Parcelable; +import android.util.SparseIntArray; import android.view.KeyCharacterMap; import android.view.KeyCharacterMap.KeyData; @@ -276,6 +277,32 @@ public class KeyEvent implements Parcelable { */ public static final int FLAG_VIRTUAL_HARD_KEY = 0x40; + /** + * This flag is set for the first key repeat that occurs after the + * long press timeout. + */ + public static final int FLAG_LONG_PRESS = 0x80; + + /** + * Set when a key event has {@link #FLAG_CANCELED} set because a long + * press action was executed while it was down. + */ + public static final int FLAG_CANCELED_LONG_PRESS = 0x100; + + /** + * Set for {@link #ACTION_UP} when this event's key code is still being + * tracked from its initial down. That is, somebody requested that tracking + * started on the key down and a long press has not caused + * the tracking to be canceled. + */ + public static final int FLAG_TRACKING = 0x200; + + /** + * Private control to determine when an app is tracking a key sequence. + * @hide + */ + public static final int FLAG_START_TRACKING = 0x40000000; + /** * Returns the maximum keycode. */ @@ -305,7 +332,11 @@ public class KeyEvent implements Parcelable { public interface Callback { /** - * Called when a key down event has occurred. + * Called when a key down event has occurred. If you return true, + * you can first call {@link KeyEvent#startTracking() + * KeyEvent.startTracking()} to have the framework track the event + * through its {@link #onKeyUp(int, KeyEvent)} and also call your + * {@link #onKeyLongPress(int, KeyEvent)} if it occurs. * * @param keyCode The value in event.getKeyCode(). * @param event Description of the key event. @@ -315,6 +346,22 @@ public class KeyEvent implements Parcelable { */ boolean onKeyDown(int keyCode, KeyEvent event); + /** + * Called when a long press has occurred. If you return true, + * the final key up will have {@link KeyEvent#FLAG_CANCELED} and + * {@link KeyEvent#FLAG_CANCELED_LONG_PRESS} set. Note that in + * order to receive this callback, someone in the event change + * must return true from {@link #onKeyDown} and + * call {@link KeyEvent#startTracking()} on the event. + * + * @param keyCode The value in event.getKeyCode(). + * @param event Description of the key event. + * + * @return If you handled the event, return true. If you want to allow + * the event to be handled by the next receiver, return false. + */ + boolean onKeyLongPress(int keyCode, KeyEvent event); + /** * Called when a key up event has occurred. * @@ -500,11 +547,15 @@ public class KeyEvent implements Parcelable { /** * Copy an existing key event, modifying its time and repeat count. * + * @deprecated Use {@link #changeTimeRepeat(KeyEvent, long, int)} + * instead. + * * @param origEvent The existing event to be copied. * @param eventTime The new event time * (in {@link android.os.SystemClock#uptimeMillis}) of the event. * @param newRepeat The new repeat count of the event. */ + @Deprecated public KeyEvent(KeyEvent origEvent, long eventTime, int newRepeat) { mDownTime = origEvent.mDownTime; mEventTime = eventTime; @@ -532,6 +583,26 @@ public class KeyEvent implements Parcelable { return new KeyEvent(event, eventTime, newRepeat); } + /** + * Create a new key event that is the same as the given one, but whose + * event time and repeat count are replaced with the given value. + * + * @param event The existing event to be copied. This is not modified. + * @param eventTime The new event time + * (in {@link android.os.SystemClock#uptimeMillis}) of the event. + * @param newRepeat The new repeat count of the event. + * @param newFlags New flags for the event, replacing the entire value + * in the original event. + */ + public static KeyEvent changeTimeRepeat(KeyEvent event, long eventTime, + int newRepeat, int newFlags) { + KeyEvent ret = new KeyEvent(event); + ret.mEventTime = eventTime; + ret.mRepeatCount = newRepeat; + ret.mFlags = newFlags; + return ret; + } + /** * Copy an existing key event, modifying its action. * @@ -720,6 +791,34 @@ public class KeyEvent implements Parcelable { return (mFlags&FLAG_CANCELED) != 0; } + /** + * Call this during {@link Callback#onKeyDown} to have the system track + * the key through its final up (possibly including a long press). Note + * that only one key can be tracked at a time -- if another key down + * event is received while a previous one is being tracked, tracking is + * stopped on the previous event. + */ + public final void startTracking() { + mFlags |= FLAG_START_TRACKING; + } + + /** + * For {@link #ACTION_UP} events, indicates that the event is still being + * tracked from its initial down event as per + * {@link #FLAG_TRACKING}. + */ + public final boolean isTracking() { + return (mFlags&FLAG_TRACKING) != 0; + } + + /** + * For {@link #ACTION_DOWN} events, indicates that the event has been + * canceled as per {@link #FLAG_LONG_PRESS}. + */ + public final boolean isLongPress() { + return (mFlags&FLAG_LONG_PRESS) != 0; + } + /** * Retrieve the key code of the key event. This is the physical key that * was pressed, not the Unicode character. @@ -905,20 +1004,50 @@ public class KeyEvent implements Parcelable { return KeyCharacterMap.load(mDeviceId).isPrintingKey(mKeyCode); } + /** + * @deprecated Use {@link #dispatch(Callback, DispatcherState, Object)} instead. + */ + @Deprecated + public final boolean dispatch(Callback receiver) { + return dispatch(receiver, null, null); + } + /** * Deliver this key event to a {@link Callback} interface. If this is * an ACTION_MULTIPLE event and it is not handled, then an attempt will * be made to deliver a single normal event. * * @param receiver The Callback that will be given the event. + * @param state State information retained across events. + * @param target The target of the dispatch, for use in tracking. * * @return The return value from the Callback method that was called. */ - public final boolean dispatch(Callback receiver) { + public final boolean dispatch(Callback receiver, DispatcherState state, + Object target) { switch (mAction) { - case ACTION_DOWN: - return receiver.onKeyDown(mKeyCode, this); + case ACTION_DOWN: { + mFlags &= ~FLAG_START_TRACKING; + boolean res = receiver.onKeyDown(mKeyCode, this); + if (state != null) { + if (res && mRepeatCount == 0 && (mFlags&FLAG_START_TRACKING) != 0) { + state.startTracking(this, target); + } else if (isLongPress() && state.isTracking(this)) { + try { + if (receiver.onKeyLongPress(mKeyCode, this)) { + state.performedLongPress(this); + res = true; + } + } catch (AbstractMethodError e) { + } + } + } + return res; + } case ACTION_UP: + if (state != null) { + state.handleUpEvent(this); + } return receiver.onKeyUp(mKeyCode, this); case ACTION_MULTIPLE: final int count = mRepeatCount; @@ -938,10 +1067,97 @@ public class KeyEvent implements Parcelable { mRepeatCount = count; return handled; } + return false; } return false; } + /** + * Use with {@link KeyEvent#dispatch(Callback, DispatcherState, Object)} + * for more advanced key dispatching, such as long presses. + */ + public static class DispatcherState { + int mDownKeyCode; + Object mDownTarget; + SparseIntArray mActiveLongPresses = new SparseIntArray(); + + /** + * Reset back to initial state. + */ + public void reset() { + mDownKeyCode = 0; + mDownTarget = null; + mActiveLongPresses.clear(); + } + + /** + * Stop any tracking associated with this target. + */ + public void reset(Object target) { + if (mDownTarget == target) { + mDownKeyCode = 0; + mDownTarget = null; + } + } + + /** + * Start tracking the key code associated with the given event. This + * can only be called on a key down. It will allow you to see any + * long press associated with the key, and will result in + * {@link KeyEvent#isTracking} return true on the long press and up + * events. + * + *

This is only needed if you are directly dispatching events, rather + * than handling them in {@link Callback#onKeyDown}. + */ + public void startTracking(KeyEvent event, Object target) { + if (event.getAction() != ACTION_DOWN) { + throw new IllegalArgumentException( + "Can only start tracking on a down event"); + } + mDownKeyCode = event.getKeyCode(); + mDownTarget = target; + } + + /** + * Return true if the key event is for a key code that is currently + * being tracked by the dispatcher. + */ + public boolean isTracking(KeyEvent event) { + return mDownKeyCode == event.getKeyCode(); + } + + /** + * Keep track of the given event's key code as having performed an + * action with a long press, so no action should occur on the up. + *

This is only needed if you are directly dispatching events, rather + * than handling them in {@link Callback#onKeyLongPress}. + */ + public void performedLongPress(KeyEvent event) { + mActiveLongPresses.put(event.getKeyCode(), 1); + } + + /** + * Handle key up event to stop tracking. This resets the dispatcher state, + * and updates the key event state based on it. + *

This is only needed if you are directly dispatching events, rather + * than handling them in {@link Callback#onKeyUp}. + */ + public void handleUpEvent(KeyEvent event) { + final int keyCode = event.getKeyCode(); + int index = mActiveLongPresses.indexOfKey(keyCode); + if (index >= 0) { + event.mFlags |= FLAG_CANCELED | FLAG_CANCELED_LONG_PRESS; + mActiveLongPresses.removeAt(index); + } + if (mDownKeyCode == keyCode) { + event.mFlags |= FLAG_TRACKING; + mDownKeyCode = 0; + mDownTarget = null; + } + } + } + public String toString() { return "KeyEvent{action=" + mAction + " code=" + mKeyCode + " repeat=" + mRepeatCount diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 2cc243e843424..6ff0fc83c22d5 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -2600,6 +2600,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility if (mOnFocusChangeListener != null) { mOnFocusChangeListener.onFocusChange(this, gainFocus); } + + if (mAttachInfo != null) { + mAttachInfo.mKeyDispatchState.reset(this); + } } /** @@ -3608,6 +3612,16 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility ViewDebug.dumpCapturedView(subTag, v); } + /** + * Return the global {@link KeyEvent.DispatcherState KeyEvent.DispatcherState} + * for this view's window. Returns null if the view is not currently attached + * to the window. Normally you will not need to use this directly, but + * just use the standard high-level event callbacks like {@link #onKeyDown}. + */ + public KeyEvent.DispatcherState getKeyDispatcherState() { + return mAttachInfo != null ? mAttachInfo.mKeyDispatchState : null; + } + /** * Dispatch a key event before it is processed by any input method * associated with the view hierarchy. This can be used to intercept @@ -3645,7 +3659,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility return true; } - return event.dispatch(this); + return event.dispatch(this, mAttachInfo != null + ? mAttachInfo.mKeyDispatchState : null, this); } /** @@ -3909,6 +3924,15 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility return result; } + /** + * Default implementation of {@link KeyEvent.Callback#onKeyLongPress(int, KeyEvent) + * KeyEvent.Callback.onKeyLongPress()}: always returns false (doesn't handle + * the event). + */ + public boolean onKeyLongPress(int keyCode, KeyEvent event) { + return false; + } + /** * Default implementation of {@link KeyEvent.Callback#onKeyMultiple(int, int, KeyEvent) * KeyEvent.Callback.onKeyMultiple()}: perform clicking of the view @@ -8572,6 +8596,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility */ final ArrayList mScrollContainers = new ArrayList(); + final KeyEvent.DispatcherState mKeyDispatchState + = new KeyEvent.DispatcherState(); + /** * Indicates whether the view's window currently has the focus. */ diff --git a/core/java/android/view/ViewRoot.java b/core/java/android/view/ViewRoot.java index 6748ade40ce67..c6937a3a332f7 100644 --- a/core/java/android/view/ViewRoot.java +++ b/core/java/android/view/ViewRoot.java @@ -1812,6 +1812,7 @@ public final class ViewRoot extends Handler implements ViewParent, if (hasWindowFocus && imm != null && mLastWasImTarget) { imm.startGettingWindowFocus(mView); } + mAttachInfo.mKeyDispatchState.reset(); mView.dispatchWindowFocusChanged(hasWindowFocus); } diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index 2f292d58aeb2f..bed2a7aa275e6 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -2945,10 +2945,14 @@ public abstract class AbsListView extends AdapterView implements Te okToSend = false; break; case KeyEvent.KEYCODE_BACK: - if (mFiltered && mPopup != null && mPopup.isShowing() && - event.getAction() == KeyEvent.ACTION_DOWN) { - handled = true; - mTextFilter.setText(""); + if (mFiltered && mPopup != null && mPopup.isShowing()) { + if (event.getAction() == KeyEvent.ACTION_DOWN) { + handled = true; + } else if (event.getAction() == KeyEvent.ACTION_UP + && event.isTracking() && !event.isCanceled()) { + handled = true; + mTextFilter.setText(""); + } } okToSend = false; break; diff --git a/core/java/android/widget/AutoCompleteTextView.java b/core/java/android/widget/AutoCompleteTextView.java index d821a7df6ddb1..7891d3cf177e1 100644 --- a/core/java/android/widget/AutoCompleteTextView.java +++ b/core/java/android/widget/AutoCompleteTextView.java @@ -132,6 +132,8 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe private AutoCompleteTextView.PassThroughClickListener mPassThroughClickListener; + private int mDownKeyCode; + public AutoCompleteTextView(Context context) { this(context, null); } @@ -603,12 +605,19 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe @Override public boolean onKeyPreIme(int keyCode, KeyEvent event) { + if (event.getAction() == KeyEvent.ACTION_DOWN + && event.getRepeatCount() == 0) { + mDownKeyCode = keyCode; + } if (isPopupShowing()) { // special case for the back key, we do not even try to send it // to the drop down list but instead, consume it immediately if (keyCode == KeyEvent.KEYCODE_BACK && !mDropDownAlwaysVisible) { - dismissDropDown(); - return true; + if (event.getAction() == KeyEvent.ACTION_UP + && mDownKeyCode == keyCode && !event.isCanceled()) { + dismissDropDown(); + return true; + } } } return super.onKeyPreIme(keyCode, event); @@ -1017,6 +1026,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe @Override public void onWindowFocusChanged(boolean hasWindowFocus) { super.onWindowFocusChanged(hasWindowFocus); + mDownKeyCode = 0; performValidation(); if (!hasWindowFocus && !mDropDownAlwaysVisible) { dismissDropDown(); @@ -1026,6 +1036,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe @Override protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { super.onFocusChanged(focused, direction, previouslyFocusedRect); + mDownKeyCode = 0; performValidation(); if (!focused && !mDropDownAlwaysVisible) { dismissDropDown(); diff --git a/services/java/com/android/server/KeyInputQueue.java b/services/java/com/android/server/KeyInputQueue.java index 244e13679bd0c..d9f4c9c7654b2 100644 --- a/services/java/com/android/server/KeyInputQueue.java +++ b/services/java/com/android/server/KeyInputQueue.java @@ -981,6 +981,29 @@ public abstract class KeyInputQueue { } } + /** + * Return true if the queue has an up event pending that corresponds + * to the same key as the given key event. + */ + boolean hasKeyUpEvent(KeyEvent origEvent) { + synchronized (mFirst) { + final int keyCode = origEvent.getKeyCode(); + QueuedEvent cur = mLast.prev; + while (cur.prev != null) { + if (cur.classType == RawInputEvent.CLASS_KEYBOARD) { + KeyEvent ke = (KeyEvent)cur.event; + if (ke.getAction() == KeyEvent.ACTION_UP + && ke.getKeyCode() == keyCode) { + return true; + } + } + cur = cur.prev; + } + } + + return false; + } + void recycleEvent(QueuedEvent ev) { synchronized (mFirst) { //Log.i(TAG, "Recycle event: " + ev); diff --git a/services/java/com/android/server/WindowManagerService.java b/services/java/com/android/server/WindowManagerService.java index c844de224d3fe..8e85a6a4b14e4 100644 --- a/services/java/com/android/server/WindowManagerService.java +++ b/services/java/com/android/server/WindowManagerService.java @@ -99,6 +99,7 @@ import android.view.RawInputEvent; import android.view.Surface; import android.view.SurfaceSession; import android.view.View; +import android.view.ViewConfiguration; import android.view.ViewTreeObserver; import android.view.WindowManager; import android.view.WindowManagerImpl; @@ -151,9 +152,6 @@ public class WindowManagerService extends IWindowManager.Stub static final int LOG_WM_NO_SURFACE_MEMORY = 31000; - /** How long to wait for first key repeat, in milliseconds */ - static final int KEY_REPEAT_FIRST_DELAY = 750; - /** How long to wait for subsequent key repeats, in milliseconds */ static final int KEY_REPEAT_DELAY = 50; @@ -4900,6 +4898,16 @@ public class WindowManagerService extends IWindowManager.Stub return INJECT_SUCCEEDED; } + // Okay we have finished waiting for the last event to be processed. + // First off, if this is a repeat event, check to see if there is + // a corresponding up event in the queue. If there is, we will + // just drop the repeat, because it makes no sense to repeat after + // the user has released a key. (This is especially important for + // long presses.) + if (event.getRepeatCount() > 0 && mQueue.hasKeyUpEvent(event)) { + return INJECT_SUCCEEDED; + } + WindowState focus = (WindowState)focusObj; if (DEBUG_INPUT) Log.v( @@ -6018,6 +6026,7 @@ public class WindowManagerService extends IWindowManager.Stub // Last keydown time for auto-repeating keys long lastKeyTime = SystemClock.uptimeMillis(); long nextKeyTime = lastKeyTime+LONG_WAIT; + long downTime = 0; // How many successive repeats we generated int keyRepeatCount = 0; @@ -6088,15 +6097,17 @@ public class WindowManagerService extends IWindowManager.Stub KeyEvent ke = (KeyEvent)ev.event; if (ke.isDown()) { lastKey = ke; + downTime = curTime; keyRepeatCount = 0; lastKeyTime = curTime; nextKeyTime = lastKeyTime - + KEY_REPEAT_FIRST_DELAY; + + ViewConfiguration.getLongPressTimeout(); if (DEBUG_INPUT) Log.v( TAG, "Received key down: first repeat @ " + nextKeyTime); } else { lastKey = null; + downTime = 0; // Arbitrary long timeout. lastKeyTime = curTime; nextKeyTime = curTime + LONG_WAIT; @@ -6144,7 +6155,19 @@ public class WindowManagerService extends IWindowManager.Stub if (DEBUG_INPUT) Log.v( TAG, "Key repeat: count=" + keyRepeatCount + ", next @ " + nextKeyTime); - dispatchKey(KeyEvent.changeTimeRepeat(lastKey, curTime, keyRepeatCount), 0, 0); + KeyEvent newEvent; + if (downTime != 0 && (downTime + + ViewConfiguration.getLongPressTimeout()) + <= curTime) { + newEvent = KeyEvent.changeTimeRepeat(lastKey, + curTime, keyRepeatCount, + lastKey.getFlags() | KeyEvent.FLAG_LONG_PRESS); + downTime = 0; + } else { + newEvent = KeyEvent.changeTimeRepeat(lastKey, + curTime, keyRepeatCount); + } + dispatchKey(newEvent, 0, 0); } else { curTime = SystemClock.uptimeMillis();