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