diff options
author | Justin Klaassen <justinklaassen@google.com> | 2017-09-15 17:58:39 -0400 |
---|---|---|
committer | Justin Klaassen <justinklaassen@google.com> | 2017-09-15 17:58:39 -0400 |
commit | 10d07c88d69cc64f73a069163e7ea5ba2519a099 (patch) | |
tree | 8dbd149eb350320a29c3d10e7ad3201de1c5cbee /android/inputmethodservice | |
parent | 677516fb6b6f207d373984757d3d9450474b6b00 (diff) | |
download | android-28-10d07c88d69cc64f73a069163e7ea5ba2519a099.tar.gz |
Import Android SDK Platform PI [4335822]
/google/data/ro/projects/android/fetch_artifact \
--bid 4335822 \
--target sdk_phone_armv7-win_sdk \
sdk-repo-linux-sources-4335822.zip
AndroidVersion.ApiLevel has been modified to appear as 28
Change-Id: Ic8f04be005a71c2b9abeaac754d8da8d6f9a2c32
Diffstat (limited to 'android/inputmethodservice')
-rw-r--r-- | android/inputmethodservice/AbstractInputMethodService.java | 251 | ||||
-rw-r--r-- | android/inputmethodservice/CompactExtractEditLayout.java | 136 | ||||
-rw-r--r-- | android/inputmethodservice/ExtractButton.java | 50 | ||||
-rw-r--r-- | android/inputmethodservice/ExtractEditLayout.java | 46 | ||||
-rw-r--r-- | android/inputmethodservice/ExtractEditText.java | 223 | ||||
-rw-r--r-- | android/inputmethodservice/IInputMethodSessionWrapper.java | 253 | ||||
-rw-r--r-- | android/inputmethodservice/IInputMethodWrapper.java | 317 | ||||
-rw-r--r-- | android/inputmethodservice/InputMethodService.java | 2716 | ||||
-rw-r--r-- | android/inputmethodservice/Keyboard.java | 907 | ||||
-rw-r--r-- | android/inputmethodservice/KeyboardView.java | 1559 | ||||
-rw-r--r-- | android/inputmethodservice/SoftInputWindow.java | 193 |
11 files changed, 6651 insertions, 0 deletions
diff --git a/android/inputmethodservice/AbstractInputMethodService.java b/android/inputmethodservice/AbstractInputMethodService.java new file mode 100644 index 00000000..29177b6b --- /dev/null +++ b/android/inputmethodservice/AbstractInputMethodService.java @@ -0,0 +1,251 @@ +/* + * 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.inputmethodservice; + +import android.annotation.NonNull; +import android.app.Service; +import android.content.Intent; +import android.os.IBinder; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.inputmethod.InputConnection; +import android.view.inputmethod.InputContentInfo; +import android.view.inputmethod.InputMethod; +import android.view.inputmethod.InputMethodSession; + +import java.io.FileDescriptor; +import java.io.PrintWriter; + +/** + * AbstractInputMethodService provides a abstract base class for input methods. + * Normal input method implementations will not derive from this directly, + * instead building on top of {@link InputMethodService} or another more + * complete base class. Be sure to read {@link InputMethod} for more + * information on the basics of writing input methods. + * + * <p>This class combines a Service (representing the input method component + * to the system with the InputMethod interface that input methods must + * implement. This base class takes care of reporting your InputMethod from + * the service when clients bind to it, but provides no standard implementation + * of the InputMethod interface itself. Derived classes must implement that + * interface. + */ +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, + * but most behavior must be implemented in a derived class. + */ + public abstract class AbstractInputMethodImpl implements InputMethod { + /** + * Instantiate a new client session for the input method, by calling + * back to {@link AbstractInputMethodService#onCreateInputMethodSessionInterface() + * AbstractInputMethodService.onCreateInputMethodSessionInterface()}. + */ + public void createSession(SessionCallback callback) { + callback.sessionCreated(onCreateInputMethodSessionInterface()); + } + + /** + * Take care of enabling or disabling an existing session by calling its + * {@link AbstractInputMethodSessionImpl#revokeSelf() + * AbstractInputMethodSessionImpl.setEnabled()} method. + */ + public void setSessionEnabled(InputMethodSession session, boolean enabled) { + ((AbstractInputMethodSessionImpl)session).setEnabled(enabled); + } + + /** + * Take care of killing an existing session by calling its + * {@link AbstractInputMethodSessionImpl#revokeSelf() + * AbstractInputMethodSessionImpl.revokeSelf()} method. + */ + public void revokeSession(InputMethodSession session) { + ((AbstractInputMethodSessionImpl)session).revokeSelf(); + } + } + + /** + * Base class for derived classes to implement their {@link InputMethodSession} + * interface. This takes care of basic maintenance of the session, + * but most behavior must be implemented in a derived class. + */ + public abstract class AbstractInputMethodSessionImpl implements InputMethodSession { + boolean mEnabled = true; + boolean mRevoked; + + /** + * Check whether this session has been enabled by the system. If not + * enabled, you should not execute any calls on to it. + */ + public boolean isEnabled() { + return mEnabled; + } + + /** + * Check whether this session has been revoked by the system. Revoked + * session is also always disabled, so there is generally no need to + * explicitly check for this. + */ + public boolean isRevoked() { + return mRevoked; + } + + /** + * Change the enabled state of the session. This only works if the + * session has not been revoked. + */ + public void setEnabled(boolean enabled) { + if (!mRevoked) { + mEnabled = enabled; + } + } + + /** + * Revoke the session from the client. This disabled the session, and + * prevents it from ever being enabled again. + */ + public void revokeSelf() { + mRevoked = true; + mEnabled = false; + } + + /** + * Take care of dispatching incoming key events to the appropriate + * callbacks on the service, and tell the client when this is done. + */ + @Override + public void dispatchKeyEvent(int seq, KeyEvent event, EventCallback callback) { + boolean handled = event.dispatch(AbstractInputMethodService.this, + mDispatcherState, this); + if (callback != null) { + callback.finishedEvent(seq, handled); + } + } + + /** + * Take care of dispatching incoming trackball events to the appropriate + * callbacks on the service, and tell the client when this is done. + */ + @Override + public void dispatchTrackballEvent(int seq, MotionEvent event, EventCallback callback) { + boolean handled = onTrackballEvent(event); + if (callback != null) { + callback.finishedEvent(seq, handled); + } + } + + /** + * Take care of dispatching incoming generic motion events to the appropriate + * callbacks on the service, and tell the client when this is done. + */ + @Override + public void dispatchGenericMotionEvent(int seq, MotionEvent event, EventCallback callback) { + boolean handled = onGenericMotionEvent(event); + if (callback != null) { + callback.finishedEvent(seq, handled); + } + } + } + + /** + * 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. + */ + public abstract AbstractInputMethodImpl onCreateInputMethodInterface(); + + /** + * Called by the framework when a new InputMethodSession interface is + * needed for a new client of the input method. + */ + public abstract AbstractInputMethodSessionImpl onCreateInputMethodSessionInterface(); + + /** + * Implement this to handle {@link android.os.Binder#dump Binder.dump()} + * calls on your input method. + */ + @Override + protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) { + } + + @Override + final public IBinder onBind(Intent intent) { + if (mInputMethod == null) { + mInputMethod = onCreateInputMethodInterface(); + } + return new IInputMethodWrapper(this, mInputMethod); + } + + /** + * Implement this to handle trackball events on your input method. + * + * @param event The motion event being received. + * @return True if the event was handled in this function, false otherwise. + * @see android.view.View#onTrackballEvent(MotionEvent) + */ + public boolean onTrackballEvent(MotionEvent event) { + return false; + } + + /** + * Implement this to handle generic motion events on your input method. + * + * @param event The motion event being received. + * @return True if the event was handled in this function, false otherwise. + * @see android.view.View#onGenericMotionEvent(MotionEvent) + */ + public boolean onGenericMotionEvent(MotionEvent event) { + return false; + } + + /** + * Allow the receiver of {@link InputContentInfo} to obtain a temporary read-only access + * permission to the content. + * + * <p>Default implementation does nothing.</p> + * + * @param inputContentInfo Content to be temporarily exposed from the input method to the + * application. + * This cannot be {@code null}. + * @param inputConnection {@link InputConnection} with which + * {@link InputConnection#commitContent(InputContentInfo, int, android.os.Bundle)} will be + * called. + * @return {@code false} if we cannot allow a temporary access permission. + * @hide + */ + public void exposeContent(@NonNull InputContentInfo inputContentInfo, + @NonNull InputConnection inputConnection) { + return; + } + +} diff --git a/android/inputmethodservice/CompactExtractEditLayout.java b/android/inputmethodservice/CompactExtractEditLayout.java new file mode 100644 index 00000000..4925d25d --- /dev/null +++ b/android/inputmethodservice/CompactExtractEditLayout.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2016 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.inputmethodservice; + +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.annotation.FractionRes; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowInsets; +import android.widget.LinearLayout; + +/** + * A special purpose layout for the editor extract view for tiny (sub 250dp) screens. + * The layout is based on sizes proportional to screen pixel size to provide for the + * best layout fidelity on varying pixel sizes and densities. + * + * @hide + */ +public class CompactExtractEditLayout extends LinearLayout { + private View mInputExtractEditText; + private View mInputExtractAccessories; + private View mInputExtractAction; + private boolean mPerformLayoutChanges; + + public CompactExtractEditLayout(Context context) { + super(context); + } + + public CompactExtractEditLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public CompactExtractEditLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mInputExtractEditText = findViewById(com.android.internal.R.id.inputExtractEditText); + mInputExtractAccessories = findViewById(com.android.internal.R.id.inputExtractAccessories); + mInputExtractAction = findViewById(com.android.internal.R.id.inputExtractAction); + + if (mInputExtractEditText != null && mInputExtractAccessories != null + && mInputExtractAction != null) { + mPerformLayoutChanges = true; + } + } + + private int applyFractionInt(@FractionRes int fraction, int whole) { + return Math.round(getResources().getFraction(fraction, whole, whole)); + } + + private static void setLayoutHeight(View v, int px) { + ViewGroup.LayoutParams lp = v.getLayoutParams(); + lp.height = px; + v.setLayoutParams(lp); + } + + private static void setLayoutMarginBottom(View v, int px) { + ViewGroup.MarginLayoutParams lp = (MarginLayoutParams) v.getLayoutParams(); + lp.bottomMargin = px; + v.setLayoutParams(lp); + } + + private void applyProportionalLayout(int screenWidthPx, int screenHeightPx) { + if (getResources().getConfiguration().isScreenRound()) { + setGravity(Gravity.BOTTOM); + } + setLayoutHeight(this, applyFractionInt( + com.android.internal.R.fraction.input_extract_layout_height, screenHeightPx)); + + setPadding( + applyFractionInt(com.android.internal.R.fraction.input_extract_layout_padding_left, + screenWidthPx), + 0, + applyFractionInt(com.android.internal.R.fraction.input_extract_layout_padding_right, + screenWidthPx), + 0); + + setLayoutMarginBottom(mInputExtractEditText, + applyFractionInt(com.android.internal.R.fraction.input_extract_text_margin_bottom, + screenHeightPx)); + + setLayoutMarginBottom(mInputExtractAccessories, + applyFractionInt(com.android.internal.R.fraction.input_extract_action_margin_bottom, + screenHeightPx)); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (mPerformLayoutChanges) { + Resources res = getResources(); + Configuration cfg = res.getConfiguration(); + DisplayMetrics dm = res.getDisplayMetrics(); + int widthPixels = dm.widthPixels; + int heightPixels = dm.heightPixels; + + // Percentages must be based on the pixel height of the full (apparent) display height + // which is sometimes different from display metrics. + // + // On a round device, a display height smaller than width indicates a chin (cropped + // edge of the display) for which there is no screen buffer allocated. This is + // typically 25-35px in height. + // + // getRootWindowInsets() does not function for InputMethod windows (always null). + // Instead just set height to match width if less. This is safe because round wear + // devices are by definition 1:1 aspect ratio. + + if (cfg.isScreenRound() && heightPixels < widthPixels) { + heightPixels = widthPixels; + } + applyProportionalLayout(widthPixels, heightPixels); + } + } +} diff --git a/android/inputmethodservice/ExtractButton.java b/android/inputmethodservice/ExtractButton.java new file mode 100644 index 00000000..fe63c1e3 --- /dev/null +++ b/android/inputmethodservice/ExtractButton.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2009 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.inputmethodservice; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.Button; + +/** + * Specialization of {@link Button} that ignores the window not being focused. + */ +class ExtractButton extends Button { + public ExtractButton(Context context) { + super(context, null); + } + + public ExtractButton(Context context, AttributeSet attrs) { + super(context, attrs, com.android.internal.R.attr.buttonStyle); + } + + public ExtractButton(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public ExtractButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + /** + * Pretend like the window this view is in always has focus, so it will + * highlight when selected. + */ + @Override public boolean hasWindowFocus() { + return isEnabled() && getVisibility() == VISIBLE ? true : false; + } +} diff --git a/android/inputmethodservice/ExtractEditLayout.java b/android/inputmethodservice/ExtractEditLayout.java new file mode 100644 index 00000000..af69f0f8 --- /dev/null +++ b/android/inputmethodservice/ExtractEditLayout.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2011 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.inputmethodservice; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.Button; +import android.widget.LinearLayout; + +/** + * ExtractEditLayout provides an ActionMode presentation for the limited screen real estate in + * extract mode. + * + * @hide + */ +public class ExtractEditLayout extends LinearLayout { + Button mExtractActionButton; + + public ExtractEditLayout(Context context) { + super(context); + } + + public ExtractEditLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public void onFinishInflate() { + super.onFinishInflate(); + mExtractActionButton = findViewById(com.android.internal.R.id.inputExtractAction); + } +} diff --git a/android/inputmethodservice/ExtractEditText.java b/android/inputmethodservice/ExtractEditText.java new file mode 100644 index 00000000..a2c1d184 --- /dev/null +++ b/android/inputmethodservice/ExtractEditText.java @@ -0,0 +1,223 @@ +/* + * Copyright (C) 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.inputmethodservice; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.inputmethod.ExtractedText; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; + +/*** + * Specialization of {@link EditText} for showing and interacting with the + * extracted text in a full-screen input method. + */ +public class ExtractEditText extends EditText { + private InputMethodService mIME; + private int mSettingExtractedText; + + public ExtractEditText(Context context) { + super(context, null); + } + + public ExtractEditText(Context context, AttributeSet attrs) { + super(context, attrs, com.android.internal.R.attr.editTextStyle); + } + + public ExtractEditText(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public ExtractEditText(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + void setIME(InputMethodService ime) { + mIME = ime; + } + + /** + * Start making changes that will not be reported to the client. That + * is, {@link #onSelectionChanged(int, int)} will not result in sending + * the new selection to the client + */ + public void startInternalChanges() { + mSettingExtractedText += 1; + } + + /** + * Finish making changes that will not be reported to the client. That + * is, {@link #onSelectionChanged(int, int)} will not result in sending + * the new selection to the client + */ + public void finishInternalChanges() { + mSettingExtractedText -= 1; + } + + /** + * Implement just to keep track of when we are setting text from the + * client (vs. seeing changes in ourself from the user). + */ + @Override public void setExtractedText(ExtractedText text) { + try { + mSettingExtractedText++; + super.setExtractedText(text); + } finally { + mSettingExtractedText--; + } + } + + /** + * Report to the underlying text editor about selection changes. + */ + @Override protected void onSelectionChanged(int selStart, int selEnd) { + if (mSettingExtractedText == 0 && mIME != null && selStart >= 0 && selEnd >= 0) { + mIME.onExtractedSelectionChanged(selStart, selEnd); + } + } + + /** + * Redirect clicks to the IME for handling there. First allows any + * on click handler to run, though. + */ + @Override public boolean performClick() { + if (!super.performClick() && mIME != null) { + mIME.onExtractedTextClicked(); + return true; + } + return false; + } + + @Override public boolean onTextContextMenuItem(int id) { + // Select all and Replace text shouldn't be handled by the original edit text, but by the + // extracted one. + if (id == android.R.id.selectAll || id == android.R.id.replaceText) { + return super.onTextContextMenuItem(id); + } + if (mIME != null && mIME.onExtractTextContextMenuItem(id)) { + // Mode was started on Extracted, needs to be stopped here. + // Cut will change the text, which stops selection mode. + if (id == android.R.id.copy || id == android.R.id.paste) stopTextActionMode(); + return true; + } + return super.onTextContextMenuItem(id); + } + + /** + * We are always considered to be an input method target. + */ + @Override + public boolean isInputMethodTarget() { + return true; + } + + /** + * Return true if the edit text is currently showing a scroll bar. + */ + public boolean hasVerticalScrollBar() { + return computeVerticalScrollRange() > computeVerticalScrollExtent(); + } + + /** + * Pretend like the window this view is in always has focus, so its + * highlight and cursor will be displayed. + */ + @Override public boolean hasWindowFocus() { + return this.isEnabled(); + } + + /** + * Pretend like this view always has focus, so its + * highlight and cursor will be displayed. + */ + @Override public boolean isFocused() { + return this.isEnabled(); + } + + /** + * Pretend like this view always has focus, so its + * highlight and cursor will be displayed. + */ + @Override public boolean hasFocus() { + return this.isEnabled(); + } + + /** + * @hide + */ + @Override protected void viewClicked(InputMethodManager imm) { + // As an instance of this class is supposed to be owned by IMS, + // and it has a reference to the IMS (the current IME), + // we just need to call back its onViewClicked() here. + // It should be good to avoid unnecessary IPCs by doing this as well. + if (mIME != null) { + mIME.onViewClicked(false); + } + } + + /** + * @hide + */ + @Override + public boolean isInExtractedMode() { + return true; + } + + /** + * {@inheritDoc} + * @hide + */ + @Override + protected void deleteText_internal(int start, int end) { + // Do not call the super method. + // This will change the source TextView instead, which will update the ExtractTextView. + mIME.onExtractedDeleteText(start, end); + } + + /** + * {@inheritDoc} + * @hide + */ + @Override + protected void replaceText_internal(int start, int end, CharSequence text) { + // Do not call the super method. + // This will change the source TextView instead, which will update the ExtractTextView. + mIME.onExtractedReplaceText(start, end, text); + } + + /** + * {@inheritDoc} + * @hide + */ + @Override + protected void setSpan_internal(Object span, int start, int end, int flags) { + // Do not call the super method. + // This will change the source TextView instead, which will update the ExtractTextView. + mIME.onExtractedSetSpan(span, start, end, flags); + } + + /** + * {@inheritDoc} + * @hide + */ + @Override + protected void setCursorPosition_internal(int start, int end) { + // Do not call the super method. + // This will change the source TextView instead, which will update the ExtractTextView. + mIME.onExtractedSelectionChanged(start, end); + } +} diff --git a/android/inputmethodservice/IInputMethodSessionWrapper.java b/android/inputmethodservice/IInputMethodSessionWrapper.java new file mode 100644 index 00000000..d2e3510e --- /dev/null +++ b/android/inputmethodservice/IInputMethodSessionWrapper.java @@ -0,0 +1,253 @@ +/* + * Copyright (C) 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.inputmethodservice; + +import android.content.Context; +import android.graphics.Rect; +import android.os.Bundle; +import android.os.Looper; +import android.os.Message; +import android.util.Log; +import android.util.SparseArray; +import android.view.InputChannel; +import android.view.InputDevice; +import android.view.InputEvent; +import android.view.InputEventReceiver; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.inputmethod.CompletionInfo; +import android.view.inputmethod.CursorAnchorInfo; +import android.view.inputmethod.ExtractedText; +import android.view.inputmethod.InputMethodSession; + +import com.android.internal.os.HandlerCaller; +import com.android.internal.os.SomeArgs; +import com.android.internal.view.IInputMethodSession; + +class IInputMethodSessionWrapper extends IInputMethodSession.Stub + implements HandlerCaller.Callback { + private static final String TAG = "InputMethodWrapper"; + + private static final int DO_FINISH_INPUT = 60; + private static final int DO_DISPLAY_COMPLETIONS = 65; + private static final int DO_UPDATE_EXTRACTED_TEXT = 67; + private static final int DO_UPDATE_SELECTION = 90; + private static final int DO_UPDATE_CURSOR = 95; + private static final int DO_UPDATE_CURSOR_ANCHOR_INFO = 99; + private static final int DO_APP_PRIVATE_COMMAND = 100; + private static final int DO_TOGGLE_SOFT_INPUT = 105; + private static final int DO_FINISH_SESSION = 110; + private static final int DO_VIEW_CLICKED = 115; + + HandlerCaller mCaller; + InputMethodSession mInputMethodSession; + InputChannel mChannel; + ImeInputEventReceiver mReceiver; + + public IInputMethodSessionWrapper(Context context, + InputMethodSession inputMethodSession, InputChannel channel) { + mCaller = new HandlerCaller(context, null, + this, true /*asyncHandler*/); + mInputMethodSession = inputMethodSession; + mChannel = channel; + if (channel != null) { + mReceiver = new ImeInputEventReceiver(channel, context.getMainLooper()); + } + } + + public InputMethodSession getInternalInputMethodSession() { + return mInputMethodSession; + } + + @Override + public void executeMessage(Message msg) { + if (mInputMethodSession == null) { + // The session has been finished. Args needs to be recycled + // for cases below. + switch (msg.what) { + case DO_UPDATE_SELECTION: + case DO_APP_PRIVATE_COMMAND: { + SomeArgs args = (SomeArgs)msg.obj; + args.recycle(); + } + } + return; + } + + switch (msg.what) { + case DO_FINISH_INPUT: + mInputMethodSession.finishInput(); + return; + case DO_DISPLAY_COMPLETIONS: + mInputMethodSession.displayCompletions((CompletionInfo[])msg.obj); + return; + case DO_UPDATE_EXTRACTED_TEXT: + mInputMethodSession.updateExtractedText(msg.arg1, + (ExtractedText)msg.obj); + return; + case DO_UPDATE_SELECTION: { + SomeArgs args = (SomeArgs)msg.obj; + mInputMethodSession.updateSelection(args.argi1, args.argi2, + args.argi3, args.argi4, args.argi5, args.argi6); + args.recycle(); + return; + } + case DO_UPDATE_CURSOR: { + mInputMethodSession.updateCursor((Rect)msg.obj); + return; + } + case DO_UPDATE_CURSOR_ANCHOR_INFO: { + mInputMethodSession.updateCursorAnchorInfo((CursorAnchorInfo)msg.obj); + return; + } + case DO_APP_PRIVATE_COMMAND: { + SomeArgs args = (SomeArgs)msg.obj; + mInputMethodSession.appPrivateCommand((String)args.arg1, + (Bundle)args.arg2); + args.recycle(); + return; + } + case DO_TOGGLE_SOFT_INPUT: { + mInputMethodSession.toggleSoftInput(msg.arg1, msg.arg2); + return; + } + case DO_FINISH_SESSION: { + doFinishSession(); + return; + } + case DO_VIEW_CLICKED: { + mInputMethodSession.viewClicked(msg.arg1 == 1); + return; + } + } + Log.w(TAG, "Unhandled message code: " + msg.what); + } + + private void doFinishSession() { + mInputMethodSession = null; + if (mReceiver != null) { + mReceiver.dispose(); + mReceiver = null; + } + if (mChannel != null) { + mChannel.dispose(); + mChannel = null; + } + } + + @Override + public void finishInput() { + mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_FINISH_INPUT)); + } + + @Override + public void displayCompletions(CompletionInfo[] completions) { + mCaller.executeOrSendMessage(mCaller.obtainMessageO( + DO_DISPLAY_COMPLETIONS, completions)); + } + + @Override + public void updateExtractedText(int token, ExtractedText text) { + mCaller.executeOrSendMessage(mCaller.obtainMessageIO( + DO_UPDATE_EXTRACTED_TEXT, token, text)); + } + + @Override + public void updateSelection(int oldSelStart, int oldSelEnd, + int newSelStart, int newSelEnd, int candidatesStart, int candidatesEnd) { + mCaller.executeOrSendMessage(mCaller.obtainMessageIIIIII(DO_UPDATE_SELECTION, + oldSelStart, oldSelEnd, newSelStart, newSelEnd, + candidatesStart, candidatesEnd)); + } + + @Override + public void viewClicked(boolean focusChanged) { + mCaller.executeOrSendMessage( + mCaller.obtainMessageI(DO_VIEW_CLICKED, focusChanged ? 1 : 0)); + } + + @Override + public void updateCursor(Rect newCursor) { + mCaller.executeOrSendMessage( + mCaller.obtainMessageO(DO_UPDATE_CURSOR, newCursor)); + } + + @Override + public void updateCursorAnchorInfo(CursorAnchorInfo cursorAnchorInfo) { + mCaller.executeOrSendMessage( + mCaller.obtainMessageO(DO_UPDATE_CURSOR_ANCHOR_INFO, cursorAnchorInfo)); + } + + @Override + public void appPrivateCommand(String action, Bundle data) { + mCaller.executeOrSendMessage( + mCaller.obtainMessageOO(DO_APP_PRIVATE_COMMAND, action, data)); + } + + @Override + public void toggleSoftInput(int showFlags, int hideFlags) { + mCaller.executeOrSendMessage( + mCaller.obtainMessageII(DO_TOGGLE_SOFT_INPUT, showFlags, hideFlags)); + } + + @Override + public void finishSession() { + mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_FINISH_SESSION)); + } + + private final class ImeInputEventReceiver extends InputEventReceiver + implements InputMethodSession.EventCallback { + private final SparseArray<InputEvent> mPendingEvents = new SparseArray<InputEvent>(); + + public ImeInputEventReceiver(InputChannel inputChannel, Looper looper) { + super(inputChannel, looper); + } + + @Override + public void onInputEvent(InputEvent event, int displayId) { + if (mInputMethodSession == null) { + // The session has been finished. + finishInputEvent(event, false); + return; + } + + final int seq = event.getSequenceNumber(); + mPendingEvents.put(seq, event); + if (event instanceof KeyEvent) { + KeyEvent keyEvent = (KeyEvent)event; + mInputMethodSession.dispatchKeyEvent(seq, keyEvent, this); + } else { + MotionEvent motionEvent = (MotionEvent)event; + if (motionEvent.isFromSource(InputDevice.SOURCE_CLASS_TRACKBALL)) { + mInputMethodSession.dispatchTrackballEvent(seq, motionEvent, this); + } else { + mInputMethodSession.dispatchGenericMotionEvent(seq, motionEvent, this); + } + } + } + + @Override + public void finishedEvent(int seq, boolean handled) { + int index = mPendingEvents.indexOfKey(seq); + if (index >= 0) { + InputEvent event = mPendingEvents.valueAt(index); + mPendingEvents.removeAt(index); + finishInputEvent(event, handled); + } + } + } +} diff --git a/android/inputmethodservice/IInputMethodWrapper.java b/android/inputmethodservice/IInputMethodWrapper.java new file mode 100644 index 00000000..765aff96 --- /dev/null +++ b/android/inputmethodservice/IInputMethodWrapper.java @@ -0,0 +1,317 @@ +/* + * Copyright (C) 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.inputmethodservice; + +import com.android.internal.os.HandlerCaller; +import com.android.internal.os.SomeArgs; +import com.android.internal.view.IInputContext; +import com.android.internal.view.IInputMethod; +import com.android.internal.view.IInputMethodSession; +import com.android.internal.view.IInputSessionCallback; +import com.android.internal.view.InputConnectionWrapper; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Binder; +import android.os.IBinder; +import android.os.Message; +import android.os.RemoteException; +import android.os.ResultReceiver; +import android.util.Log; +import android.view.InputChannel; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputBinding; +import android.view.inputmethod.InputConnection; +import android.view.inputmethod.InputConnectionInspector; +import android.view.inputmethod.InputMethod; +import android.view.inputmethod.InputMethodSession; +import android.view.inputmethod.InputMethodSubtype; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.lang.ref.WeakReference; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * Implements the internal IInputMethod interface to convert incoming calls + * on to it back to calls on the public InputMethod interface, scheduling + * them on the main thread of the process. + */ +class IInputMethodWrapper extends IInputMethod.Stub + implements HandlerCaller.Callback { + private static final String TAG = "InputMethodWrapper"; + + private static final int DO_DUMP = 1; + private static final int DO_ATTACH_TOKEN = 10; + private static final int DO_SET_INPUT_CONTEXT = 20; + private static final int DO_UNSET_INPUT_CONTEXT = 30; + private static final int DO_START_INPUT = 32; + private static final int DO_CREATE_SESSION = 40; + private static final int DO_SET_SESSION_ENABLED = 45; + private static final int DO_REVOKE_SESSION = 50; + private static final int DO_SHOW_SOFT_INPUT = 60; + private static final int DO_HIDE_SOFT_INPUT = 70; + private static final int DO_CHANGE_INPUTMETHOD_SUBTYPE = 80; + + final WeakReference<AbstractInputMethodService> mTarget; + final Context mContext; + final HandlerCaller mCaller; + final WeakReference<InputMethod> mInputMethod; + final int mTargetSdkVersion; + + static class Notifier { + boolean notified; + } + + // NOTE: we should have a cache of these. + static final class InputMethodSessionCallbackWrapper implements InputMethod.SessionCallback { + final Context mContext; + final InputChannel mChannel; + final IInputSessionCallback mCb; + + InputMethodSessionCallbackWrapper(Context context, InputChannel channel, + IInputSessionCallback cb) { + mContext = context; + mChannel = channel; + mCb = cb; + } + + @Override + public void sessionCreated(InputMethodSession session) { + try { + if (session != null) { + IInputMethodSessionWrapper wrap = + new IInputMethodSessionWrapper(mContext, session, mChannel); + mCb.sessionCreated(wrap); + } else { + if (mChannel != null) { + mChannel.dispose(); + } + mCb.sessionCreated(null); + } + } catch (RemoteException e) { + } + } + } + + public IInputMethodWrapper(AbstractInputMethodService context, + InputMethod inputMethod) { + mTarget = new WeakReference<AbstractInputMethodService>(context); + mContext = context.getApplicationContext(); + mCaller = new HandlerCaller(mContext, null, this, true /*asyncHandler*/); + mInputMethod = new WeakReference<InputMethod>(inputMethod); + mTargetSdkVersion = context.getApplicationInfo().targetSdkVersion; + } + + public InputMethod getInternalInputMethod() { + return mInputMethod.get(); + } + + @Override + public void executeMessage(Message msg) { + InputMethod inputMethod = mInputMethod.get(); + // Need a valid reference to the inputMethod for everything except a dump. + if (inputMethod == null && msg.what != DO_DUMP) { + Log.w(TAG, "Input method reference was null, ignoring message: " + msg.what); + return; + } + + switch (msg.what) { + case DO_DUMP: { + AbstractInputMethodService target = mTarget.get(); + if (target == null) { + return; + } + SomeArgs args = (SomeArgs)msg.obj; + try { + target.dump((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(); + } + args.recycle(); + return; + } + + case DO_ATTACH_TOKEN: { + inputMethod.attachToken((IBinder)msg.obj); + return; + } + case DO_SET_INPUT_CONTEXT: { + inputMethod.bindInput((InputBinding)msg.obj); + return; + } + case DO_UNSET_INPUT_CONTEXT: + inputMethod.unbindInput(); + return; + case DO_START_INPUT: { + final SomeArgs args = (SomeArgs) msg.obj; + final int missingMethods = msg.arg1; + final boolean restarting = msg.arg2 != 0; + final IBinder startInputToken = (IBinder) args.arg1; + final IInputContext inputContext = (IInputContext) args.arg2; + final EditorInfo info = (EditorInfo) args.arg3; + final InputConnection ic = inputContext != null + ? new InputConnectionWrapper(mTarget, inputContext, missingMethods) : null; + info.makeCompatible(mTargetSdkVersion); + inputMethod.dispatchStartInputWithToken(ic, info, restarting /* restarting */, + startInputToken); + args.recycle(); + return; + } + case DO_CREATE_SESSION: { + SomeArgs args = (SomeArgs)msg.obj; + inputMethod.createSession(new InputMethodSessionCallbackWrapper( + mContext, (InputChannel)args.arg1, + (IInputSessionCallback)args.arg2)); + args.recycle(); + return; + } + case DO_SET_SESSION_ENABLED: + inputMethod.setSessionEnabled((InputMethodSession)msg.obj, + msg.arg1 != 0); + return; + case DO_REVOKE_SESSION: + inputMethod.revokeSession((InputMethodSession)msg.obj); + return; + case DO_SHOW_SOFT_INPUT: + inputMethod.showSoftInput(msg.arg1, (ResultReceiver)msg.obj); + return; + case DO_HIDE_SOFT_INPUT: + inputMethod.hideSoftInput(msg.arg1, (ResultReceiver)msg.obj); + return; + case DO_CHANGE_INPUTMETHOD_SUBTYPE: + inputMethod.changeInputMethodSubtype((InputMethodSubtype)msg.obj); + return; + } + Log.w(TAG, "Unhandled message code: " + msg.what); + } + + @Override + protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) { + AbstractInputMethodService target = mTarget.get(); + if (target == null) { + return; + } + if (target.checkCallingOrSelfPermission(android.Manifest.permission.DUMP) + != PackageManager.PERMISSION_GRANTED) { + + fout.println("Permission Denial: can't dump InputMethodManager from from pid=" + + Binder.getCallingPid() + + ", uid=" + Binder.getCallingUid()); + return; + } + + CountDownLatch latch = new CountDownLatch(1); + mCaller.executeOrSendMessage(mCaller.obtainMessageOOOO(DO_DUMP, + fd, fout, args, latch)); + try { + if (!latch.await(5, TimeUnit.SECONDS)) { + fout.println("Timeout waiting for dump"); + } + } catch (InterruptedException e) { + fout.println("Interrupted waiting for dump"); + } + } + + @Override + public void attachToken(IBinder token) { + mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_ATTACH_TOKEN, token)); + } + + @Override + public void bindInput(InputBinding binding) { + // This IInputContext is guaranteed to implement all the methods. + final int missingMethodFlags = 0; + InputConnection ic = new InputConnectionWrapper(mTarget, + IInputContext.Stub.asInterface(binding.getConnectionToken()), missingMethodFlags); + InputBinding nu = new InputBinding(ic, binding); + mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_SET_INPUT_CONTEXT, nu)); + } + + @Override + public void unbindInput() { + mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_UNSET_INPUT_CONTEXT)); + } + + @Override + public void startInput(IBinder startInputToken, IInputContext inputContext, + @InputConnectionInspector.MissingMethodFlags final int missingMethods, + EditorInfo attribute, boolean restarting) { + mCaller.executeOrSendMessage(mCaller.obtainMessageIIOOO(DO_START_INPUT, + missingMethods, restarting ? 1 : 0, startInputToken, inputContext, attribute)); + } + + @Override + public void createSession(InputChannel channel, IInputSessionCallback callback) { + mCaller.executeOrSendMessage(mCaller.obtainMessageOO(DO_CREATE_SESSION, + channel, callback)); + } + + @Override + public void setSessionEnabled(IInputMethodSession session, boolean enabled) { + try { + InputMethodSession ls = ((IInputMethodSessionWrapper) + session).getInternalInputMethodSession(); + if (ls == null) { + Log.w(TAG, "Session is already finished: " + session); + return; + } + mCaller.executeOrSendMessage(mCaller.obtainMessageIO( + DO_SET_SESSION_ENABLED, enabled ? 1 : 0, ls)); + } catch (ClassCastException e) { + Log.w(TAG, "Incoming session not of correct type: " + session, e); + } + } + + @Override + public void revokeSession(IInputMethodSession session) { + try { + InputMethodSession ls = ((IInputMethodSessionWrapper) + session).getInternalInputMethodSession(); + if (ls == null) { + Log.w(TAG, "Session is already finished: " + session); + return; + } + mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_REVOKE_SESSION, ls)); + } catch (ClassCastException e) { + Log.w(TAG, "Incoming session not of correct type: " + session, e); + } + } + + @Override + public void showSoftInput(int flags, ResultReceiver resultReceiver) { + mCaller.executeOrSendMessage(mCaller.obtainMessageIO(DO_SHOW_SOFT_INPUT, + flags, resultReceiver)); + } + + @Override + public void hideSoftInput(int flags, ResultReceiver resultReceiver) { + mCaller.executeOrSendMessage(mCaller.obtainMessageIO(DO_HIDE_SOFT_INPUT, + flags, resultReceiver)); + } + + @Override + public void changeInputMethodSubtype(InputMethodSubtype subtype) { + mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_CHANGE_INPUTMETHOD_SUBTYPE, + subtype)); + } +} diff --git a/android/inputmethodservice/InputMethodService.java b/android/inputmethodservice/InputMethodService.java new file mode 100644 index 00000000..7a20943e --- /dev/null +++ b/android/inputmethodservice/InputMethodService.java @@ -0,0 +1,2716 @@ +/* + * 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.inputmethodservice; + +import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; +import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; + +import android.annotation.CallSuper; +import android.annotation.DrawableRes; +import android.annotation.IntDef; +import android.annotation.MainThread; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.ActivityManager; +import android.app.Dialog; +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.database.ContentObserver; +import android.graphics.Rect; +import android.graphics.Region; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.ResultReceiver; +import android.os.SystemClock; +import android.provider.Settings; +import android.text.InputType; +import android.text.Layout; +import android.text.Spannable; +import android.text.method.MovementMethod; +import android.util.Log; +import android.util.PrintWriterPrinter; +import android.util.Printer; +import android.view.Gravity; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.Window; +import android.view.WindowManager; +import android.view.WindowManager.BadTokenException; +import android.view.animation.AnimationUtils; +import android.view.inputmethod.CompletionInfo; +import android.view.inputmethod.CursorAnchorInfo; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.ExtractedText; +import android.view.inputmethod.ExtractedTextRequest; +import android.view.inputmethod.InputBinding; +import android.view.inputmethod.InputConnection; +import android.view.inputmethod.InputContentInfo; +import android.view.inputmethod.InputMethod; +import android.view.inputmethod.InputMethodManager; +import android.view.inputmethod.InputMethodSubtype; +import android.widget.FrameLayout; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.TextView; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * InputMethodService provides a standard implementation of an InputMethod, + * which final implementations can derive from and customize. See the + * base class {@link AbstractInputMethodService} and the {@link InputMethod} + * interface for more information on the basics of writing input methods. + * + * <p>In addition to the normal Service lifecycle methods, this class + * introduces some new specific callbacks that most subclasses will want + * to make use of:</p> + * <ul> + * <li> {@link #onInitializeInterface()} for user-interface initialization, + * in particular to deal with configuration changes while the service is + * running. + * <li> {@link #onBindInput} to find out about switching to a new client. + * <li> {@link #onStartInput} to deal with an input session starting with + * the client. + * <li> {@link #onCreateInputView()}, {@link #onCreateCandidatesView()}, + * and {@link #onCreateExtractTextView()} for non-demand generation of the UI. + * <li> {@link #onStartInputView(EditorInfo, boolean)} to deal with input + * starting within the input area of the IME. + * </ul> + * + * <p>An input method has significant discretion in how it goes about its + * work: the {@link android.inputmethodservice.InputMethodService} provides + * a basic framework for standard UI elements (input view, candidates view, + * and running in fullscreen mode), but it is up to a particular implementor + * to decide how to use them. For example, one input method could implement + * an input area with a keyboard, another could allow the user to draw text, + * while a third could have no input area (and thus not be visible to the + * user) but instead listen to audio and perform text to speech conversion.</p> + * + * <p>In the implementation provided here, all of these elements are placed + * together in a single window managed by the InputMethodService. It will + * execute callbacks as it needs information about them, and provides APIs for + * programmatic control over them. They layout of these elements is explicitly + * defined:</p> + * + * <ul> + * <li>The soft input view, if available, is placed at the bottom of the + * screen. + * <li>The candidates view, if currently shown, is placed above the soft + * input view. + * <li>If not running fullscreen, the application is moved or resized to be + * above these views; if running fullscreen, the window will completely cover + * the application and its top part will contain the extract text of what is + * currently being edited by the application. + * </ul> + * + * + * <a name="SoftInputView"></a> + * <h3>Soft Input View</h3> + * + * <p>Central to most input methods is the soft input view. This is where most + * user interaction occurs: pressing on soft keys, drawing characters, or + * however else your input method wants to generate text. Most implementations + * will simply have their own view doing all of this work, and return a new + * instance of it when {@link #onCreateInputView()} is called. At that point, + * as long as the input view is visible, you will see user interaction in + * that view and can call back on the InputMethodService to interact with the + * application as appropriate.</p> + * + * <p>There are some situations where you want to decide whether or not your + * soft input view should be shown to the user. This is done by implementing + * the {@link #onEvaluateInputViewShown()} to return true or false based on + * whether it should be shown in the current environment. If any of your + * state has changed that may impact this, call + * {@link #updateInputViewShown()} to have it re-evaluated. The default + * implementation always shows the input view unless there is a hard + * keyboard available, which is the appropriate behavior for most input + * methods.</p> + * + * + * <a name="CandidatesView"></a> + * <h3>Candidates View</h3> + * + * <p>Often while the user is generating raw text, an input method wants to + * provide them with a list of possible interpretations of that text that can + * be selected for use. This is accomplished with the candidates view, and + * like the soft input view you implement {@link #onCreateCandidatesView()} + * to instantiate your own view implementing your candidates UI.</p> + * + * <p>Management of the candidates view is a little different than the input + * view, because the candidates view tends to be more transient, being shown + * only when there are possible candidates for the current text being entered + * by the user. To control whether the candidates view is shown, you use + * {@link #setCandidatesViewShown(boolean)}. Note that because the candidate + * view tends to be shown and hidden a lot, it does not impact the application + * UI in the same way as the soft input view: it will never cause application + * windows to resize, only cause them to be panned if needed for the user to + * see the current focus.</p> + * + * + * <a name="FullscreenMode"></a> + * <h3>Fullscreen Mode</h3> + * + * <p>Sometimes your input method UI is too large to integrate with the + * application UI, so you just want to take over the screen. This is + * accomplished by switching to full-screen mode, causing the input method + * window to fill the entire screen and add its own "extracted text" editor + * showing the user the text that is being typed. Unlike the other UI elements, + * there is a standard implementation for the extract editor that you should + * not need to change. The editor is placed at the top of the IME, above the + * input and candidates views.</p> + * + * <p>Similar to the input view, you control whether the IME is running in + * fullscreen mode by implementing {@link #onEvaluateFullscreenMode()} + * to return true or false based on + * whether it should be fullscreen in the current environment. If any of your + * state has changed that may impact this, call + * {@link #updateFullscreenMode()} to have it re-evaluated. The default + * implementation selects fullscreen mode when the screen is in a landscape + * orientation, which is appropriate behavior for most input methods that have + * a significant input area.</p> + * + * <p>When in fullscreen mode, you have some special requirements because the + * user can not see the application UI. In particular, you should implement + * {@link #onDisplayCompletions(CompletionInfo[])} to show completions + * generated by your application, typically in your candidates view like you + * would normally show candidates. + * + * + * <a name="GeneratingText"></a> + * <h3>Generating Text</h3> + * + * <p>The key part of an IME is of course generating text for the application. + * This is done through calls to the + * {@link android.view.inputmethod.InputConnection} interface to the + * application, which can be retrieved from {@link #getCurrentInputConnection()}. + * This interface allows you to generate raw key events or, if the target + * supports it, directly edit in strings of candidates and committed text.</p> + * + * <p>Information about what the target is expected and supports can be found + * through the {@link android.view.inputmethod.EditorInfo} class, which is + * retrieved with {@link #getCurrentInputEditorInfo()} method. The most + * important part of this is {@link android.view.inputmethod.EditorInfo#inputType + * EditorInfo.inputType}; in particular, if this is + * {@link android.view.inputmethod.EditorInfo#TYPE_NULL EditorInfo.TYPE_NULL}, + * then the target does not support complex edits and you need to only deliver + * raw key events to it. An input method will also want to look at other + * values here, to for example detect password mode, auto complete text views, + * phone number entry, etc.</p> + * + * <p>When the user switches between input targets, you will receive calls to + * {@link #onFinishInput()} and {@link #onStartInput(EditorInfo, boolean)}. + * You can use these to reset and initialize your input state for the current + * target. For example, you will often want to clear any input state, and + * update a soft keyboard to be appropriate for the new inputType.</p> + * + * @attr ref android.R.styleable#InputMethodService_imeFullscreenBackground + * @attr ref android.R.styleable#InputMethodService_imeExtractEnterAnimation + * @attr ref android.R.styleable#InputMethodService_imeExtractExitAnimation + */ +public class InputMethodService extends AbstractInputMethodService { + static final String TAG = "InputMethodService"; + static final boolean DEBUG = false; + + /** + * The back button will close the input window. + */ + public static final int BACK_DISPOSITION_DEFAULT = 0; // based on window + + /** + * This input method will not consume the back key. + */ + public static final int BACK_DISPOSITION_WILL_NOT_DISMISS = 1; // back + + /** + * This input method will consume the back key. + */ + public static final int BACK_DISPOSITION_WILL_DISMISS = 2; // down + + /** + * @hide + * The IME is active. It may or may not be visible. + */ + public static final int IME_ACTIVE = 0x1; + + /** + * @hide + * The IME is visible. + */ + public static final int IME_VISIBLE = 0x2; + + InputMethodManager mImm; + + int mTheme = 0; + + LayoutInflater mInflater; + TypedArray mThemeAttrs; + View mRootView; + SoftInputWindow mWindow; + boolean mInitialized; + boolean mWindowCreated; + boolean mWindowAdded; + boolean mWindowVisible; + boolean mWindowWasVisible; + boolean mInShowWindow; + ViewGroup mFullscreenArea; + FrameLayout mExtractFrame; + FrameLayout mCandidatesFrame; + FrameLayout mInputFrame; + + IBinder mToken; + + InputBinding mInputBinding; + InputConnection mInputConnection; + boolean mInputStarted; + boolean mInputViewStarted; + boolean mCandidatesViewStarted; + InputConnection mStartedInputConnection; + EditorInfo mInputEditorInfo; + + /** + * A token to keep tracking the last IPC that triggered + * {@link #doStartInput(InputConnection, EditorInfo, boolean)}. If + * {@link #doStartInput(InputConnection, EditorInfo, boolean)} was not caused by IPCs from + * {@link com.android.server.InputMethodManagerService}, this needs to remain unchanged. + * + * <p>Some IPCs to {@link com.android.server.InputMethodManagerService} require this token to + * disentangle event flows for various purposes such as better window animation and providing + * fine-grained debugging information.</p> + */ + @Nullable + private IBinder mStartInputToken; + + int mShowInputFlags; + boolean mShowInputRequested; + boolean mLastShowInputRequested; + int mCandidatesVisibility; + CompletionInfo[] mCurCompletions; + + boolean mFullscreenApplied; + boolean mIsFullscreen; + View mExtractView; + boolean mExtractViewHidden; + ExtractEditText mExtractEditText; + ViewGroup mExtractAccessories; + View mExtractAction; + ExtractedText mExtractedText; + int mExtractedToken; + + View mInputView; + boolean mIsInputViewShown; + + int mStatusIcon; + int mBackDisposition; + + /** + * {@code true} when the previous IME had non-empty inset at the bottom of the screen and we + * have not shown our own window yet. In this situation, the previous inset continues to be + * shown as an empty region until it is explicitly updated. Basically we can trigger the update + * by calling 1) {@code mWindow.show()} or 2) {@link #clearInsetOfPreviousIme()}. + */ + boolean mShouldClearInsetOfPreviousIme; + + final Insets mTmpInsets = new Insets(); + final int[] mTmpLocation = new int[2]; + + final ViewTreeObserver.OnComputeInternalInsetsListener mInsetsComputer = + new ViewTreeObserver.OnComputeInternalInsetsListener() { + public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo info) { + if (isExtractViewShown()) { + // In true fullscreen mode, we just say the window isn't covering + // any content so we don't impact whatever is behind. + View decor = getWindow().getWindow().getDecorView(); + info.contentInsets.top = info.visibleInsets.top + = decor.getHeight(); + info.touchableRegion.setEmpty(); + info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME); + } else { + onComputeInsets(mTmpInsets); + info.contentInsets.top = mTmpInsets.contentTopInsets; + info.visibleInsets.top = mTmpInsets.visibleTopInsets; + info.touchableRegion.set(mTmpInsets.touchableRegion); + info.setTouchableInsets(mTmpInsets.touchableInsets); + } + } + }; + + final View.OnClickListener mActionClickListener = new View.OnClickListener() { + public void onClick(View v) { + final EditorInfo ei = getCurrentInputEditorInfo(); + final InputConnection ic = getCurrentInputConnection(); + if (ei != null && ic != null) { + if (ei.actionId != 0) { + ic.performEditorAction(ei.actionId); + } else if ((ei.imeOptions&EditorInfo.IME_MASK_ACTION) + != EditorInfo.IME_ACTION_NONE) { + ic.performEditorAction(ei.imeOptions&EditorInfo.IME_MASK_ACTION); + } + } + } + }; + + /** + * Concrete implementation of + * {@link AbstractInputMethodService.AbstractInputMethodImpl} that provides + * all of the standard behavior for an input method. + */ + public class InputMethodImpl extends AbstractInputMethodImpl { + /** + * Take care of attaching the given window token provided by the system. + */ + public void attachToken(IBinder token) { + if (mToken == null) { + mToken = token; + mWindow.setToken(token); + } + } + + /** + * Handle a new input binding, calling + * {@link InputMethodService#onBindInput InputMethodService.onBindInput()} + * when done. + */ + public void bindInput(InputBinding binding) { + mInputBinding = binding; + mInputConnection = binding.getConnection(); + if (DEBUG) Log.v(TAG, "bindInput(): binding=" + binding + + " ic=" + mInputConnection); + if (mImm != null && mToken != null) { + mImm.reportFullscreenMode(mToken, mIsFullscreen); + } + initialize(); + onBindInput(); + } + + /** + * Clear the current input binding. + */ + public void unbindInput() { + if (DEBUG) Log.v(TAG, "unbindInput(): binding=" + mInputBinding + + " ic=" + mInputConnection); + onUnbindInput(); + mInputBinding = null; + mInputConnection = null; + } + + public void startInput(InputConnection ic, EditorInfo attribute) { + if (DEBUG) Log.v(TAG, "startInput(): editor=" + attribute); + doStartInput(ic, attribute, false); + } + + public void restartInput(InputConnection ic, EditorInfo attribute) { + if (DEBUG) Log.v(TAG, "restartInput(): editor=" + attribute); + doStartInput(ic, attribute, true); + } + + /** + * {@inheritDoc} + * @hide + */ + @Override + public void dispatchStartInputWithToken(@Nullable InputConnection inputConnection, + @NonNull EditorInfo editorInfo, boolean restarting, + @NonNull IBinder startInputToken) { + mStartInputToken = startInputToken; + + // This needs to be dispatched to interface methods rather than doStartInput(). + // Otherwise IME developers who have overridden those interface methods will lose + // notifications. + super.dispatchStartInputWithToken(inputConnection, editorInfo, restarting, + startInputToken); + } + + /** + * Handle a request by the system to hide the soft input area. + */ + public void hideSoftInput(int flags, ResultReceiver resultReceiver) { + if (DEBUG) Log.v(TAG, "hideSoftInput()"); + boolean wasVis = isInputViewShown(); + mShowInputFlags = 0; + mShowInputRequested = false; + doHideWindow(); + clearInsetOfPreviousIme(); + if (resultReceiver != null) { + resultReceiver.send(wasVis != isInputViewShown() + ? InputMethodManager.RESULT_HIDDEN + : (wasVis ? InputMethodManager.RESULT_UNCHANGED_SHOWN + : InputMethodManager.RESULT_UNCHANGED_HIDDEN), null); + } + } + + /** + * Handle a request by the system to show the soft input area. + */ + public void showSoftInput(int flags, ResultReceiver resultReceiver) { + if (DEBUG) Log.v(TAG, "showSoftInput()"); + boolean wasVis = isInputViewShown(); + if (dispatchOnShowInputRequested(flags, false)) { + try { + showWindow(true); + } catch (BadTokenException e) { + // We have ignored BadTokenException here since Jelly Bean MR-2 (API Level 18). + // We could ignore BadTokenException in InputMethodService#showWindow() instead, + // but it may break assumptions for those who override #showWindow() that we can + // detect errors in #showWindow() by checking BadTokenException. + // TODO: Investigate its feasibility. Update JavaDoc of #showWindow() of + // whether it's OK to override #showWindow() or not. + } + } + clearInsetOfPreviousIme(); + // If user uses hard keyboard, IME button should always be shown. + boolean showing = isInputViewShown(); + mImm.setImeWindowStatus(mToken, mStartInputToken, + IME_ACTIVE | (showing ? IME_VISIBLE : 0), mBackDisposition); + if (resultReceiver != null) { + resultReceiver.send(wasVis != isInputViewShown() + ? InputMethodManager.RESULT_SHOWN + : (wasVis ? InputMethodManager.RESULT_UNCHANGED_SHOWN + : InputMethodManager.RESULT_UNCHANGED_HIDDEN), null); + } + } + + public void changeInputMethodSubtype(InputMethodSubtype subtype) { + onCurrentInputMethodSubtypeChanged(subtype); + } + } + + /** + * Concrete implementation of + * {@link AbstractInputMethodService.AbstractInputMethodSessionImpl} that provides + * all of the standard behavior for an input method session. + */ + public class InputMethodSessionImpl extends AbstractInputMethodSessionImpl { + public void finishInput() { + if (!isEnabled()) { + return; + } + if (DEBUG) Log.v(TAG, "finishInput() in " + this); + doFinishInput(); + } + + /** + * Call {@link InputMethodService#onDisplayCompletions + * InputMethodService.onDisplayCompletions()}. + */ + public void displayCompletions(CompletionInfo[] completions) { + if (!isEnabled()) { + return; + } + mCurCompletions = completions; + onDisplayCompletions(completions); + } + + /** + * Call {@link InputMethodService#onUpdateExtractedText + * InputMethodService.onUpdateExtractedText()}. + */ + public void updateExtractedText(int token, ExtractedText text) { + if (!isEnabled()) { + return; + } + onUpdateExtractedText(token, text); + } + + /** + * Call {@link InputMethodService#onUpdateSelection + * InputMethodService.onUpdateSelection()}. + */ + public void updateSelection(int oldSelStart, int oldSelEnd, + int newSelStart, int newSelEnd, + int candidatesStart, int candidatesEnd) { + if (!isEnabled()) { + return; + } + InputMethodService.this.onUpdateSelection(oldSelStart, oldSelEnd, + newSelStart, newSelEnd, candidatesStart, candidatesEnd); + } + + @Override + public void viewClicked(boolean focusChanged) { + if (!isEnabled()) { + return; + } + InputMethodService.this.onViewClicked(focusChanged); + } + + /** + * Call {@link InputMethodService#onUpdateCursor + * InputMethodService.onUpdateCursor()}. + */ + public void updateCursor(Rect newCursor) { + if (!isEnabled()) { + return; + } + InputMethodService.this.onUpdateCursor(newCursor); + } + + /** + * Call {@link InputMethodService#onAppPrivateCommand + * InputMethodService.onAppPrivateCommand()}. + */ + public void appPrivateCommand(String action, Bundle data) { + if (!isEnabled()) { + return; + } + InputMethodService.this.onAppPrivateCommand(action, data); + } + + /** + * + */ + public void toggleSoftInput(int showFlags, int hideFlags) { + InputMethodService.this.onToggleSoftInput(showFlags, hideFlags); + } + + /** + * Call {@link InputMethodService#onUpdateCursorAnchorInfo + * InputMethodService.onUpdateCursorAnchorInfo()}. + */ + public void updateCursorAnchorInfo(CursorAnchorInfo info) { + if (!isEnabled()) { + return; + } + InputMethodService.this.onUpdateCursorAnchorInfo(info); + } + } + + /** + * Information about where interesting parts of the input method UI appear. + */ + public static final class Insets { + /** + * This is the top part of the UI that is the main content. It is + * used to determine the basic space needed, to resize/pan the + * application behind. It is assumed that this inset does not + * change very much, since any change will cause a full resize/pan + * of the application behind. This value is relative to the top edge + * of the input method window. + */ + public int contentTopInsets; + + /** + * This is the top part of the UI that is visibly covering the + * application behind it. This provides finer-grained control over + * visibility, allowing you to change it relatively frequently (such + * as hiding or showing candidates) without disrupting the underlying + * UI too much. For example, this will never resize the application + * UI, will only pan if needed to make the current focus visible, and + * will not aggressively move the pan position when this changes unless + * needed to make the focus visible. This value is relative to the top edge + * of the input method window. + */ + public int visibleTopInsets; + + /** + * This is the region of the UI that is touchable. It is used when + * {@link #touchableInsets} is set to {@link #TOUCHABLE_INSETS_REGION}. + * The region should be specified relative to the origin of the window frame. + */ + public final Region touchableRegion = new Region(); + + /** + * Option for {@link #touchableInsets}: the entire window frame + * can be touched. + */ + public static final int TOUCHABLE_INSETS_FRAME + = ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME; + + /** + * Option for {@link #touchableInsets}: the area inside of + * the content insets can be touched. + */ + public static final int TOUCHABLE_INSETS_CONTENT + = ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_CONTENT; + + /** + * Option for {@link #touchableInsets}: the area inside of + * the visible insets can be touched. + */ + public static final int TOUCHABLE_INSETS_VISIBLE + = ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_VISIBLE; + + /** + * Option for {@link #touchableInsets}: the region specified by + * {@link #touchableRegion} can be touched. + */ + public static final int TOUCHABLE_INSETS_REGION + = ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION; + + /** + * Determine which area of the window is touchable by the user. May + * be one of: {@link #TOUCHABLE_INSETS_FRAME}, + * {@link #TOUCHABLE_INSETS_CONTENT}, {@link #TOUCHABLE_INSETS_VISIBLE}, + * or {@link #TOUCHABLE_INSETS_REGION}. + */ + public int touchableInsets; + } + + /** + * A {@link ContentObserver} to monitor {@link Settings.Secure#SHOW_IME_WITH_HARD_KEYBOARD}. + * + * <p>Note that {@link Settings.Secure#SHOW_IME_WITH_HARD_KEYBOARD} is not a public API. + * Basically this functionality still needs to be considered as implementation details.</p> + */ + @MainThread + private static final class SettingsObserver extends ContentObserver { + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + ShowImeWithHardKeyboardType.UNKNOWN, + ShowImeWithHardKeyboardType.FALSE, + ShowImeWithHardKeyboardType.TRUE, + }) + private @interface ShowImeWithHardKeyboardType { + int UNKNOWN = 0; + int FALSE = 1; + int TRUE = 2; + } + @ShowImeWithHardKeyboardType + private int mShowImeWithHardKeyboard = ShowImeWithHardKeyboardType.UNKNOWN; + + private final InputMethodService mService; + + private SettingsObserver(InputMethodService service) { + super(new Handler(service.getMainLooper())); + mService = service; + } + + /** + * A factory method that internally enforces two-phase initialization to make sure that the + * object reference will not be escaped until the object is properly constructed. + * + * <p>NOTE: Currently {@link SettingsObserver} is accessed only from main thread. Hence + * this enforcement of two-phase initialization may be unnecessary at the moment.</p> + * + * @param service {@link InputMethodService} that needs to receive the callback. + * @return {@link SettingsObserver} that is already registered to + * {@link android.content.ContentResolver}. The caller must call + * {@link SettingsObserver#unregister()}. + */ + public static SettingsObserver createAndRegister(InputMethodService service) { + final SettingsObserver observer = new SettingsObserver(service); + // The observer is properly constructed. Let's start accepting the event. + service.getContentResolver().registerContentObserver( + Settings.Secure.getUriFor(Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD), + false, observer); + return observer; + } + + void unregister() { + mService.getContentResolver().unregisterContentObserver(this); + } + + private boolean shouldShowImeWithHardKeyboard() { + // Lazily initialize as needed. + if (mShowImeWithHardKeyboard == ShowImeWithHardKeyboardType.UNKNOWN) { + mShowImeWithHardKeyboard = Settings.Secure.getInt(mService.getContentResolver(), + Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD, 0) != 0 ? + ShowImeWithHardKeyboardType.TRUE : ShowImeWithHardKeyboardType.FALSE; + } + switch (mShowImeWithHardKeyboard) { + case ShowImeWithHardKeyboardType.TRUE: + return true; + case ShowImeWithHardKeyboardType.FALSE: + return false; + default: + Log.e(TAG, "Unexpected mShowImeWithHardKeyboard=" + mShowImeWithHardKeyboard); + return false; + } + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + final Uri showImeWithHardKeyboardUri = + Settings.Secure.getUriFor(Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD); + if (showImeWithHardKeyboardUri.equals(uri)) { + mShowImeWithHardKeyboard = Settings.Secure.getInt(mService.getContentResolver(), + Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD, 0) != 0 ? + ShowImeWithHardKeyboardType.TRUE : ShowImeWithHardKeyboardType.FALSE; + // In Android M and prior, state change of + // Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD has triggered + // #onConfigurationChanged(). For compatibility reasons, we reset the internal + // state as if configuration was changed. + mService.resetStateForNewConfiguration(); + } + } + + @Override + public String toString() { + return "SettingsObserver{mShowImeWithHardKeyboard=" + mShowImeWithHardKeyboard + "}"; + } + } + private SettingsObserver mSettingsObserver; + + /** + * You can call this to customize the theme used by your IME's window. + * This theme should typically be one that derives from + * {@link android.R.style#Theme_InputMethod}, which is the default theme + * you will get. This must be set before {@link #onCreate}, so you + * will typically call it in your constructor with the resource ID + * of your custom theme. + */ + @Override + public void setTheme(int theme) { + if (mWindow != null) { + throw new IllegalStateException("Must be called before onCreate()"); + } + mTheme = theme; + } + + /** + * You can call this to try to enable accelerated drawing for your IME. This must be set before + * {@link #onCreate()}, so you will typically call it in your constructor. It is not always + * possible to use hardware accelerated drawing in an IME (for example on low-end devices that + * do not have the resources to support this), so the call {@code true} if it succeeds otherwise + * {@code false} if you will need to draw in software. You must be able to handle either case. + * + * <p>In API 21 and later, system may automatically enable hardware accelerated drawing for your + * IME on capable devices even if this method is not explicitly called. Make sure that your IME + * is able to handle either case.</p> + * + * @return {@code true} if accelerated drawing is successfully enabled otherwise {@code false}. + * On API 21 and later devices the return value is basically just a hint and your IME + * does not need to change the behavior based on the it + * @deprecated Starting in API 21, hardware acceleration is always enabled on capable devices + */ + @Deprecated + public boolean enableHardwareAcceleration() { + if (mWindow != null) { + throw new IllegalStateException("Must be called before onCreate()"); + } + return ActivityManager.isHighEndGfx(); + } + + @Override public void onCreate() { + mTheme = Resources.selectSystemTheme(mTheme, + getApplicationInfo().targetSdkVersion, + android.R.style.Theme_InputMethod, + android.R.style.Theme_Holo_InputMethod, + android.R.style.Theme_DeviceDefault_InputMethod, + android.R.style.Theme_DeviceDefault_InputMethod); + super.setTheme(mTheme); + super.onCreate(); + mImm = (InputMethodManager)getSystemService(INPUT_METHOD_SERVICE); + mSettingsObserver = SettingsObserver.createAndRegister(this); + // If the previous IME has occupied non-empty inset in the screen, we need to decide whether + // we continue to use the same size of the inset or update it + mShouldClearInsetOfPreviousIme = (mImm.getInputMethodWindowVisibleHeight() > 0); + mInflater = (LayoutInflater)getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + mWindow = new SoftInputWindow(this, "InputMethod", mTheme, null, null, mDispatcherState, + WindowManager.LayoutParams.TYPE_INPUT_METHOD, Gravity.BOTTOM, false); + initViews(); + mWindow.getWindow().setLayout(MATCH_PARENT, WRAP_CONTENT); + } + + /** + * This is a hook that subclasses can use to perform initialization of + * their interface. It is called for you prior to any of your UI objects + * being created, both after the service is first created and after a + * configuration change happens. + */ + public void onInitializeInterface() { + // Intentionally empty + } + + void initialize() { + if (!mInitialized) { + mInitialized = true; + onInitializeInterface(); + } + } + + void initViews() { + mInitialized = false; + mWindowCreated = false; + mShowInputRequested = false; + mShowInputFlags = 0; + + mThemeAttrs = obtainStyledAttributes(android.R.styleable.InputMethodService); + mRootView = mInflater.inflate( + com.android.internal.R.layout.input_method, null); + mRootView.setSystemUiVisibility( + View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); + mWindow.setContentView(mRootView); + mRootView.getViewTreeObserver().removeOnComputeInternalInsetsListener(mInsetsComputer); + mRootView.getViewTreeObserver().addOnComputeInternalInsetsListener(mInsetsComputer); + if (Settings.Global.getInt(getContentResolver(), + Settings.Global.FANCY_IME_ANIMATIONS, 0) != 0) { + mWindow.getWindow().setWindowAnimations( + com.android.internal.R.style.Animation_InputMethodFancy); + } + mFullscreenArea = (ViewGroup)mRootView.findViewById(com.android.internal.R.id.fullscreenArea); + mExtractViewHidden = false; + mExtractFrame = (FrameLayout)mRootView.findViewById(android.R.id.extractArea); + mExtractView = null; + mExtractEditText = null; + mExtractAccessories = null; + mExtractAction = null; + mFullscreenApplied = false; + + mCandidatesFrame = (FrameLayout)mRootView.findViewById(android.R.id.candidatesArea); + mInputFrame = (FrameLayout)mRootView.findViewById(android.R.id.inputArea); + mInputView = null; + mIsInputViewShown = false; + + mExtractFrame.setVisibility(View.GONE); + mCandidatesVisibility = getCandidatesHiddenVisibility(); + mCandidatesFrame.setVisibility(mCandidatesVisibility); + mInputFrame.setVisibility(View.GONE); + } + + @Override public void onDestroy() { + super.onDestroy(); + mRootView.getViewTreeObserver().removeOnComputeInternalInsetsListener( + mInsetsComputer); + doFinishInput(); + if (mWindowAdded) { + // Disable exit animation for the current IME window + // to avoid the race condition between the exit and enter animations + // when the current IME is being switched to another one. + mWindow.getWindow().setWindowAnimations(0); + mWindow.dismiss(); + } + if (mSettingsObserver != null) { + mSettingsObserver.unregister(); + mSettingsObserver = null; + } + } + + /** + * Take care of handling configuration changes. Subclasses of + * InputMethodService generally don't need to deal directly with + * this on their own; the standard implementation here takes care of + * regenerating the input method UI as a result of the configuration + * change, so you can rely on your {@link #onCreateInputView} and + * other methods being called as appropriate due to a configuration change. + * + * <p>When a configuration change does happen, + * {@link #onInitializeInterface()} is guaranteed to be called the next + * time prior to any of the other input or UI creation callbacks. The + * following will be called immediately depending if appropriate for current + * state: {@link #onStartInput} if input is active, and + * {@link #onCreateInputView} and {@link #onStartInputView} and related + * appropriate functions if the UI is displayed. + */ + @Override public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + resetStateForNewConfiguration(); + } + + private void resetStateForNewConfiguration() { + boolean visible = mWindowVisible; + int showFlags = mShowInputFlags; + boolean showingInput = mShowInputRequested; + CompletionInfo[] completions = mCurCompletions; + initViews(); + mInputViewStarted = false; + mCandidatesViewStarted = false; + if (mInputStarted) { + doStartInput(getCurrentInputConnection(), + getCurrentInputEditorInfo(), true); + } + if (visible) { + if (showingInput) { + // If we were last showing the soft keyboard, try to do so again. + if (dispatchOnShowInputRequested(showFlags, true)) { + showWindow(true); + if (completions != null) { + mCurCompletions = completions; + onDisplayCompletions(completions); + } + } else { + doHideWindow(); + } + } else if (mCandidatesVisibility == View.VISIBLE) { + // If the candidates are currently visible, make sure the + // window is shown for them. + showWindow(false); + } else { + // Otherwise hide the window. + doHideWindow(); + } + // If user uses hard keyboard, IME button should always be shown. + boolean showing = onEvaluateInputViewShown(); + mImm.setImeWindowStatus(mToken, mStartInputToken, + IME_ACTIVE | (showing ? IME_VISIBLE : 0), mBackDisposition); + } + } + + /** + * Implement to return our standard {@link InputMethodImpl}. Subclasses + * can override to provide their own customized version. + */ + @Override + public AbstractInputMethodImpl onCreateInputMethodInterface() { + return new InputMethodImpl(); + } + + /** + * Implement to return our standard {@link InputMethodSessionImpl}. Subclasses + * can override to provide their own customized version. + */ + @Override + public AbstractInputMethodSessionImpl onCreateInputMethodSessionInterface() { + return new InputMethodSessionImpl(); + } + + public LayoutInflater getLayoutInflater() { + return mInflater; + } + + public Dialog getWindow() { + return mWindow; + } + + public void setBackDisposition(int disposition) { + mBackDisposition = disposition; + } + + public int getBackDisposition() { + return mBackDisposition; + } + + /** + * Return the maximum width, in pixels, available the input method. + * Input methods are positioned at the bottom of the screen and, unless + * running in fullscreen, will generally want to be as short as possible + * so should compute their height based on their contents. However, they + * can stretch as much as needed horizontally. The function returns to + * you the maximum amount of space available horizontally, which you can + * use if needed for UI placement. + * + * <p>In many cases this is not needed, you can just rely on the normal + * view layout mechanisms to position your views within the full horizontal + * space given to the input method. + * + * <p>Note that this value can change dynamically, in particular when the + * screen orientation changes. + */ + public int getMaxWidth() { + WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE); + return wm.getDefaultDisplay().getWidth(); + } + + /** + * Return the currently active InputBinding for the input method, or + * null if there is none. + */ + public InputBinding getCurrentInputBinding() { + return mInputBinding; + } + + /** + * Retrieve the currently active InputConnection that is bound to + * the input method, or null if there is none. + */ + public InputConnection getCurrentInputConnection() { + InputConnection ic = mStartedInputConnection; + if (ic != null) { + return ic; + } + return mInputConnection; + } + + public boolean getCurrentInputStarted() { + return mInputStarted; + } + + public EditorInfo getCurrentInputEditorInfo() { + return mInputEditorInfo; + } + + /** + * Re-evaluate whether the input method should be running in fullscreen + * mode, and update its UI if this has changed since the last time it + * was evaluated. This will call {@link #onEvaluateFullscreenMode()} to + * determine whether it should currently run in fullscreen mode. You + * can use {@link #isFullscreenMode()} to determine if the input method + * is currently running in fullscreen mode. + */ + public void updateFullscreenMode() { + boolean isFullscreen = mShowInputRequested && onEvaluateFullscreenMode(); + boolean changed = mLastShowInputRequested != mShowInputRequested; + if (mIsFullscreen != isFullscreen || !mFullscreenApplied) { + changed = true; + mIsFullscreen = isFullscreen; + if (mImm != null && mToken != null) { + mImm.reportFullscreenMode(mToken, mIsFullscreen); + } + mFullscreenApplied = true; + initialize(); + LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) + mFullscreenArea.getLayoutParams(); + if (isFullscreen) { + mFullscreenArea.setBackgroundDrawable(mThemeAttrs.getDrawable( + com.android.internal.R.styleable.InputMethodService_imeFullscreenBackground)); + lp.height = 0; + lp.weight = 1; + } else { + mFullscreenArea.setBackgroundDrawable(null); + lp.height = LinearLayout.LayoutParams.WRAP_CONTENT; + lp.weight = 0; + } + ((ViewGroup)mFullscreenArea.getParent()).updateViewLayout( + mFullscreenArea, lp); + if (isFullscreen) { + if (mExtractView == null) { + View v = onCreateExtractTextView(); + if (v != null) { + setExtractView(v); + } + } + startExtractingText(false); + } + updateExtractFrameVisibility(); + } + + if (changed) { + onConfigureWindow(mWindow.getWindow(), isFullscreen, !mShowInputRequested); + mLastShowInputRequested = mShowInputRequested; + } + } + + /** + * Update the given window's parameters for the given mode. This is called + * when the window is first displayed and each time the fullscreen or + * candidates only mode changes. + * + * <p>The default implementation makes the layout for the window + * MATCH_PARENT x MATCH_PARENT when in fullscreen mode, and + * MATCH_PARENT x WRAP_CONTENT when in non-fullscreen mode. + * + * @param win The input method's window. + * @param isFullscreen If true, the window is running in fullscreen mode + * and intended to cover the entire application display. + * @param isCandidatesOnly If true, the window is only showing the + * candidates view and none of the rest of its UI. This is mutually + * exclusive with fullscreen mode. + */ + public void onConfigureWindow(Window win, boolean isFullscreen, + boolean isCandidatesOnly) { + final int currentHeight = mWindow.getWindow().getAttributes().height; + final int newHeight = isFullscreen ? MATCH_PARENT : WRAP_CONTENT; + if (mIsInputViewShown && currentHeight != newHeight) { + if (DEBUG) { + Log.w(TAG,"Window size has been changed. This may cause jankiness of resizing " + + "window: " + currentHeight + " -> " + newHeight); + } + } + mWindow.getWindow().setLayout(MATCH_PARENT, newHeight); + } + + /** + * Return whether the input method is <em>currently</em> running in + * fullscreen mode. This is the mode that was last determined and + * applied by {@link #updateFullscreenMode()}. + */ + public boolean isFullscreenMode() { + return mIsFullscreen; + } + + /** + * Override this to control when the input method should run in + * fullscreen mode. The default implementation runs in fullsceen only + * when the screen is in landscape mode. If you change what + * this returns, you will need to call {@link #updateFullscreenMode()} + * yourself whenever the returned value may have changed to have it + * re-evaluated and applied. + */ + public boolean onEvaluateFullscreenMode() { + Configuration config = getResources().getConfiguration(); + if (config.orientation != Configuration.ORIENTATION_LANDSCAPE) { + return false; + } + if (mInputEditorInfo != null + && (mInputEditorInfo.imeOptions & EditorInfo.IME_FLAG_NO_FULLSCREEN) != 0) { + return false; + } + return true; + } + + /** + * Controls the visibility of the extracted text area. This only applies + * when the input method is in fullscreen mode, and thus showing extracted + * text. When false, the extracted text will not be shown, allowing some + * of the application to be seen behind. This is normally set for you + * by {@link #onUpdateExtractingVisibility}. This controls the visibility + * of both the extracted text and candidate view; the latter since it is + * not useful if there is no text to see. + */ + public void setExtractViewShown(boolean shown) { + if (mExtractViewHidden == shown) { + mExtractViewHidden = !shown; + updateExtractFrameVisibility(); + } + } + + /** + * Return whether the fullscreen extract view is shown. This will only + * return true if {@link #isFullscreenMode()} returns true, and in that + * case its value depends on the last call to + * {@link #setExtractViewShown(boolean)}. This effectively lets you + * determine if the application window is entirely covered (when this + * returns true) or if some part of it may be shown (if this returns + * false, though if {@link #isFullscreenMode()} returns true in that case + * then it is probably only a sliver of the application). + */ + public boolean isExtractViewShown() { + return mIsFullscreen && !mExtractViewHidden; + } + + void updateExtractFrameVisibility() { + final int vis; + if (isFullscreenMode()) { + vis = mExtractViewHidden ? View.INVISIBLE : View.VISIBLE; + // "vis" should be applied for the extract frame as well in the fullscreen mode. + mExtractFrame.setVisibility(vis); + } else { + vis = View.VISIBLE; + mExtractFrame.setVisibility(View.GONE); + } + updateCandidatesVisibility(mCandidatesVisibility == View.VISIBLE); + if (mWindowWasVisible && mFullscreenArea.getVisibility() != vis) { + int animRes = mThemeAttrs.getResourceId(vis == View.VISIBLE + ? com.android.internal.R.styleable.InputMethodService_imeExtractEnterAnimation + : com.android.internal.R.styleable.InputMethodService_imeExtractExitAnimation, + 0); + if (animRes != 0) { + mFullscreenArea.startAnimation(AnimationUtils.loadAnimation( + this, animRes)); + } + } + mFullscreenArea.setVisibility(vis); + } + + /** + * Compute the interesting insets into your UI. The default implementation + * uses the top of the candidates frame for the visible insets, and the + * top of the input frame for the content insets. The default touchable + * insets are {@link Insets#TOUCHABLE_INSETS_VISIBLE}. + * + * <p>Note that this method is not called when + * {@link #isExtractViewShown} returns true, since + * in that case the application is left as-is behind the input method and + * not impacted by anything in its UI. + * + * @param outInsets Fill in with the current UI insets. + */ + public void onComputeInsets(Insets outInsets) { + int[] loc = mTmpLocation; + if (mInputFrame.getVisibility() == View.VISIBLE) { + mInputFrame.getLocationInWindow(loc); + } else { + View decor = getWindow().getWindow().getDecorView(); + loc[1] = decor.getHeight(); + } + if (isFullscreenMode()) { + // In fullscreen mode, we never resize the underlying window. + View decor = getWindow().getWindow().getDecorView(); + outInsets.contentTopInsets = decor.getHeight(); + } else { + outInsets.contentTopInsets = loc[1]; + } + if (mCandidatesFrame.getVisibility() == View.VISIBLE) { + mCandidatesFrame.getLocationInWindow(loc); + } + outInsets.visibleTopInsets = loc[1]; + outInsets.touchableInsets = Insets.TOUCHABLE_INSETS_VISIBLE; + outInsets.touchableRegion.setEmpty(); + } + + /** + * Re-evaluate whether the soft input area should currently be shown, and + * update its UI if this has changed since the last time it + * was evaluated. This will call {@link #onEvaluateInputViewShown()} to + * determine whether the input view should currently be shown. You + * can use {@link #isInputViewShown()} to determine if the input view + * is currently shown. + */ + public void updateInputViewShown() { + boolean isShown = mShowInputRequested && onEvaluateInputViewShown(); + if (mIsInputViewShown != isShown && mWindowVisible) { + mIsInputViewShown = isShown; + mInputFrame.setVisibility(isShown ? View.VISIBLE : View.GONE); + if (mInputView == null) { + initialize(); + View v = onCreateInputView(); + if (v != null) { + setInputView(v); + } + } + } + } + + /** + * Returns true if we have been asked to show our input view. + */ + public boolean isShowInputRequested() { + return mShowInputRequested; + } + + /** + * Return whether the soft input view is <em>currently</em> shown to the + * user. This is the state that was last determined and + * applied by {@link #updateInputViewShown()}. + */ + public boolean isInputViewShown() { + return mIsInputViewShown && mWindowVisible; + } + + /** + * Override this to control when the soft input area should be shown to the user. The default + * implementation returns {@code false} when there is no hard keyboard or the keyboard is hidden + * unless the user shows an intention to use software keyboard. If you change what this + * returns, you will need to call {@link #updateInputViewShown()} yourself whenever the returned + * value may have changed to have it re-evaluated and applied. + * + * <p>When you override this method, it is recommended to call + * {@code super.onEvaluateInputViewShown()} and return {@code true} when {@code true} is + * returned.</p> + */ + @CallSuper + public boolean onEvaluateInputViewShown() { + if (mSettingsObserver == null) { + Log.w(TAG, "onEvaluateInputViewShown: mSettingsObserver must not be null here."); + return false; + } + if (mSettingsObserver.shouldShowImeWithHardKeyboard()) { + return true; + } + Configuration config = getResources().getConfiguration(); + return config.keyboard == Configuration.KEYBOARD_NOKEYS + || config.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_YES; + } + + /** + * Controls the visibility of the candidates display area. By default + * it is hidden. + */ + public void setCandidatesViewShown(boolean shown) { + updateCandidatesVisibility(shown); + if (!mShowInputRequested && mWindowVisible != shown) { + // If we are being asked to show the candidates view while the app + // has not asked for the input view to be shown, then we need + // to update whether the window is shown. + if (shown) { + showWindow(false); + } else { + doHideWindow(); + } + } + } + + void updateCandidatesVisibility(boolean shown) { + int vis = shown ? View.VISIBLE : getCandidatesHiddenVisibility(); + if (mCandidatesVisibility != vis) { + mCandidatesFrame.setVisibility(vis); + mCandidatesVisibility = vis; + } + } + + /** + * Returns the visibility mode (either {@link View#INVISIBLE View.INVISIBLE} + * or {@link View#GONE View.GONE}) of the candidates view when it is not + * shown. The default implementation returns GONE when + * {@link #isExtractViewShown} returns true, + * otherwise VISIBLE. Be careful if you change this to return GONE in + * other situations -- if showing or hiding the candidates view causes + * your window to resize, this can cause temporary drawing artifacts as + * the resize takes place. + */ + public int getCandidatesHiddenVisibility() { + return isExtractViewShown() ? View.GONE : View.INVISIBLE; + } + + public void showStatusIcon(@DrawableRes int iconResId) { + mStatusIcon = iconResId; + mImm.showStatusIcon(mToken, getPackageName(), iconResId); + } + + public void hideStatusIcon() { + mStatusIcon = 0; + mImm.hideStatusIcon(mToken); + } + + /** + * Force switch to a new input method, as identified by <var>id</var>. This + * input method will be destroyed, and the requested one started on the + * current input field. + * + * @param id Unique identifier of the new input method ot start. + */ + public void switchInputMethod(String id) { + mImm.setInputMethod(mToken, id); + } + + public void setExtractView(View view) { + mExtractFrame.removeAllViews(); + mExtractFrame.addView(view, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + mExtractView = view; + if (view != null) { + mExtractEditText = (ExtractEditText)view.findViewById( + com.android.internal.R.id.inputExtractEditText); + mExtractEditText.setIME(this); + mExtractAction = view.findViewById( + com.android.internal.R.id.inputExtractAction); + if (mExtractAction != null) { + mExtractAccessories = (ViewGroup)view.findViewById( + com.android.internal.R.id.inputExtractAccessories); + } + startExtractingText(false); + } else { + mExtractEditText = null; + mExtractAccessories = null; + mExtractAction = null; + } + } + + /** + * Replaces the current candidates view with a new one. You only need to + * call this when dynamically changing the view; normally, you should + * implement {@link #onCreateCandidatesView()} and create your view when + * first needed by the input method. + */ + public void setCandidatesView(View view) { + mCandidatesFrame.removeAllViews(); + mCandidatesFrame.addView(view, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + } + + /** + * Replaces the current input view with a new one. You only need to + * call this when dynamically changing the view; normally, you should + * implement {@link #onCreateInputView()} and create your view when + * first needed by the input method. + */ + public void setInputView(View view) { + mInputFrame.removeAllViews(); + mInputFrame.addView(view, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + mInputView = view; + } + + /** + * Called by the framework to create the layout for showing extacted text. + * Only called when in fullscreen mode. The returned view hierarchy must + * have an {@link ExtractEditText} whose ID is + * {@link android.R.id#inputExtractEditText}. + */ + public View onCreateExtractTextView() { + return mInflater.inflate( + com.android.internal.R.layout.input_method_extract_view, null); + } + + /** + * Create and return the view hierarchy used to show candidates. This will + * be called once, when the candidates are first displayed. You can return + * null to have no candidates view; the default implementation returns null. + * + * <p>To control when the candidates view is displayed, use + * {@link #setCandidatesViewShown(boolean)}. + * To change the candidates view after the first one is created by this + * function, use {@link #setCandidatesView(View)}. + */ + public View onCreateCandidatesView() { + return null; + } + + /** + * Create and return the view hierarchy used for the input area (such as + * a soft keyboard). This will be called once, when the input area is + * first displayed. You can return null to have no input area; the default + * implementation returns null. + * + * <p>To control when the input view is displayed, implement + * {@link #onEvaluateInputViewShown()}. + * To change the input view after the first one is created by this + * function, use {@link #setInputView(View)}. + */ + public View onCreateInputView() { + return null; + } + + /** + * Called when the input view is being shown and input has started on + * a new editor. This will always be called after {@link #onStartInput}, + * allowing you to do your general setup there and just view-specific + * setup here. You are guaranteed that {@link #onCreateInputView()} will + * have been called some time before this function is called. + * + * @param info Description of the type of text being edited. + * @param restarting Set to true if we are restarting input on the + * same text field as before. + */ + public void onStartInputView(EditorInfo info, boolean restarting) { + // Intentionally empty + } + + /** + * Called when the input view is being hidden from the user. This will + * be called either prior to hiding the window, or prior to switching to + * another target for editing. + * + * <p>The default + * implementation uses the InputConnection to clear any active composing + * text; you can override this (not calling the base class implementation) + * to perform whatever behavior you would like. + * + * @param finishingInput If true, {@link #onFinishInput} will be + * called immediately after. + */ + public void onFinishInputView(boolean finishingInput) { + if (!finishingInput) { + InputConnection ic = getCurrentInputConnection(); + if (ic != null) { + ic.finishComposingText(); + } + } + } + + /** + * Called when only the candidates view has been shown for showing + * processing as the user enters text through a hard keyboard. + * This will always be called after {@link #onStartInput}, + * allowing you to do your general setup there and just view-specific + * setup here. You are guaranteed that {@link #onCreateCandidatesView()} + * will have been called some time before this function is called. + * + * <p>Note that this will <em>not</em> be called when the input method + * is running in full editing mode, and thus receiving + * {@link #onStartInputView} to initiate that operation. This is only + * for the case when candidates are being shown while the input method + * editor is hidden but wants to show its candidates UI as text is + * entered through some other mechanism. + * + * @param info Description of the type of text being edited. + * @param restarting Set to true if we are restarting input on the + * same text field as before. + */ + public void onStartCandidatesView(EditorInfo info, boolean restarting) { + // Intentionally empty + } + + /** + * Called when the candidates view is being hidden from the user. This will + * be called either prior to hiding the window, or prior to switching to + * another target for editing. + * + * <p>The default + * implementation uses the InputConnection to clear any active composing + * text; you can override this (not calling the base class implementation) + * to perform whatever behavior you would like. + * + * @param finishingInput If true, {@link #onFinishInput} will be + * called immediately after. + */ + public void onFinishCandidatesView(boolean finishingInput) { + if (!finishingInput) { + InputConnection ic = getCurrentInputConnection(); + if (ic != null) { + ic.finishComposingText(); + } + } + } + + /** + * The system has decided that it may be time to show your input method. + * This is called due to a corresponding call to your + * {@link InputMethod#showSoftInput InputMethod.showSoftInput()} + * method. The default implementation uses + * {@link #onEvaluateInputViewShown()}, {@link #onEvaluateFullscreenMode()}, + * and the current configuration to decide whether the input view should + * be shown at this point. + * + * @param flags Provides additional information about the show request, + * as per {@link InputMethod#showSoftInput InputMethod.showSoftInput()}. + * @param configChange This is true if we are re-showing due to a + * configuration change. + * @return Returns true to indicate that the window should be shown. + */ + public boolean onShowInputRequested(int flags, boolean configChange) { + if (!onEvaluateInputViewShown()) { + return false; + } + if ((flags&InputMethod.SHOW_EXPLICIT) == 0) { + if (!configChange && onEvaluateFullscreenMode()) { + // Don't show if this is not explicitly requested by the user and + // the input method is fullscreen. That would be too disruptive. + // However, we skip this change for a config change, since if + // the IME is already shown we do want to go into fullscreen + // mode at this point. + return false; + } + if (!mSettingsObserver.shouldShowImeWithHardKeyboard() && + getResources().getConfiguration().keyboard != Configuration.KEYBOARD_NOKEYS) { + // And if the device has a hard keyboard, even if it is + // currently hidden, don't show the input method implicitly. + // These kinds of devices don't need it that much. + return false; + } + } + return true; + } + + /** + * A utility method to call {{@link #onShowInputRequested(int, boolean)}} and update internal + * states depending on its result. Since {@link #onShowInputRequested(int, boolean)} is + * exposed to IME authors as an overridable public method without {@code @CallSuper}, we have + * to have this method to ensure that those internal states are always updated no matter how + * {@link #onShowInputRequested(int, boolean)} is overridden by the IME author. + * @param flags Provides additional information about the show request, + * as per {@link InputMethod#showSoftInput InputMethod.showSoftInput()}. + * @param configChange This is true if we are re-showing due to a + * configuration change. + * @return Returns true to indicate that the window should be shown. + * @see #onShowInputRequested(int, boolean) + */ + private boolean dispatchOnShowInputRequested(int flags, boolean configChange) { + final boolean result = onShowInputRequested(flags, configChange); + if (result) { + mShowInputFlags = flags; + } else { + mShowInputFlags = 0; + } + return result; + } + + public void showWindow(boolean showInput) { + if (DEBUG) Log.v(TAG, "Showing window: showInput=" + showInput + + " mShowInputRequested=" + mShowInputRequested + + " mWindowAdded=" + mWindowAdded + + " mWindowCreated=" + mWindowCreated + + " mWindowVisible=" + mWindowVisible + + " mInputStarted=" + mInputStarted + + " mShowInputFlags=" + mShowInputFlags); + + if (mInShowWindow) { + Log.w(TAG, "Re-entrance in to showWindow"); + return; + } + + try { + mWindowWasVisible = mWindowVisible; + mInShowWindow = true; + showWindowInner(showInput); + } catch (BadTokenException e) { + // BadTokenException is a normal consequence in certain situations, e.g., swapping IMEs + // while there is a DO_SHOW_SOFT_INPUT message in the IIMethodWrapper queue. + if (DEBUG) Log.v(TAG, "BadTokenException: IME is done."); + mWindowVisible = false; + mWindowAdded = false; + // Rethrow the exception to preserve the existing behavior. Some IMEs may have directly + // called this method and relied on this exception for some clean-up tasks. + // TODO: Give developers a clear guideline of whether it's OK to call this method or + // InputMethodManager#showSoftInputFromInputMethod() should always be used instead. + throw e; + } finally { + // TODO: Is it OK to set true when we get BadTokenException? + mWindowWasVisible = true; + mInShowWindow = false; + } + } + + void showWindowInner(boolean showInput) { + boolean doShowInput = false; + final int previousImeWindowStatus = + (mWindowVisible ? IME_ACTIVE : 0) | (isInputViewShown() ? IME_VISIBLE : 0); + mWindowVisible = true; + if (!mShowInputRequested && mInputStarted && showInput) { + doShowInput = true; + mShowInputRequested = true; + } + + if (DEBUG) Log.v(TAG, "showWindow: updating UI"); + initialize(); + updateFullscreenMode(); + updateInputViewShown(); + + if (!mWindowAdded || !mWindowCreated) { + mWindowAdded = true; + mWindowCreated = true; + initialize(); + if (DEBUG) Log.v(TAG, "CALL: onCreateCandidatesView"); + View v = onCreateCandidatesView(); + if (DEBUG) Log.v(TAG, "showWindow: candidates=" + v); + if (v != null) { + setCandidatesView(v); + } + } + if (mShowInputRequested) { + if (!mInputViewStarted) { + if (DEBUG) Log.v(TAG, "CALL: onStartInputView"); + mInputViewStarted = true; + onStartInputView(mInputEditorInfo, false); + } + } else if (!mCandidatesViewStarted) { + if (DEBUG) Log.v(TAG, "CALL: onStartCandidatesView"); + mCandidatesViewStarted = true; + onStartCandidatesView(mInputEditorInfo, false); + } + + if (doShowInput) { + startExtractingText(false); + } + + final int nextImeWindowStatus = IME_ACTIVE | (isInputViewShown() ? IME_VISIBLE : 0); + if (previousImeWindowStatus != nextImeWindowStatus) { + mImm.setImeWindowStatus(mToken, mStartInputToken, nextImeWindowStatus, + mBackDisposition); + } + if ((previousImeWindowStatus & IME_ACTIVE) == 0) { + if (DEBUG) Log.v(TAG, "showWindow: showing!"); + onWindowShown(); + mWindow.show(); + // Put here rather than in onWindowShown() in case people forget to call + // super.onWindowShown(). + mShouldClearInsetOfPreviousIme = false; + } + } + + private void finishViews() { + if (mInputViewStarted) { + if (DEBUG) Log.v(TAG, "CALL: onFinishInputView"); + onFinishInputView(false); + } else if (mCandidatesViewStarted) { + if (DEBUG) Log.v(TAG, "CALL: onFinishCandidatesView"); + onFinishCandidatesView(false); + } + mInputViewStarted = false; + mCandidatesViewStarted = false; + } + + private void doHideWindow() { + mImm.setImeWindowStatus(mToken, mStartInputToken, 0, mBackDisposition); + hideWindow(); + } + + public void hideWindow() { + finishViews(); + if (mWindowVisible) { + mWindow.hide(); + mWindowVisible = false; + onWindowHidden(); + mWindowWasVisible = false; + } + updateFullscreenMode(); + } + + /** + * Called when the input method window has been shown to the user, after + * previously not being visible. This is done after all of the UI setup + * for the window has occurred (creating its views etc). + */ + public void onWindowShown() { + // Intentionally empty + } + + /** + * Called when the input method window has been hidden from the user, + * after previously being visible. + */ + public void onWindowHidden() { + // Intentionally empty + } + + /** + * Reset the inset occupied the previous IME when and only when + * {@link #mShouldClearInsetOfPreviousIme} is {@code true}. + */ + private void clearInsetOfPreviousIme() { + if (DEBUG) Log.v(TAG, "clearInsetOfPreviousIme() " + + " mShouldClearInsetOfPreviousIme=" + mShouldClearInsetOfPreviousIme); + if (!mShouldClearInsetOfPreviousIme) return; + + mImm.clearLastInputMethodWindowForTransition(mToken); + mShouldClearInsetOfPreviousIme = false; + } + + /** + * Called when a new client has bound to the input method. This + * may be followed by a series of {@link #onStartInput(EditorInfo, boolean)} + * and {@link #onFinishInput()} calls as the user navigates through its + * UI. Upon this call you know that {@link #getCurrentInputBinding} + * and {@link #getCurrentInputConnection} return valid objects. + */ + public void onBindInput() { + // Intentionally empty + } + + /** + * Called when the previous bound client is no longer associated + * with the input method. After returning {@link #getCurrentInputBinding} + * and {@link #getCurrentInputConnection} will no longer return + * valid objects. + */ + public void onUnbindInput() { + // Intentionally empty + } + + /** + * Called to inform the input method that text input has started in an + * editor. You should use this callback to initialize the state of your + * input to match the state of the editor given to it. + * + * @param attribute The attributes of the editor that input is starting + * in. + * @param restarting Set to true if input is restarting in the same + * editor such as because the application has changed the text in + * the editor. Otherwise will be false, indicating this is a new + * session with the editor. + */ + public void onStartInput(EditorInfo attribute, boolean restarting) { + // Intentionally empty + } + + void doFinishInput() { + if (mInputViewStarted) { + if (DEBUG) Log.v(TAG, "CALL: onFinishInputView"); + onFinishInputView(true); + } else if (mCandidatesViewStarted) { + if (DEBUG) Log.v(TAG, "CALL: onFinishCandidatesView"); + onFinishCandidatesView(true); + } + mInputViewStarted = false; + mCandidatesViewStarted = false; + if (mInputStarted) { + if (DEBUG) Log.v(TAG, "CALL: onFinishInput"); + onFinishInput(); + } + mInputStarted = false; + mStartedInputConnection = null; + mCurCompletions = null; + } + + void doStartInput(InputConnection ic, EditorInfo attribute, boolean restarting) { + if (!restarting) { + doFinishInput(); + } + mInputStarted = true; + mStartedInputConnection = ic; + mInputEditorInfo = attribute; + initialize(); + if (DEBUG) Log.v(TAG, "CALL: onStartInput"); + onStartInput(attribute, restarting); + if (mWindowVisible) { + if (mShowInputRequested) { + if (DEBUG) Log.v(TAG, "CALL: onStartInputView"); + mInputViewStarted = true; + onStartInputView(mInputEditorInfo, restarting); + startExtractingText(true); + } else if (mCandidatesVisibility == View.VISIBLE) { + if (DEBUG) Log.v(TAG, "CALL: onStartCandidatesView"); + mCandidatesViewStarted = true; + onStartCandidatesView(mInputEditorInfo, restarting); + } + } + } + + /** + * Called to inform the input method that text input has finished in + * the last editor. At this point there may be a call to + * {@link #onStartInput(EditorInfo, boolean)} to perform input in a + * new editor, or the input method may be left idle. This method is + * <em>not</em> called when input restarts in the same editor. + * + * <p>The default + * implementation uses the InputConnection to clear any active composing + * text; you can override this (not calling the base class implementation) + * to perform whatever behavior you would like. + */ + public void onFinishInput() { + InputConnection ic = getCurrentInputConnection(); + if (ic != null) { + ic.finishComposingText(); + } + } + + /** + * Called when the application has reported auto-completion candidates that + * it would like to have the input method displayed. Typically these are + * only used when an input method is running in full-screen mode, since + * otherwise the user can see and interact with the pop-up window of + * completions shown by the application. + * + * <p>The default implementation here does nothing. + */ + public void onDisplayCompletions(CompletionInfo[] completions) { + // Intentionally empty + } + + /** + * Called when the application has reported new extracted text to be shown + * due to changes in its current text state. The default implementation + * here places the new text in the extract edit text, when the input + * method is running in fullscreen mode. + */ + public void onUpdateExtractedText(int token, ExtractedText text) { + if (mExtractedToken != token) { + return; + } + if (text != null) { + if (mExtractEditText != null) { + mExtractedText = text; + mExtractEditText.setExtractedText(text); + } + } + } + + /** + * Called when the application has reported a new selection region of + * the text. This is called whether or not the input method has requested + * extracted text updates, although if so it will not receive this call + * if the extracted text has changed as well. + * + * <p>Be careful about changing the text in reaction to this call with + * methods such as setComposingText, commitText or + * deleteSurroundingText. If the cursor moves as a result, this method + * will be called again, which may result in an infinite loop. + * + * <p>The default implementation takes care of updating the cursor in + * the extract text, if it is being shown. + */ + public void onUpdateSelection(int oldSelStart, int oldSelEnd, + int newSelStart, int newSelEnd, + int candidatesStart, int candidatesEnd) { + final ExtractEditText eet = mExtractEditText; + if (eet != null && isFullscreenMode() && mExtractedText != null) { + final int off = mExtractedText.startOffset; + eet.startInternalChanges(); + newSelStart -= off; + newSelEnd -= off; + final int len = eet.getText().length(); + if (newSelStart < 0) newSelStart = 0; + else if (newSelStart > len) newSelStart = len; + if (newSelEnd < 0) newSelEnd = 0; + else if (newSelEnd > len) newSelEnd = len; + eet.setSelection(newSelStart, newSelEnd); + eet.finishInternalChanges(); + } + } + + /** + * Called when the user tapped or clicked a text view. + * IMEs can't rely on this method being called because this was not part of the original IME + * protocol, so applications with custom text editing written before this method appeared will + * not call to inform the IME of this interaction. + * @param focusChanged true if the user changed the focused view by this click. + */ + public void onViewClicked(boolean focusChanged) { + // Intentionally empty + } + + /** + * Called when the application has reported a new location of its text + * cursor. This is only called if explicitly requested by the input method. + * The default implementation does nothing. + * @deprecated Use {#link onUpdateCursorAnchorInfo(CursorAnchorInfo)} instead. + */ + @Deprecated + public void onUpdateCursor(Rect newCursor) { + // Intentionally empty + } + + /** + * Called when the application has reported a new location of its text insertion point and + * characters in the composition string. This is only called if explicitly requested by the + * input method. The default implementation does nothing. + * @param cursorAnchorInfo The positional information of the text insertion point and the + * composition string. + */ + public void onUpdateCursorAnchorInfo(CursorAnchorInfo cursorAnchorInfo) { + // Intentionally empty + } + + /** + * Close this input method's soft input area, removing it from the display. + * The input method will continue running, but the user can no longer use + * it to generate input by touching the screen. + * @param flags Provides additional operating flags. Currently may be + * 0 or have the {@link InputMethodManager#HIDE_IMPLICIT_ONLY + * InputMethodManager.HIDE_IMPLICIT_ONLY} bit set. + */ + public void requestHideSelf(int flags) { + mImm.hideSoftInputFromInputMethod(mToken, flags); + } + + /** + * Show the input method. This is a call back to the + * IMF to handle showing the input method. + * @param flags Provides additional operating flags. Currently may be + * 0 or have the {@link InputMethodManager#SHOW_FORCED + * InputMethodManager.} bit set. + */ + private void requestShowSelf(int flags) { + 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) doHideWindow(); + } + return true; + } + return false; + } + + /** + * @return {#link ExtractEditText} if it is considered to be visible and active. Otherwise + * {@code null} is returned. + */ + private ExtractEditText getExtractEditTextIfVisible() { + if (!isExtractViewShown() || !isInputViewShown()) { + return null; + } + return mExtractEditText; + } + + /** + * Override this to intercept key down events before they are processed by the + * application. If you return true, the application will not + * process the event itself. If you return false, the normal application processing + * will occur as if the IME had not seen the event at all. + * + * <p>The default implementation intercepts {@link KeyEvent#KEYCODE_BACK + * 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) { + final ExtractEditText eet = getExtractEditTextIfVisible(); + if (eet != null && eet.handleBackInTextActionModeIfNeeded(event)) { + return true; + } + if (handleBack(false)) { + event.startTracking(); + 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 + * application. If you return true, the application will not itself + * process the event. If you return false, the normal application processing + * will occur as if the IME had not seen the event at all. + * + * <p>The default implementation always returns false, except when + * in fullscreen mode, where 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 onKeyMultiple(int keyCode, int count, KeyEvent event) { + return doMovementKey(keyCode, event, count); + } + + /** + * Override this to intercept key up events before they are processed by the + * application. If you return true, the application will not itself + * process the event. If you return false, the normal application processing + * will occur as if the IME had not seen the event at all. + * + * <p>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) { + final ExtractEditText eet = getExtractEditTextIfVisible(); + if (eet != null && eet.handleBackInTextActionModeIfNeeded(event)) { + return true; + } + if (event.isTracking() && !event.isCanceled()) { + return handleBack(true); + } + } + return doMovementKey(keyCode, event, MOVEMENT_UP); + } + + /** + * Override this to intercept trackball motion events before they are + * processed by the application. + * If you return true, the application will not itself process the event. + * If you return false, the normal application processing will occur as if + * the IME had not seen the event at all. + */ + @Override + public boolean onTrackballEvent(MotionEvent event) { + if (DEBUG) Log.v(TAG, "onTrackballEvent: " + event); + return false; + } + + /** + * Override this to intercept generic motion events before they are + * processed by the application. + * If you return true, the application will not itself process the event. + * If you return false, the normal application processing will occur as if + * the IME had not seen the event at all. + */ + @Override + public boolean onGenericMotionEvent(MotionEvent event) { + if (DEBUG) Log.v(TAG, "onGenericMotionEvent(): event " + event); + return false; + } + + public void onAppPrivateCommand(String action, Bundle data) { + } + + /** + * Handle a request by the system to toggle the soft input area. + */ + private void onToggleSoftInput(int showFlags, int hideFlags) { + if (DEBUG) Log.v(TAG, "toggleSoftInput()"); + if (isInputViewShown()) { + requestHideSelf(hideFlags); + } else { + requestShowSelf(showFlags); + } + } + + static final int MOVEMENT_DOWN = -1; + static final int MOVEMENT_UP = -2; + + void reportExtractedMovement(int keyCode, int count) { + int dx = 0, dy = 0; + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_LEFT: + dx = -count; + break; + case KeyEvent.KEYCODE_DPAD_RIGHT: + dx = count; + break; + case KeyEvent.KEYCODE_DPAD_UP: + dy = -count; + break; + case KeyEvent.KEYCODE_DPAD_DOWN: + dy = count; + break; + } + onExtractedCursorMovement(dx, dy); + } + + boolean doMovementKey(int keyCode, KeyEvent event, int count) { + final ExtractEditText eet = getExtractEditTextIfVisible(); + if (eet != null) { + // If we are in fullscreen mode, the cursor will move around + // the extract edit text, but should NOT cause focus to move + // to other fields. + MovementMethod movement = eet.getMovementMethod(); + Layout layout = eet.getLayout(); + if (movement != null && layout != null) { + // We want our own movement method to handle the key, so the + // cursor will properly move in our own word wrapping. + if (count == MOVEMENT_DOWN) { + if (movement.onKeyDown(eet, eet.getText(), keyCode, event)) { + reportExtractedMovement(keyCode, 1); + return true; + } + } else if (count == MOVEMENT_UP) { + if (movement.onKeyUp(eet, eet.getText(), keyCode, event)) { + return true; + } + } else { + if (movement.onKeyOther(eet, eet.getText(), event)) { + reportExtractedMovement(keyCode, count); + } else { + KeyEvent down = KeyEvent.changeAction(event, KeyEvent.ACTION_DOWN); + if (movement.onKeyDown(eet, eet.getText(), keyCode, down)) { + KeyEvent up = KeyEvent.changeAction(event, KeyEvent.ACTION_UP); + movement.onKeyUp(eet, eet.getText(), keyCode, up); + while (--count > 0) { + movement.onKeyDown(eet, eet.getText(), keyCode, down); + movement.onKeyUp(eet, eet.getText(), keyCode, up); + } + reportExtractedMovement(keyCode, count); + } + } + } + } + // Regardless of whether the movement method handled the key, + // we never allow DPAD navigation to the application. + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_LEFT: + case KeyEvent.KEYCODE_DPAD_RIGHT: + case KeyEvent.KEYCODE_DPAD_UP: + case KeyEvent.KEYCODE_DPAD_DOWN: + return true; + } + } + + return false; + } + + /** + * Send the given key event code (as defined by {@link KeyEvent}) to the + * current input connection is a key down + key up event pair. The sent + * events have {@link KeyEvent#FLAG_SOFT_KEYBOARD KeyEvent.FLAG_SOFT_KEYBOARD} + * set, so that the recipient can identify them as coming from a software + * input method, and + * {@link KeyEvent#FLAG_KEEP_TOUCH_MODE KeyEvent.FLAG_KEEP_TOUCH_MODE}, so + * that they don't impact the current touch mode of the UI. + * + * <p>Note that it's discouraged to send such key events in normal operation; + * this is mainly for use with {@link android.text.InputType#TYPE_NULL} type + * text fields, or for non-rich input methods. A reasonably capable software + * input method should use the + * {@link android.view.inputmethod.InputConnection#commitText} family of methods + * to send text to an application, rather than sending key events.</p> + * + * @param keyEventCode The raw key code to send, as defined by + * {@link KeyEvent}. + */ + public void sendDownUpKeyEvents(int keyEventCode) { + InputConnection ic = getCurrentInputConnection(); + if (ic == null) return; + long eventTime = SystemClock.uptimeMillis(); + ic.sendKeyEvent(new KeyEvent(eventTime, eventTime, + KeyEvent.ACTION_DOWN, keyEventCode, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, + KeyEvent.FLAG_SOFT_KEYBOARD|KeyEvent.FLAG_KEEP_TOUCH_MODE)); + ic.sendKeyEvent(new KeyEvent(eventTime, SystemClock.uptimeMillis(), + KeyEvent.ACTION_UP, keyEventCode, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, + KeyEvent.FLAG_SOFT_KEYBOARD|KeyEvent.FLAG_KEEP_TOUCH_MODE)); + } + + /** + * Ask the input target to execute its default action via + * {@link InputConnection#performEditorAction + * InputConnection.performEditorAction()}. + * + * @param fromEnterKey If true, this will be executed as if the user had + * pressed an enter key on the keyboard, that is it will <em>not</em> + * be done if the editor has set {@link EditorInfo#IME_FLAG_NO_ENTER_ACTION + * EditorInfo.IME_FLAG_NO_ENTER_ACTION}. If false, the action will be + * sent regardless of how the editor has set that flag. + * + * @return Returns a boolean indicating whether an action has been sent. + * If false, either the editor did not specify a default action or it + * does not want an action from the enter key. If true, the action was + * sent (or there was no input connection at all). + */ + public boolean sendDefaultEditorAction(boolean fromEnterKey) { + EditorInfo ei = getCurrentInputEditorInfo(); + if (ei != null && + (!fromEnterKey || (ei.imeOptions & + EditorInfo.IME_FLAG_NO_ENTER_ACTION) == 0) && + (ei.imeOptions & EditorInfo.IME_MASK_ACTION) != + EditorInfo.IME_ACTION_NONE) { + // If the enter key was pressed, and the editor has a default + // action associated with pressing enter, then send it that + // explicit action instead of the key event. + InputConnection ic = getCurrentInputConnection(); + if (ic != null) { + ic.performEditorAction(ei.imeOptions&EditorInfo.IME_MASK_ACTION); + } + return true; + } + + return false; + } + + /** + * Send the given UTF-16 character to the current input connection. Most + * characters will be delivered simply by calling + * {@link InputConnection#commitText InputConnection.commitText()} with + * the character; some, however, may be handled different. In particular, + * the enter character ('\n') will either be delivered as an action code + * or a raw key event, as appropriate. Consider this as a convenience + * method for IMEs that do not have a full implementation of actions; a + * fully complying IME will decide of the right action for each event and + * will likely never call this method except maybe to handle events coming + * from an actual hardware keyboard. + * + * @param charCode The UTF-16 character code to send. + */ + public void sendKeyChar(char charCode) { + switch (charCode) { + case '\n': // Apps may be listening to an enter key to perform an action + if (!sendDefaultEditorAction(true)) { + sendDownUpKeyEvents(KeyEvent.KEYCODE_ENTER); + } + break; + default: + // Make sure that digits go through any text watcher on the client side. + if (charCode >= '0' && charCode <= '9') { + sendDownUpKeyEvents(charCode - '0' + KeyEvent.KEYCODE_0); + } else { + InputConnection ic = getCurrentInputConnection(); + if (ic != null) { + ic.commitText(String.valueOf(charCode), 1); + } + } + break; + } + } + + /** + * This is called when the user has moved the cursor in the extracted + * text view, when running in fullsreen mode. The default implementation + * performs the corresponding selection change on the underlying text + * editor. + */ + public void onExtractedSelectionChanged(int start, int end) { + InputConnection conn = getCurrentInputConnection(); + if (conn != null) { + conn.setSelection(start, end); + } + } + + /** + * @hide + */ + public void onExtractedDeleteText(int start, int end) { + InputConnection conn = getCurrentInputConnection(); + if (conn != null) { + conn.finishComposingText(); + conn.setSelection(start, start); + conn.deleteSurroundingText(0, end - start); + } + } + + /** + * @hide + */ + public void onExtractedReplaceText(int start, int end, CharSequence text) { + InputConnection conn = getCurrentInputConnection(); + if (conn != null) { + conn.setComposingRegion(start, end); + conn.commitText(text, 1); + } + } + + /** + * @hide + */ + public void onExtractedSetSpan(Object span, int start, int end, int flags) { + InputConnection conn = getCurrentInputConnection(); + if (conn != null) { + if (!conn.setSelection(start, end)) return; + CharSequence text = conn.getSelectedText(InputConnection.GET_TEXT_WITH_STYLES); + if (text instanceof Spannable) { + ((Spannable) text).setSpan(span, 0, text.length(), flags); + conn.setComposingRegion(start, end); + conn.commitText(text, 1); + } + } + } + + /** + * This is called when the user has clicked on the extracted text view, + * when running in fullscreen mode. The default implementation hides + * the candidates view when this happens, but only if the extracted text + * editor has a vertical scroll bar because its text doesn't fit. + * Re-implement this to provide whatever behavior you want. + */ + public void onExtractedTextClicked() { + if (mExtractEditText == null) { + return; + } + if (mExtractEditText.hasVerticalScrollBar()) { + setCandidatesViewShown(false); + } + } + + /** + * This is called when the user has performed a cursor movement in the + * extracted text view, when it is running in fullscreen mode. The default + * implementation hides the candidates view when a vertical movement + * happens, but only if the extracted text editor has a vertical scroll bar + * because its text doesn't fit. + * Re-implement this to provide whatever behavior you want. + * @param dx The amount of cursor movement in the x dimension. + * @param dy The amount of cursor movement in the y dimension. + */ + public void onExtractedCursorMovement(int dx, int dy) { + if (mExtractEditText == null || dy == 0) { + return; + } + if (mExtractEditText.hasVerticalScrollBar()) { + setCandidatesViewShown(false); + } + } + + /** + * This is called when the user has selected a context menu item from the + * extracted text view, when running in fullscreen mode. The default + * implementation sends this action to the current InputConnection's + * {@link InputConnection#performContextMenuAction(int)}, for it + * to be processed in underlying "real" editor. Re-implement this to + * provide whatever behavior you want. + */ + public boolean onExtractTextContextMenuItem(int id) { + InputConnection ic = getCurrentInputConnection(); + if (ic != null) { + ic.performContextMenuAction(id); + } + return true; + } + + /** + * Return text that can be used as a button label for the given + * {@link EditorInfo#imeOptions EditorInfo.imeOptions}. Returns null + * if there is no action requested. Note that there is no guarantee that + * the returned text will be relatively short, so you probably do not + * want to use it as text on a soft keyboard key label. + * + * @param imeOptions The value from {@link EditorInfo#imeOptions EditorInfo.imeOptions}. + * + * @return Returns a label to use, or null if there is no action. + */ + public CharSequence getTextForImeAction(int imeOptions) { + switch (imeOptions&EditorInfo.IME_MASK_ACTION) { + case EditorInfo.IME_ACTION_NONE: + return null; + case EditorInfo.IME_ACTION_GO: + return getText(com.android.internal.R.string.ime_action_go); + case EditorInfo.IME_ACTION_SEARCH: + return getText(com.android.internal.R.string.ime_action_search); + case EditorInfo.IME_ACTION_SEND: + return getText(com.android.internal.R.string.ime_action_send); + case EditorInfo.IME_ACTION_NEXT: + return getText(com.android.internal.R.string.ime_action_next); + case EditorInfo.IME_ACTION_DONE: + return getText(com.android.internal.R.string.ime_action_done); + case EditorInfo.IME_ACTION_PREVIOUS: + return getText(com.android.internal.R.string.ime_action_previous); + default: + return getText(com.android.internal.R.string.ime_action_default); + } + } + + /** + * Return a drawable resource id that can be used as a button icon for the given + * {@link EditorInfo#imeOptions EditorInfo.imeOptions}. + * + * @param imeOptions The value from @link EditorInfo#imeOptions EditorInfo.imeOptions}. + * + * @return Returns a drawable resource id to use. + */ + @DrawableRes + private int getIconForImeAction(int imeOptions) { + switch (imeOptions&EditorInfo.IME_MASK_ACTION) { + case EditorInfo.IME_ACTION_GO: + return com.android.internal.R.drawable.ic_input_extract_action_go; + case EditorInfo.IME_ACTION_SEARCH: + return com.android.internal.R.drawable.ic_input_extract_action_search; + case EditorInfo.IME_ACTION_SEND: + return com.android.internal.R.drawable.ic_input_extract_action_send; + case EditorInfo.IME_ACTION_NEXT: + return com.android.internal.R.drawable.ic_input_extract_action_next; + case EditorInfo.IME_ACTION_DONE: + return com.android.internal.R.drawable.ic_input_extract_action_done; + case EditorInfo.IME_ACTION_PREVIOUS: + return com.android.internal.R.drawable.ic_input_extract_action_previous; + default: + return com.android.internal.R.drawable.ic_input_extract_action_return; + } + } + + /** + * Called when the fullscreen-mode extracting editor info has changed, + * to determine whether the extracting (extract text and candidates) portion + * of the UI should be shown. The standard implementation hides or shows + * the extract area depending on whether it makes sense for the + * current editor. In particular, a {@link InputType#TYPE_NULL} + * input type or {@link EditorInfo#IME_FLAG_NO_EXTRACT_UI} flag will + * turn off the extract area since there is no text to be shown. + */ + public void onUpdateExtractingVisibility(EditorInfo ei) { + if (ei.inputType == InputType.TYPE_NULL || + (ei.imeOptions&EditorInfo.IME_FLAG_NO_EXTRACT_UI) != 0) { + // No reason to show extract UI! + setExtractViewShown(false); + return; + } + + setExtractViewShown(true); + } + + /** + * Called when the fullscreen-mode extracting editor info has changed, + * to update the state of its UI such as the action buttons shown. + * You do not need to deal with this if you are using the standard + * full screen extract UI. If replacing it, you will need to re-implement + * this to put the appropriate action button in your own UI and handle it, + * and perform any other changes. + * + * <p>The standard implementation turns on or off its accessory area + * depending on whether there is an action button, and hides or shows + * the entire extract area depending on whether it makes sense for the + * current editor. In particular, a {@link InputType#TYPE_NULL} or + * {@link InputType#TYPE_TEXT_VARIATION_FILTER} input type will turn off the + * extract area since there is no text to be shown. + */ + public void onUpdateExtractingViews(EditorInfo ei) { + if (!isExtractViewShown()) { + return; + } + + if (mExtractAccessories == null) { + return; + } + final boolean hasAction = ei.actionLabel != null || ( + (ei.imeOptions&EditorInfo.IME_MASK_ACTION) != EditorInfo.IME_ACTION_NONE && + (ei.imeOptions&EditorInfo.IME_FLAG_NO_ACCESSORY_ACTION) == 0 && + ei.inputType != InputType.TYPE_NULL); + if (hasAction) { + mExtractAccessories.setVisibility(View.VISIBLE); + if (mExtractAction != null) { + if (mExtractAction instanceof ImageButton) { + ((ImageButton) mExtractAction) + .setImageResource(getIconForImeAction(ei.imeOptions)); + if (ei.actionLabel != null) { + mExtractAction.setContentDescription(ei.actionLabel); + } else { + mExtractAction.setContentDescription(getTextForImeAction(ei.imeOptions)); + } + } else { + if (ei.actionLabel != null) { + ((TextView) mExtractAction).setText(ei.actionLabel); + } else { + ((TextView) mExtractAction).setText(getTextForImeAction(ei.imeOptions)); + } + } + mExtractAction.setOnClickListener(mActionClickListener); + } + } else { + mExtractAccessories.setVisibility(View.GONE); + if (mExtractAction != null) { + mExtractAction.setOnClickListener(null); + } + } + } + + /** + * This is called when, while currently displayed in extract mode, the + * current input target changes. The default implementation will + * auto-hide the IME if the new target is not a full editor, since this + * can be a confusing experience for the user. + */ + public void onExtractingInputChanged(EditorInfo ei) { + if (ei.inputType == InputType.TYPE_NULL) { + requestHideSelf(InputMethodManager.HIDE_NOT_ALWAYS); + } + } + + void startExtractingText(boolean inputChanged) { + final ExtractEditText eet = mExtractEditText; + if (eet != null && getCurrentInputStarted() + && isFullscreenMode()) { + mExtractedToken++; + ExtractedTextRequest req = new ExtractedTextRequest(); + req.token = mExtractedToken; + req.flags = InputConnection.GET_TEXT_WITH_STYLES; + req.hintMaxLines = 10; + req.hintMaxChars = 10000; + InputConnection ic = getCurrentInputConnection(); + mExtractedText = ic == null? null + : ic.getExtractedText(req, InputConnection.GET_EXTRACTED_TEXT_MONITOR); + if (mExtractedText == null || ic == null) { + Log.e(TAG, "Unexpected null in startExtractingText : mExtractedText = " + + mExtractedText + ", input connection = " + ic); + } + final EditorInfo ei = getCurrentInputEditorInfo(); + + try { + eet.startInternalChanges(); + onUpdateExtractingVisibility(ei); + onUpdateExtractingViews(ei); + int inputType = ei.inputType; + if ((inputType&EditorInfo.TYPE_MASK_CLASS) + == EditorInfo.TYPE_CLASS_TEXT) { + if ((inputType&EditorInfo.TYPE_TEXT_FLAG_IME_MULTI_LINE) != 0) { + inputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE; + } + } + eet.setInputType(inputType); + eet.setHint(ei.hintText); + if (mExtractedText != null) { + eet.setEnabled(true); + eet.setExtractedText(mExtractedText); + } else { + eet.setEnabled(false); + eet.setText(""); + } + } finally { + eet.finishInternalChanges(); + } + + if (inputChanged) { + onExtractingInputChanged(ei); + } + } + } + + // TODO: Handle the subtype change event + /** + * Called when the subtype was changed. + * @param newSubtype the subtype which is being changed to. + */ + protected void onCurrentInputMethodSubtypeChanged(InputMethodSubtype newSubtype) { + if (DEBUG) { + int nameResId = newSubtype.getNameResId(); + String mode = newSubtype.getMode(); + String output = "changeInputMethodSubtype:" + + (nameResId == 0 ? "<none>" : getString(nameResId)) + "," + + mode + "," + + newSubtype.getLocale() + "," + newSubtype.getExtraValue(); + Log.v(TAG, "--- " + output); + } + } + + /** + * @return The recommended height of the input method window. + * An IME author can get the last input method's height as the recommended height + * by calling this in + * {@link android.inputmethodservice.InputMethodService#onStartInputView(EditorInfo, boolean)}. + * If you don't need to use a predefined fixed height, you can avoid the window-resizing of IME + * switching by using this value as a visible inset height. It's efficient for the smooth + * transition between different IMEs. However, note that this may return 0 (or possibly + * unexpectedly low height). You should thus avoid relying on the return value of this method + * all the time. Please make sure to use a reasonable height for the IME. + */ + public int getInputMethodWindowRecommendedHeight() { + return mImm.getInputMethodWindowVisibleHeight(); + } + + /** + * Allow the receiver of {@link InputContentInfo} to obtain a temporary read-only access + * permission to the content. + * + * @param inputContentInfo Content to be temporarily exposed from the input method to the + * application. + * This cannot be {@code null}. + * @param inputConnection {@link InputConnection} with which + * {@link InputConnection#commitContent(InputContentInfo, Bundle)} will be called. + * @hide + */ + @Override + public final void exposeContent(@NonNull InputContentInfo inputContentInfo, + @NonNull InputConnection inputConnection) { + if (inputConnection == null) { + return; + } + if (getCurrentInputConnection() != inputConnection) { + return; + } + mImm.exposeContent(mToken, inputContentInfo, getCurrentInputEditorInfo()); + } + + /** + * Performs a dump of the InputMethodService's internal state. Override + * to add your own information to the dump. + */ + @Override protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) { + final Printer p = new PrintWriterPrinter(fout); + p.println("Input method service state for " + this + ":"); + p.println(" mWindowCreated=" + mWindowCreated + + " mWindowAdded=" + mWindowAdded); + p.println(" mWindowVisible=" + mWindowVisible + + " mWindowWasVisible=" + mWindowWasVisible + + " mInShowWindow=" + mInShowWindow); + p.println(" Configuration=" + getResources().getConfiguration()); + p.println(" mToken=" + mToken); + p.println(" mInputBinding=" + mInputBinding); + p.println(" mInputConnection=" + mInputConnection); + p.println(" mStartedInputConnection=" + mStartedInputConnection); + p.println(" mInputStarted=" + mInputStarted + + " mInputViewStarted=" + mInputViewStarted + + " mCandidatesViewStarted=" + mCandidatesViewStarted); + p.println(" mStartInputToken=" + mStartInputToken); + + if (mInputEditorInfo != null) { + p.println(" mInputEditorInfo:"); + mInputEditorInfo.dump(p, " "); + } else { + p.println(" mInputEditorInfo: null"); + } + + p.println(" mShowInputRequested=" + mShowInputRequested + + " mLastShowInputRequested=" + mLastShowInputRequested + + " mShowInputFlags=0x" + Integer.toHexString(mShowInputFlags)); + p.println(" mCandidatesVisibility=" + mCandidatesVisibility + + " mFullscreenApplied=" + mFullscreenApplied + + " mIsFullscreen=" + mIsFullscreen + + " mExtractViewHidden=" + mExtractViewHidden); + + if (mExtractedText != null) { + p.println(" mExtractedText:"); + p.println(" text=" + mExtractedText.text.length() + " chars" + + " startOffset=" + mExtractedText.startOffset); + p.println(" selectionStart=" + mExtractedText.selectionStart + + " selectionEnd=" + mExtractedText.selectionEnd + + " flags=0x" + Integer.toHexString(mExtractedText.flags)); + } else { + p.println(" mExtractedText: null"); + } + p.println(" mExtractedToken=" + mExtractedToken); + p.println(" mIsInputViewShown=" + mIsInputViewShown + + " mStatusIcon=" + mStatusIcon); + p.println("Last computed insets:"); + p.println(" contentTopInsets=" + mTmpInsets.contentTopInsets + + " visibleTopInsets=" + mTmpInsets.visibleTopInsets + + " touchableInsets=" + mTmpInsets.touchableInsets + + " touchableRegion=" + mTmpInsets.touchableRegion); + p.println(" mShouldClearInsetOfPreviousIme=" + mShouldClearInsetOfPreviousIme); + p.println(" mSettingsObserver=" + mSettingsObserver); + } +} diff --git a/android/inputmethodservice/Keyboard.java b/android/inputmethodservice/Keyboard.java new file mode 100644 index 00000000..a5490eff --- /dev/null +++ b/android/inputmethodservice/Keyboard.java @@ -0,0 +1,907 @@ +/* + * Copyright (C) 2008-2009 Google Inc. + * + * 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.inputmethodservice; + +import org.xmlpull.v1.XmlPullParserException; + +import android.annotation.XmlRes; +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.graphics.drawable.Drawable; +import android.text.TextUtils; +import android.util.Log; +import android.util.TypedValue; +import android.util.Xml; +import android.util.DisplayMetrics; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.StringTokenizer; + + +/** + * Loads an XML description of a keyboard and stores the attributes of the keys. A keyboard + * consists of rows of keys. + * <p>The layout file for a keyboard contains XML that looks like the following snippet:</p> + * <pre> + * <Keyboard + * android:keyWidth="%10p" + * android:keyHeight="50px" + * android:horizontalGap="2px" + * android:verticalGap="2px" > + * <Row android:keyWidth="32px" > + * <Key android:keyLabel="A" /> + * ... + * </Row> + * ... + * </Keyboard> + * </pre> + * @attr ref android.R.styleable#Keyboard_keyWidth + * @attr ref android.R.styleable#Keyboard_keyHeight + * @attr ref android.R.styleable#Keyboard_horizontalGap + * @attr ref android.R.styleable#Keyboard_verticalGap + */ +public class Keyboard { + + static final String TAG = "Keyboard"; + + // Keyboard XML Tags + private static final String TAG_KEYBOARD = "Keyboard"; + private static final String TAG_ROW = "Row"; + private static final String TAG_KEY = "Key"; + + public static final int EDGE_LEFT = 0x01; + public static final int EDGE_RIGHT = 0x02; + public static final int EDGE_TOP = 0x04; + public static final int EDGE_BOTTOM = 0x08; + + public static final int KEYCODE_SHIFT = -1; + public static final int KEYCODE_MODE_CHANGE = -2; + public static final int KEYCODE_CANCEL = -3; + public static final int KEYCODE_DONE = -4; + public static final int KEYCODE_DELETE = -5; + public static final int KEYCODE_ALT = -6; + + /** Keyboard label **/ + private CharSequence mLabel; + + /** Horizontal gap default for all rows */ + private int mDefaultHorizontalGap; + + /** Default key width */ + private int mDefaultWidth; + + /** Default key height */ + private int mDefaultHeight; + + /** Default gap between rows */ + private int mDefaultVerticalGap; + + /** Is the keyboard in the shifted state */ + private boolean mShifted; + + /** Key instance for the shift key, if present */ + private Key[] mShiftKeys = { null, null }; + + /** Key index for the shift key, if present */ + private int[] mShiftKeyIndices = {-1, -1}; + + /** Current key width, while loading the keyboard */ + private int mKeyWidth; + + /** Current key height, while loading the keyboard */ + private int mKeyHeight; + + /** Total height of the keyboard, including the padding and keys */ + private int mTotalHeight; + + /** + * Total width of the keyboard, including left side gaps and keys, but not any gaps on the + * right side. + */ + private int mTotalWidth; + + /** List of keys in this keyboard */ + private List<Key> mKeys; + + /** List of modifier keys such as Shift & Alt, if any */ + private List<Key> mModifierKeys; + + /** Width of the screen available to fit the keyboard */ + private int mDisplayWidth; + + /** Height of the screen */ + private int mDisplayHeight; + + /** Keyboard mode, or zero, if none. */ + private int mKeyboardMode; + + // Variables for pre-computing nearest keys. + + private static final int GRID_WIDTH = 10; + private static final int GRID_HEIGHT = 5; + private static final int GRID_SIZE = GRID_WIDTH * GRID_HEIGHT; + private int mCellWidth; + private int mCellHeight; + private int[][] mGridNeighbors; + private int mProximityThreshold; + /** Number of key widths from current touch point to search for nearest keys. */ + private static float SEARCH_DISTANCE = 1.8f; + + private ArrayList<Row> rows = new ArrayList<Row>(); + + /** + * Container for keys in the keyboard. All keys in a row are at the same Y-coordinate. + * Some of the key size defaults can be overridden per row from what the {@link Keyboard} + * defines. + * @attr ref android.R.styleable#Keyboard_keyWidth + * @attr ref android.R.styleable#Keyboard_keyHeight + * @attr ref android.R.styleable#Keyboard_horizontalGap + * @attr ref android.R.styleable#Keyboard_verticalGap + * @attr ref android.R.styleable#Keyboard_Row_rowEdgeFlags + * @attr ref android.R.styleable#Keyboard_Row_keyboardMode + */ + public static class Row { + /** Default width of a key in this row. */ + public int defaultWidth; + /** Default height of a key in this row. */ + public int defaultHeight; + /** Default horizontal gap between keys in this row. */ + public int defaultHorizontalGap; + /** Vertical gap following this row. */ + public int verticalGap; + + ArrayList<Key> mKeys = new ArrayList<Key>(); + + /** + * Edge flags for this row of keys. Possible values that can be assigned are + * {@link Keyboard#EDGE_TOP EDGE_TOP} and {@link Keyboard#EDGE_BOTTOM EDGE_BOTTOM} + */ + public int rowEdgeFlags; + + /** The keyboard mode for this row */ + public int mode; + + private Keyboard parent; + + public Row(Keyboard parent) { + this.parent = parent; + } + + public Row(Resources res, Keyboard parent, XmlResourceParser parser) { + this.parent = parent; + TypedArray a = res.obtainAttributes(Xml.asAttributeSet(parser), + com.android.internal.R.styleable.Keyboard); + defaultWidth = getDimensionOrFraction(a, + com.android.internal.R.styleable.Keyboard_keyWidth, + parent.mDisplayWidth, parent.mDefaultWidth); + defaultHeight = getDimensionOrFraction(a, + com.android.internal.R.styleable.Keyboard_keyHeight, + parent.mDisplayHeight, parent.mDefaultHeight); + defaultHorizontalGap = getDimensionOrFraction(a, + com.android.internal.R.styleable.Keyboard_horizontalGap, + parent.mDisplayWidth, parent.mDefaultHorizontalGap); + verticalGap = getDimensionOrFraction(a, + com.android.internal.R.styleable.Keyboard_verticalGap, + parent.mDisplayHeight, parent.mDefaultVerticalGap); + a.recycle(); + a = res.obtainAttributes(Xml.asAttributeSet(parser), + com.android.internal.R.styleable.Keyboard_Row); + rowEdgeFlags = a.getInt(com.android.internal.R.styleable.Keyboard_Row_rowEdgeFlags, 0); + mode = a.getResourceId(com.android.internal.R.styleable.Keyboard_Row_keyboardMode, + 0); + } + } + + /** + * Class for describing the position and characteristics of a single key in the keyboard. + * + * @attr ref android.R.styleable#Keyboard_keyWidth + * @attr ref android.R.styleable#Keyboard_keyHeight + * @attr ref android.R.styleable#Keyboard_horizontalGap + * @attr ref android.R.styleable#Keyboard_Key_codes + * @attr ref android.R.styleable#Keyboard_Key_keyIcon + * @attr ref android.R.styleable#Keyboard_Key_keyLabel + * @attr ref android.R.styleable#Keyboard_Key_iconPreview + * @attr ref android.R.styleable#Keyboard_Key_isSticky + * @attr ref android.R.styleable#Keyboard_Key_isRepeatable + * @attr ref android.R.styleable#Keyboard_Key_isModifier + * @attr ref android.R.styleable#Keyboard_Key_popupKeyboard + * @attr ref android.R.styleable#Keyboard_Key_popupCharacters + * @attr ref android.R.styleable#Keyboard_Key_keyOutputText + * @attr ref android.R.styleable#Keyboard_Key_keyEdgeFlags + */ + public static class Key { + /** + * All the key codes (unicode or custom code) that this key could generate, zero'th + * being the most important. + */ + public int[] codes; + + /** Label to display */ + public CharSequence label; + + /** Icon to display instead of a label. Icon takes precedence over a label */ + public Drawable icon; + /** Preview version of the icon, for the preview popup */ + public Drawable iconPreview; + /** Width of the key, not including the gap */ + public int width; + /** Height of the key, not including the gap */ + public int height; + /** The horizontal gap before this key */ + public int gap; + /** Whether this key is sticky, i.e., a toggle key */ + public boolean sticky; + /** X coordinate of the key in the keyboard layout */ + public int x; + /** Y coordinate of the key in the keyboard layout */ + public int y; + /** The current pressed state of this key */ + public boolean pressed; + /** If this is a sticky key, is it on? */ + public boolean on; + /** Text to output when pressed. This can be multiple characters, like ".com" */ + public CharSequence text; + /** Popup characters */ + public CharSequence popupCharacters; + + /** + * Flags that specify the anchoring to edges of the keyboard for detecting touch events + * that are just out of the boundary of the key. This is a bit mask of + * {@link Keyboard#EDGE_LEFT}, {@link Keyboard#EDGE_RIGHT}, {@link Keyboard#EDGE_TOP} and + * {@link Keyboard#EDGE_BOTTOM}. + */ + public int edgeFlags; + /** Whether this is a modifier key, such as Shift or Alt */ + public boolean modifier; + /** The keyboard that this key belongs to */ + private Keyboard keyboard; + /** + * If this key pops up a mini keyboard, this is the resource id for the XML layout for that + * keyboard. + */ + public int popupResId; + /** Whether this key repeats itself when held down */ + public boolean repeatable; + + + private final static int[] KEY_STATE_NORMAL_ON = { + android.R.attr.state_checkable, + android.R.attr.state_checked + }; + + private final static int[] KEY_STATE_PRESSED_ON = { + android.R.attr.state_pressed, + android.R.attr.state_checkable, + android.R.attr.state_checked + }; + + private final static int[] KEY_STATE_NORMAL_OFF = { + android.R.attr.state_checkable + }; + + private final static int[] KEY_STATE_PRESSED_OFF = { + android.R.attr.state_pressed, + android.R.attr.state_checkable + }; + + private final static int[] KEY_STATE_NORMAL = { + }; + + private final static int[] KEY_STATE_PRESSED = { + android.R.attr.state_pressed + }; + + /** Create an empty key with no attributes. */ + public Key(Row parent) { + keyboard = parent.parent; + height = parent.defaultHeight; + width = parent.defaultWidth; + gap = parent.defaultHorizontalGap; + edgeFlags = parent.rowEdgeFlags; + } + + /** Create a key with the given top-left coordinate and extract its attributes from + * the XML parser. + * @param res resources associated with the caller's context + * @param parent the row that this key belongs to. The row must already be attached to + * a {@link Keyboard}. + * @param x the x coordinate of the top-left + * @param y the y coordinate of the top-left + * @param parser the XML parser containing the attributes for this key + */ + public Key(Resources res, Row parent, int x, int y, XmlResourceParser parser) { + this(parent); + + this.x = x; + this.y = y; + + TypedArray a = res.obtainAttributes(Xml.asAttributeSet(parser), + com.android.internal.R.styleable.Keyboard); + + width = getDimensionOrFraction(a, + com.android.internal.R.styleable.Keyboard_keyWidth, + keyboard.mDisplayWidth, parent.defaultWidth); + height = getDimensionOrFraction(a, + com.android.internal.R.styleable.Keyboard_keyHeight, + keyboard.mDisplayHeight, parent.defaultHeight); + gap = getDimensionOrFraction(a, + com.android.internal.R.styleable.Keyboard_horizontalGap, + keyboard.mDisplayWidth, parent.defaultHorizontalGap); + a.recycle(); + a = res.obtainAttributes(Xml.asAttributeSet(parser), + com.android.internal.R.styleable.Keyboard_Key); + this.x += gap; + TypedValue codesValue = new TypedValue(); + a.getValue(com.android.internal.R.styleable.Keyboard_Key_codes, + codesValue); + if (codesValue.type == TypedValue.TYPE_INT_DEC + || codesValue.type == TypedValue.TYPE_INT_HEX) { + codes = new int[] { codesValue.data }; + } else if (codesValue.type == TypedValue.TYPE_STRING) { + codes = parseCSV(codesValue.string.toString()); + } + + iconPreview = a.getDrawable(com.android.internal.R.styleable.Keyboard_Key_iconPreview); + if (iconPreview != null) { + iconPreview.setBounds(0, 0, iconPreview.getIntrinsicWidth(), + iconPreview.getIntrinsicHeight()); + } + popupCharacters = a.getText( + com.android.internal.R.styleable.Keyboard_Key_popupCharacters); + popupResId = a.getResourceId( + com.android.internal.R.styleable.Keyboard_Key_popupKeyboard, 0); + repeatable = a.getBoolean( + com.android.internal.R.styleable.Keyboard_Key_isRepeatable, false); + modifier = a.getBoolean( + com.android.internal.R.styleable.Keyboard_Key_isModifier, false); + sticky = a.getBoolean( + com.android.internal.R.styleable.Keyboard_Key_isSticky, false); + edgeFlags = a.getInt(com.android.internal.R.styleable.Keyboard_Key_keyEdgeFlags, 0); + edgeFlags |= parent.rowEdgeFlags; + + icon = a.getDrawable( + com.android.internal.R.styleable.Keyboard_Key_keyIcon); + if (icon != null) { + icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight()); + } + label = a.getText(com.android.internal.R.styleable.Keyboard_Key_keyLabel); + text = a.getText(com.android.internal.R.styleable.Keyboard_Key_keyOutputText); + + if (codes == null && !TextUtils.isEmpty(label)) { + codes = new int[] { label.charAt(0) }; + } + a.recycle(); + } + + /** + * Informs the key that it has been pressed, in case it needs to change its appearance or + * state. + * @see #onReleased(boolean) + */ + public void onPressed() { + pressed = !pressed; + } + + /** + * Changes the pressed state of the key. + * + * <p>Toggled state of the key will be flipped when all the following conditions are + * fulfilled:</p> + * + * <ul> + * <li>This is a sticky key, that is, {@link #sticky} is {@code true}. + * <li>The parameter {@code inside} is {@code true}. + * <li>{@link android.os.Build.VERSION#SDK_INT} is greater than + * {@link android.os.Build.VERSION_CODES#LOLLIPOP_MR1}. + * </ul> + * + * @param inside whether the finger was released inside the key. Works only on Android M and + * later. See the method document for details. + * @see #onPressed() + */ + public void onReleased(boolean inside) { + pressed = !pressed; + if (sticky && inside) { + on = !on; + } + } + + int[] parseCSV(String value) { + int count = 0; + int lastIndex = 0; + if (value.length() > 0) { + count++; + while ((lastIndex = value.indexOf(",", lastIndex + 1)) > 0) { + count++; + } + } + int[] values = new int[count]; + count = 0; + StringTokenizer st = new StringTokenizer(value, ","); + while (st.hasMoreTokens()) { + try { + values[count++] = Integer.parseInt(st.nextToken()); + } catch (NumberFormatException nfe) { + Log.e(TAG, "Error parsing keycodes " + value); + } + } + return values; + } + + /** + * Detects if a point falls inside this key. + * @param x the x-coordinate of the point + * @param y the y-coordinate of the point + * @return whether or not the point falls inside the key. If the key is attached to an edge, + * it will assume that all points between the key and the edge are considered to be inside + * the key. + */ + public boolean isInside(int x, int y) { + boolean leftEdge = (edgeFlags & EDGE_LEFT) > 0; + boolean rightEdge = (edgeFlags & EDGE_RIGHT) > 0; + boolean topEdge = (edgeFlags & EDGE_TOP) > 0; + boolean bottomEdge = (edgeFlags & EDGE_BOTTOM) > 0; + if ((x >= this.x || (leftEdge && x <= this.x + this.width)) + && (x < this.x + this.width || (rightEdge && x >= this.x)) + && (y >= this.y || (topEdge && y <= this.y + this.height)) + && (y < this.y + this.height || (bottomEdge && y >= this.y))) { + return true; + } else { + return false; + } + } + + /** + * Returns the square of the distance between the center of the key and the given point. + * @param x the x-coordinate of the point + * @param y the y-coordinate of the point + * @return the square of the distance of the point from the center of the key + */ + public int squaredDistanceFrom(int x, int y) { + int xDist = this.x + width / 2 - x; + int yDist = this.y + height / 2 - y; + return xDist * xDist + yDist * yDist; + } + + /** + * Returns the drawable state for the key, based on the current state and type of the key. + * @return the drawable state of the key. + * @see android.graphics.drawable.StateListDrawable#setState(int[]) + */ + public int[] getCurrentDrawableState() { + int[] states = KEY_STATE_NORMAL; + + if (on) { + if (pressed) { + states = KEY_STATE_PRESSED_ON; + } else { + states = KEY_STATE_NORMAL_ON; + } + } else { + if (sticky) { + if (pressed) { + states = KEY_STATE_PRESSED_OFF; + } else { + states = KEY_STATE_NORMAL_OFF; + } + } else { + if (pressed) { + states = KEY_STATE_PRESSED; + } + } + } + return states; + } + } + + /** + * Creates a keyboard from the given xml key layout file. + * @param context the application or service context + * @param xmlLayoutResId the resource file that contains the keyboard layout and keys. + */ + public Keyboard(Context context, int xmlLayoutResId) { + this(context, xmlLayoutResId, 0); + } + + /** + * Creates a keyboard from the given xml key layout file. Weeds out rows + * that have a keyboard mode defined but don't match the specified mode. + * @param context the application or service context + * @param xmlLayoutResId the resource file that contains the keyboard layout and keys. + * @param modeId keyboard mode identifier + * @param width sets width of keyboard + * @param height sets height of keyboard + */ + public Keyboard(Context context, @XmlRes int xmlLayoutResId, int modeId, int width, + int height) { + mDisplayWidth = width; + mDisplayHeight = height; + + mDefaultHorizontalGap = 0; + mDefaultWidth = mDisplayWidth / 10; + mDefaultVerticalGap = 0; + mDefaultHeight = mDefaultWidth; + mKeys = new ArrayList<Key>(); + mModifierKeys = new ArrayList<Key>(); + mKeyboardMode = modeId; + loadKeyboard(context, context.getResources().getXml(xmlLayoutResId)); + } + + /** + * Creates a keyboard from the given xml key layout file. Weeds out rows + * that have a keyboard mode defined but don't match the specified mode. + * @param context the application or service context + * @param xmlLayoutResId the resource file that contains the keyboard layout and keys. + * @param modeId keyboard mode identifier + */ + public Keyboard(Context context, @XmlRes int xmlLayoutResId, int modeId) { + DisplayMetrics dm = context.getResources().getDisplayMetrics(); + mDisplayWidth = dm.widthPixels; + mDisplayHeight = dm.heightPixels; + //Log.v(TAG, "keyboard's display metrics:" + dm); + + mDefaultHorizontalGap = 0; + mDefaultWidth = mDisplayWidth / 10; + mDefaultVerticalGap = 0; + mDefaultHeight = mDefaultWidth; + mKeys = new ArrayList<Key>(); + mModifierKeys = new ArrayList<Key>(); + mKeyboardMode = modeId; + loadKeyboard(context, context.getResources().getXml(xmlLayoutResId)); + } + + /** + * <p>Creates a blank keyboard from the given resource file and populates it with the specified + * characters in left-to-right, top-to-bottom fashion, using the specified number of columns. + * </p> + * <p>If the specified number of columns is -1, then the keyboard will fit as many keys as + * possible in each row.</p> + * @param context the application or service context + * @param layoutTemplateResId the layout template file, containing no keys. + * @param characters the list of characters to display on the keyboard. One key will be created + * for each character. + * @param columns the number of columns of keys to display. If this number is greater than the + * number of keys that can fit in a row, it will be ignored. If this number is -1, the + * keyboard will fit as many keys as possible in each row. + */ + public Keyboard(Context context, int layoutTemplateResId, + CharSequence characters, int columns, int horizontalPadding) { + this(context, layoutTemplateResId); + int x = 0; + int y = 0; + int column = 0; + mTotalWidth = 0; + + Row row = new Row(this); + row.defaultHeight = mDefaultHeight; + row.defaultWidth = mDefaultWidth; + row.defaultHorizontalGap = mDefaultHorizontalGap; + row.verticalGap = mDefaultVerticalGap; + row.rowEdgeFlags = EDGE_TOP | EDGE_BOTTOM; + final int maxColumns = columns == -1 ? Integer.MAX_VALUE : columns; + for (int i = 0; i < characters.length(); i++) { + char c = characters.charAt(i); + if (column >= maxColumns + || x + mDefaultWidth + horizontalPadding > mDisplayWidth) { + x = 0; + y += mDefaultVerticalGap + mDefaultHeight; + column = 0; + } + final Key key = new Key(row); + key.x = x; + key.y = y; + key.label = String.valueOf(c); + key.codes = new int[] { c }; + column++; + x += key.width + key.gap; + mKeys.add(key); + row.mKeys.add(key); + if (x > mTotalWidth) { + mTotalWidth = x; + } + } + mTotalHeight = y + mDefaultHeight; + rows.add(row); + } + + final void resize(int newWidth, int newHeight) { + int numRows = rows.size(); + for (int rowIndex = 0; rowIndex < numRows; ++rowIndex) { + Row row = rows.get(rowIndex); + int numKeys = row.mKeys.size(); + int totalGap = 0; + int totalWidth = 0; + for (int keyIndex = 0; keyIndex < numKeys; ++keyIndex) { + Key key = row.mKeys.get(keyIndex); + if (keyIndex > 0) { + totalGap += key.gap; + } + totalWidth += key.width; + } + if (totalGap + totalWidth > newWidth) { + int x = 0; + float scaleFactor = (float)(newWidth - totalGap) / totalWidth; + for (int keyIndex = 0; keyIndex < numKeys; ++keyIndex) { + Key key = row.mKeys.get(keyIndex); + key.width *= scaleFactor; + key.x = x; + x += key.width + key.gap; + } + } + } + mTotalWidth = newWidth; + // TODO: This does not adjust the vertical placement according to the new size. + // The main problem in the previous code was horizontal placement/size, but we should + // also recalculate the vertical sizes/positions when we get this resize call. + } + + public List<Key> getKeys() { + return mKeys; + } + + public List<Key> getModifierKeys() { + return mModifierKeys; + } + + protected int getHorizontalGap() { + return mDefaultHorizontalGap; + } + + protected void setHorizontalGap(int gap) { + mDefaultHorizontalGap = gap; + } + + protected int getVerticalGap() { + return mDefaultVerticalGap; + } + + protected void setVerticalGap(int gap) { + mDefaultVerticalGap = gap; + } + + protected int getKeyHeight() { + return mDefaultHeight; + } + + protected void setKeyHeight(int height) { + mDefaultHeight = height; + } + + protected int getKeyWidth() { + return mDefaultWidth; + } + + protected void setKeyWidth(int width) { + mDefaultWidth = width; + } + + /** + * Returns the total height of the keyboard + * @return the total height of the keyboard + */ + public int getHeight() { + return mTotalHeight; + } + + public int getMinWidth() { + return mTotalWidth; + } + + public boolean setShifted(boolean shiftState) { + for (Key shiftKey : mShiftKeys) { + if (shiftKey != null) { + shiftKey.on = shiftState; + } + } + if (mShifted != shiftState) { + mShifted = shiftState; + return true; + } + return false; + } + + public boolean isShifted() { + return mShifted; + } + + /** + * @hide + */ + public int[] getShiftKeyIndices() { + return mShiftKeyIndices; + } + + public int getShiftKeyIndex() { + return mShiftKeyIndices[0]; + } + + private void computeNearestNeighbors() { + // Round-up so we don't have any pixels outside the grid + mCellWidth = (getMinWidth() + GRID_WIDTH - 1) / GRID_WIDTH; + mCellHeight = (getHeight() + GRID_HEIGHT - 1) / GRID_HEIGHT; + mGridNeighbors = new int[GRID_SIZE][]; + int[] indices = new int[mKeys.size()]; + final int gridWidth = GRID_WIDTH * mCellWidth; + final int gridHeight = GRID_HEIGHT * mCellHeight; + for (int x = 0; x < gridWidth; x += mCellWidth) { + for (int y = 0; y < gridHeight; y += mCellHeight) { + int count = 0; + for (int i = 0; i < mKeys.size(); i++) { + final Key key = mKeys.get(i); + if (key.squaredDistanceFrom(x, y) < mProximityThreshold || + key.squaredDistanceFrom(x + mCellWidth - 1, y) < mProximityThreshold || + key.squaredDistanceFrom(x + mCellWidth - 1, y + mCellHeight - 1) + < mProximityThreshold || + key.squaredDistanceFrom(x, y + mCellHeight - 1) < mProximityThreshold) { + indices[count++] = i; + } + } + int [] cell = new int[count]; + System.arraycopy(indices, 0, cell, 0, count); + mGridNeighbors[(y / mCellHeight) * GRID_WIDTH + (x / mCellWidth)] = cell; + } + } + } + + /** + * Returns the indices of the keys that are closest to the given point. + * @param x the x-coordinate of the point + * @param y the y-coordinate of the point + * @return the array of integer indices for the nearest keys to the given point. If the given + * point is out of range, then an array of size zero is returned. + */ + public int[] getNearestKeys(int x, int y) { + if (mGridNeighbors == null) computeNearestNeighbors(); + if (x >= 0 && x < getMinWidth() && y >= 0 && y < getHeight()) { + int index = (y / mCellHeight) * GRID_WIDTH + (x / mCellWidth); + if (index < GRID_SIZE) { + return mGridNeighbors[index]; + } + } + return new int[0]; + } + + protected Row createRowFromXml(Resources res, XmlResourceParser parser) { + return new Row(res, this, parser); + } + + protected Key createKeyFromXml(Resources res, Row parent, int x, int y, + XmlResourceParser parser) { + return new Key(res, parent, x, y, parser); + } + + private void loadKeyboard(Context context, XmlResourceParser parser) { + boolean inKey = false; + boolean inRow = false; + boolean leftMostKey = false; + int row = 0; + int x = 0; + int y = 0; + Key key = null; + Row currentRow = null; + Resources res = context.getResources(); + boolean skipRow = false; + + try { + int event; + while ((event = parser.next()) != XmlResourceParser.END_DOCUMENT) { + if (event == XmlResourceParser.START_TAG) { + String tag = parser.getName(); + if (TAG_ROW.equals(tag)) { + inRow = true; + x = 0; + currentRow = createRowFromXml(res, parser); + rows.add(currentRow); + skipRow = currentRow.mode != 0 && currentRow.mode != mKeyboardMode; + if (skipRow) { + skipToEndOfRow(parser); + inRow = false; + } + } else if (TAG_KEY.equals(tag)) { + inKey = true; + key = createKeyFromXml(res, currentRow, x, y, parser); + mKeys.add(key); + if (key.codes[0] == KEYCODE_SHIFT) { + // Find available shift key slot and put this shift key in it + for (int i = 0; i < mShiftKeys.length; i++) { + if (mShiftKeys[i] == null) { + mShiftKeys[i] = key; + mShiftKeyIndices[i] = mKeys.size()-1; + break; + } + } + mModifierKeys.add(key); + } else if (key.codes[0] == KEYCODE_ALT) { + mModifierKeys.add(key); + } + currentRow.mKeys.add(key); + } else if (TAG_KEYBOARD.equals(tag)) { + parseKeyboardAttributes(res, parser); + } + } else if (event == XmlResourceParser.END_TAG) { + if (inKey) { + inKey = false; + x += key.gap + key.width; + if (x > mTotalWidth) { + mTotalWidth = x; + } + } else if (inRow) { + inRow = false; + y += currentRow.verticalGap; + y += currentRow.defaultHeight; + row++; + } else { + // TODO: error or extend? + } + } + } + } catch (Exception e) { + Log.e(TAG, "Parse error:" + e); + e.printStackTrace(); + } + mTotalHeight = y - mDefaultVerticalGap; + } + + private void skipToEndOfRow(XmlResourceParser parser) + throws XmlPullParserException, IOException { + int event; + while ((event = parser.next()) != XmlResourceParser.END_DOCUMENT) { + if (event == XmlResourceParser.END_TAG + && parser.getName().equals(TAG_ROW)) { + break; + } + } + } + + private void parseKeyboardAttributes(Resources res, XmlResourceParser parser) { + TypedArray a = res.obtainAttributes(Xml.asAttributeSet(parser), + com.android.internal.R.styleable.Keyboard); + + mDefaultWidth = getDimensionOrFraction(a, + com.android.internal.R.styleable.Keyboard_keyWidth, + mDisplayWidth, mDisplayWidth / 10); + mDefaultHeight = getDimensionOrFraction(a, + com.android.internal.R.styleable.Keyboard_keyHeight, + mDisplayHeight, 50); + mDefaultHorizontalGap = getDimensionOrFraction(a, + com.android.internal.R.styleable.Keyboard_horizontalGap, + mDisplayWidth, 0); + mDefaultVerticalGap = getDimensionOrFraction(a, + com.android.internal.R.styleable.Keyboard_verticalGap, + mDisplayHeight, 0); + mProximityThreshold = (int) (mDefaultWidth * SEARCH_DISTANCE); + mProximityThreshold = mProximityThreshold * mProximityThreshold; // Square it for comparison + a.recycle(); + } + + static int getDimensionOrFraction(TypedArray a, int index, int base, int defValue) { + TypedValue value = a.peekValue(index); + if (value == null) return defValue; + if (value.type == TypedValue.TYPE_DIMENSION) { + return a.getDimensionPixelOffset(index, defValue); + } else if (value.type == TypedValue.TYPE_FRACTION) { + // Round it to avoid values like 47.9999 from getting truncated + return Math.round(a.getFraction(index, base, base, defValue)); + } + return defValue; + } +} diff --git a/android/inputmethodservice/KeyboardView.java b/android/inputmethodservice/KeyboardView.java new file mode 100644 index 00000000..13b9206b --- /dev/null +++ b/android/inputmethodservice/KeyboardView.java @@ -0,0 +1,1559 @@ +/* + * Copyright (C) 2008-2009 Google Inc. + * + * 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.inputmethodservice; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.graphics.Paint.Align; +import android.graphics.Region.Op; +import android.graphics.drawable.Drawable; +import android.inputmethodservice.Keyboard.Key; +import android.media.AudioManager; +import android.os.Handler; +import android.os.Message; +import android.os.UserHandle; +import android.provider.Settings; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.GestureDetector; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup.LayoutParams; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; +import android.widget.PopupWindow; +import android.widget.TextView; + +import com.android.internal.R; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * A view that renders a virtual {@link Keyboard}. It handles rendering of keys and + * detecting key presses and touch movements. + * + * @attr ref android.R.styleable#KeyboardView_keyBackground + * @attr ref android.R.styleable#KeyboardView_keyPreviewLayout + * @attr ref android.R.styleable#KeyboardView_keyPreviewOffset + * @attr ref android.R.styleable#KeyboardView_labelTextSize + * @attr ref android.R.styleable#KeyboardView_keyTextSize + * @attr ref android.R.styleable#KeyboardView_keyTextColor + * @attr ref android.R.styleable#KeyboardView_verticalCorrection + * @attr ref android.R.styleable#KeyboardView_popupLayout + */ +public class KeyboardView extends View implements View.OnClickListener { + + /** + * Listener for virtual keyboard events. + */ + public interface OnKeyboardActionListener { + + /** + * Called when the user presses a key. This is sent before the {@link #onKey} is called. + * For keys that repeat, this is only called once. + * @param primaryCode the unicode of the key being pressed. If the touch is not on a valid + * key, the value will be zero. + */ + void onPress(int primaryCode); + + /** + * Called when the user releases a key. This is sent after the {@link #onKey} is called. + * For keys that repeat, this is only called once. + * @param primaryCode the code of the key that was released + */ + void onRelease(int primaryCode); + + /** + * Send a key press to the listener. + * @param primaryCode this is the key that was pressed + * @param keyCodes the codes for all the possible alternative keys + * with the primary code being the first. If the primary key code is + * a single character such as an alphabet or number or symbol, the alternatives + * will include other characters that may be on the same key or adjacent keys. + * These codes are useful to correct for accidental presses of a key adjacent to + * the intended key. + */ + void onKey(int primaryCode, int[] keyCodes); + + /** + * Sends a sequence of characters to the listener. + * @param text the sequence of characters to be displayed. + */ + void onText(CharSequence text); + + /** + * Called when the user quickly moves the finger from right to left. + */ + void swipeLeft(); + + /** + * Called when the user quickly moves the finger from left to right. + */ + void swipeRight(); + + /** + * Called when the user quickly moves the finger from up to down. + */ + void swipeDown(); + + /** + * Called when the user quickly moves the finger from down to up. + */ + void swipeUp(); + } + + private static final boolean DEBUG = false; + private static final int NOT_A_KEY = -1; + private static final int[] KEY_DELETE = { Keyboard.KEYCODE_DELETE }; + private static final int[] LONG_PRESSABLE_STATE_SET = { R.attr.state_long_pressable }; + + private Keyboard mKeyboard; + private int mCurrentKeyIndex = NOT_A_KEY; + private int mLabelTextSize; + private int mKeyTextSize; + private int mKeyTextColor; + private float mShadowRadius; + private int mShadowColor; + private float mBackgroundDimAmount; + + private TextView mPreviewText; + private PopupWindow mPreviewPopup; + private int mPreviewTextSizeLarge; + private int mPreviewOffset; + private int mPreviewHeight; + // Working variable + private final int[] mCoordinates = new int[2]; + + private PopupWindow mPopupKeyboard; + private View mMiniKeyboardContainer; + private KeyboardView mMiniKeyboard; + private boolean mMiniKeyboardOnScreen; + private View mPopupParent; + private int mMiniKeyboardOffsetX; + private int mMiniKeyboardOffsetY; + private Map<Key,View> mMiniKeyboardCache; + private Key[] mKeys; + + /** Listener for {@link OnKeyboardActionListener}. */ + private OnKeyboardActionListener mKeyboardActionListener; + + private static final int MSG_SHOW_PREVIEW = 1; + private static final int MSG_REMOVE_PREVIEW = 2; + private static final int MSG_REPEAT = 3; + private static final int MSG_LONGPRESS = 4; + + private static final int DELAY_BEFORE_PREVIEW = 0; + private static final int DELAY_AFTER_PREVIEW = 70; + private static final int DEBOUNCE_TIME = 70; + + private int mVerticalCorrection; + private int mProximityThreshold; + + private boolean mPreviewCentered = false; + private boolean mShowPreview = true; + private boolean mShowTouchPoints = true; + private int mPopupPreviewX; + private int mPopupPreviewY; + + private int mLastX; + private int mLastY; + private int mStartX; + private int mStartY; + + private boolean mProximityCorrectOn; + + private Paint mPaint; + private Rect mPadding; + + private long mDownTime; + private long mLastMoveTime; + private int mLastKey; + private int mLastCodeX; + private int mLastCodeY; + private int mCurrentKey = NOT_A_KEY; + private int mDownKey = NOT_A_KEY; + private long mLastKeyTime; + private long mCurrentKeyTime; + private int[] mKeyIndices = new int[12]; + private GestureDetector mGestureDetector; + private int mPopupX; + private int mPopupY; + private int mRepeatKeyIndex = NOT_A_KEY; + private int mPopupLayout; + private boolean mAbortKey; + private Key mInvalidatedKey; + private Rect mClipRegion = new Rect(0, 0, 0, 0); + private boolean mPossiblePoly; + private SwipeTracker mSwipeTracker = new SwipeTracker(); + private int mSwipeThreshold; + private boolean mDisambiguateSwipe; + + // Variables for dealing with multiple pointers + private int mOldPointerCount = 1; + private float mOldPointerX; + private float mOldPointerY; + + private Drawable mKeyBackground; + + private static final int REPEAT_INTERVAL = 50; // ~20 keys per second + private static final int REPEAT_START_DELAY = 400; + private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout(); + + private static int MAX_NEARBY_KEYS = 12; + private int[] mDistances = new int[MAX_NEARBY_KEYS]; + + // For multi-tap + private int mLastSentIndex; + private int mTapCount; + private long mLastTapTime; + private boolean mInMultiTap; + private static final int MULTITAP_INTERVAL = 800; // milliseconds + private StringBuilder mPreviewLabel = new StringBuilder(1); + + /** Whether the keyboard bitmap needs to be redrawn before it's blitted. **/ + private boolean mDrawPending; + /** The dirty region in the keyboard bitmap */ + private Rect mDirtyRect = new Rect(); + /** The keyboard bitmap for faster updates */ + private Bitmap mBuffer; + /** Notes if the keyboard just changed, so that we could possibly reallocate the mBuffer. */ + private boolean mKeyboardChanged; + /** The canvas for the above mutable keyboard bitmap */ + private Canvas mCanvas; + /** The accessibility manager for accessibility support */ + private AccessibilityManager mAccessibilityManager; + /** The audio manager for accessibility support */ + private AudioManager mAudioManager; + /** Whether the requirement of a headset to hear passwords if accessibility is enabled is announced. */ + private boolean mHeadsetRequiredToHearPasswordsAnnounced; + + Handler mHandler; + + public KeyboardView(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.keyboardViewStyle); + } + + public KeyboardView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public KeyboardView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + TypedArray a = context.obtainStyledAttributes( + attrs, android.R.styleable.KeyboardView, defStyleAttr, defStyleRes); + + LayoutInflater inflate = + (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + + int previewLayout = 0; + int keyTextSize = 0; + + int n = a.getIndexCount(); + + for (int i = 0; i < n; i++) { + int attr = a.getIndex(i); + + switch (attr) { + case com.android.internal.R.styleable.KeyboardView_keyBackground: + mKeyBackground = a.getDrawable(attr); + break; + case com.android.internal.R.styleable.KeyboardView_verticalCorrection: + mVerticalCorrection = a.getDimensionPixelOffset(attr, 0); + break; + case com.android.internal.R.styleable.KeyboardView_keyPreviewLayout: + previewLayout = a.getResourceId(attr, 0); + break; + case com.android.internal.R.styleable.KeyboardView_keyPreviewOffset: + mPreviewOffset = a.getDimensionPixelOffset(attr, 0); + break; + case com.android.internal.R.styleable.KeyboardView_keyPreviewHeight: + mPreviewHeight = a.getDimensionPixelSize(attr, 80); + break; + case com.android.internal.R.styleable.KeyboardView_keyTextSize: + mKeyTextSize = a.getDimensionPixelSize(attr, 18); + break; + case com.android.internal.R.styleable.KeyboardView_keyTextColor: + mKeyTextColor = a.getColor(attr, 0xFF000000); + break; + case com.android.internal.R.styleable.KeyboardView_labelTextSize: + mLabelTextSize = a.getDimensionPixelSize(attr, 14); + break; + case com.android.internal.R.styleable.KeyboardView_popupLayout: + mPopupLayout = a.getResourceId(attr, 0); + break; + case com.android.internal.R.styleable.KeyboardView_shadowColor: + mShadowColor = a.getColor(attr, 0); + break; + case com.android.internal.R.styleable.KeyboardView_shadowRadius: + mShadowRadius = a.getFloat(attr, 0f); + break; + } + } + + a = mContext.obtainStyledAttributes( + com.android.internal.R.styleable.Theme); + mBackgroundDimAmount = a.getFloat(android.R.styleable.Theme_backgroundDimAmount, 0.5f); + + mPreviewPopup = new PopupWindow(context); + if (previewLayout != 0) { + mPreviewText = (TextView) inflate.inflate(previewLayout, null); + mPreviewTextSizeLarge = (int) mPreviewText.getTextSize(); + mPreviewPopup.setContentView(mPreviewText); + mPreviewPopup.setBackgroundDrawable(null); + } else { + mShowPreview = false; + } + + mPreviewPopup.setTouchable(false); + + mPopupKeyboard = new PopupWindow(context); + mPopupKeyboard.setBackgroundDrawable(null); + //mPopupKeyboard.setClippingEnabled(false); + + mPopupParent = this; + //mPredicting = true; + + mPaint = new Paint(); + mPaint.setAntiAlias(true); + mPaint.setTextSize(keyTextSize); + mPaint.setTextAlign(Align.CENTER); + mPaint.setAlpha(255); + + mPadding = new Rect(0, 0, 0, 0); + mMiniKeyboardCache = new HashMap<Key,View>(); + mKeyBackground.getPadding(mPadding); + + mSwipeThreshold = (int) (500 * getResources().getDisplayMetrics().density); + mDisambiguateSwipe = getResources().getBoolean( + com.android.internal.R.bool.config_swipeDisambiguation); + + mAccessibilityManager = AccessibilityManager.getInstance(context); + mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + + resetMultiTap(); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + initGestureDetector(); + if (mHandler == null) { + mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_SHOW_PREVIEW: + showKey(msg.arg1); + break; + case MSG_REMOVE_PREVIEW: + mPreviewText.setVisibility(INVISIBLE); + break; + case MSG_REPEAT: + if (repeatKey()) { + Message repeat = Message.obtain(this, MSG_REPEAT); + sendMessageDelayed(repeat, REPEAT_INTERVAL); + } + break; + case MSG_LONGPRESS: + openPopupIfRequired((MotionEvent) msg.obj); + break; + } + } + }; + } + } + + private void initGestureDetector() { + if (mGestureDetector == null) { + mGestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() { + @Override + public boolean onFling(MotionEvent me1, MotionEvent me2, + float velocityX, float velocityY) { + if (mPossiblePoly) return false; + final float absX = Math.abs(velocityX); + final float absY = Math.abs(velocityY); + float deltaX = me2.getX() - me1.getX(); + float deltaY = me2.getY() - me1.getY(); + int travelX = getWidth() / 2; // Half the keyboard width + int travelY = getHeight() / 2; // Half the keyboard height + mSwipeTracker.computeCurrentVelocity(1000); + final float endingVelocityX = mSwipeTracker.getXVelocity(); + final float endingVelocityY = mSwipeTracker.getYVelocity(); + boolean sendDownKey = false; + if (velocityX > mSwipeThreshold && absY < absX && deltaX > travelX) { + if (mDisambiguateSwipe && endingVelocityX < velocityX / 4) { + sendDownKey = true; + } else { + swipeRight(); + return true; + } + } else if (velocityX < -mSwipeThreshold && absY < absX && deltaX < -travelX) { + if (mDisambiguateSwipe && endingVelocityX > velocityX / 4) { + sendDownKey = true; + } else { + swipeLeft(); + return true; + } + } else if (velocityY < -mSwipeThreshold && absX < absY && deltaY < -travelY) { + if (mDisambiguateSwipe && endingVelocityY > velocityY / 4) { + sendDownKey = true; + } else { + swipeUp(); + return true; + } + } else if (velocityY > mSwipeThreshold && absX < absY / 2 && deltaY > travelY) { + if (mDisambiguateSwipe && endingVelocityY < velocityY / 4) { + sendDownKey = true; + } else { + swipeDown(); + return true; + } + } + + if (sendDownKey) { + detectAndSendKey(mDownKey, mStartX, mStartY, me1.getEventTime()); + } + return false; + } + }); + + mGestureDetector.setIsLongpressEnabled(false); + } + } + + public void setOnKeyboardActionListener(OnKeyboardActionListener listener) { + mKeyboardActionListener = listener; + } + + /** + * Returns the {@link OnKeyboardActionListener} object. + * @return the listener attached to this keyboard + */ + protected OnKeyboardActionListener getOnKeyboardActionListener() { + return mKeyboardActionListener; + } + + /** + * Attaches a keyboard to this view. The keyboard can be switched at any time and the + * view will re-layout itself to accommodate the keyboard. + * @see Keyboard + * @see #getKeyboard() + * @param keyboard the keyboard to display in this view + */ + public void setKeyboard(Keyboard keyboard) { + if (mKeyboard != null) { + showPreview(NOT_A_KEY); + } + // Remove any pending messages + removeMessages(); + mKeyboard = keyboard; + List<Key> keys = mKeyboard.getKeys(); + mKeys = keys.toArray(new Key[keys.size()]); + requestLayout(); + // Hint to reallocate the buffer if the size changed + mKeyboardChanged = true; + invalidateAllKeys(); + computeProximityThreshold(keyboard); + mMiniKeyboardCache.clear(); // Not really necessary to do every time, but will free up views + // Switching to a different keyboard should abort any pending keys so that the key up + // doesn't get delivered to the old or new keyboard + mAbortKey = true; // Until the next ACTION_DOWN + } + + /** + * Returns the current keyboard being displayed by this view. + * @return the currently attached keyboard + * @see #setKeyboard(Keyboard) + */ + public Keyboard getKeyboard() { + return mKeyboard; + } + + /** + * Sets the state of the shift key of the keyboard, if any. + * @param shifted whether or not to enable the state of the shift key + * @return true if the shift key state changed, false if there was no change + * @see KeyboardView#isShifted() + */ + public boolean setShifted(boolean shifted) { + if (mKeyboard != null) { + if (mKeyboard.setShifted(shifted)) { + // The whole keyboard probably needs to be redrawn + invalidateAllKeys(); + return true; + } + } + return false; + } + + /** + * Returns the state of the shift key of the keyboard, if any. + * @return true if the shift is in a pressed state, false otherwise. If there is + * no shift key on the keyboard or there is no keyboard attached, it returns false. + * @see KeyboardView#setShifted(boolean) + */ + public boolean isShifted() { + if (mKeyboard != null) { + return mKeyboard.isShifted(); + } + return false; + } + + /** + * Enables or disables the key feedback popup. This is a popup that shows a magnified + * version of the depressed key. By default the preview is enabled. + * @param previewEnabled whether or not to enable the key feedback popup + * @see #isPreviewEnabled() + */ + public void setPreviewEnabled(boolean previewEnabled) { + mShowPreview = previewEnabled; + } + + /** + * Returns the enabled state of the key feedback popup. + * @return whether or not the key feedback popup is enabled + * @see #setPreviewEnabled(boolean) + */ + public boolean isPreviewEnabled() { + return mShowPreview; + } + + public void setVerticalCorrection(int verticalOffset) { + + } + public void setPopupParent(View v) { + mPopupParent = v; + } + + public void setPopupOffset(int x, int y) { + mMiniKeyboardOffsetX = x; + mMiniKeyboardOffsetY = y; + if (mPreviewPopup.isShowing()) { + mPreviewPopup.dismiss(); + } + } + + /** + * When enabled, calls to {@link OnKeyboardActionListener#onKey} will include key + * codes for adjacent keys. When disabled, only the primary key code will be + * reported. + * @param enabled whether or not the proximity correction is enabled + */ + public void setProximityCorrectionEnabled(boolean enabled) { + mProximityCorrectOn = enabled; + } + + /** + * Returns true if proximity correction is enabled. + */ + public boolean isProximityCorrectionEnabled() { + return mProximityCorrectOn; + } + + /** + * Popup keyboard close button clicked. + * @hide + */ + public void onClick(View v) { + dismissPopupKeyboard(); + } + + private CharSequence adjustCase(CharSequence label) { + if (mKeyboard.isShifted() && label != null && label.length() < 3 + && Character.isLowerCase(label.charAt(0))) { + label = label.toString().toUpperCase(); + } + return label; + } + + @Override + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // Round up a little + if (mKeyboard == null) { + setMeasuredDimension(mPaddingLeft + mPaddingRight, mPaddingTop + mPaddingBottom); + } else { + int width = mKeyboard.getMinWidth() + mPaddingLeft + mPaddingRight; + if (MeasureSpec.getSize(widthMeasureSpec) < width + 10) { + width = MeasureSpec.getSize(widthMeasureSpec); + } + setMeasuredDimension(width, mKeyboard.getHeight() + mPaddingTop + mPaddingBottom); + } + } + + /** + * Compute the average distance between adjacent keys (horizontally and vertically) + * and square it to get the proximity threshold. We use a square here and in computing + * the touch distance from a key's center to avoid taking a square root. + * @param keyboard + */ + private void computeProximityThreshold(Keyboard keyboard) { + if (keyboard == null) return; + final Key[] keys = mKeys; + if (keys == null) return; + int length = keys.length; + int dimensionSum = 0; + for (int i = 0; i < length; i++) { + Key key = keys[i]; + dimensionSum += Math.min(key.width, key.height) + key.gap; + } + if (dimensionSum < 0 || length == 0) return; + mProximityThreshold = (int) (dimensionSum * 1.4f / length); + mProximityThreshold *= mProximityThreshold; // Square it + } + + @Override + public void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + if (mKeyboard != null) { + mKeyboard.resize(w, h); + } + // Release the buffer, if any and it will be reallocated on the next draw + mBuffer = null; + } + + @Override + public void onDraw(Canvas canvas) { + super.onDraw(canvas); + if (mDrawPending || mBuffer == null || mKeyboardChanged) { + onBufferDraw(); + } + canvas.drawBitmap(mBuffer, 0, 0, null); + } + + private void onBufferDraw() { + if (mBuffer == null || mKeyboardChanged) { + if (mBuffer == null || mKeyboardChanged && + (mBuffer.getWidth() != getWidth() || mBuffer.getHeight() != getHeight())) { + // Make sure our bitmap is at least 1x1 + final int width = Math.max(1, getWidth()); + final int height = Math.max(1, getHeight()); + mBuffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + mCanvas = new Canvas(mBuffer); + } + invalidateAllKeys(); + mKeyboardChanged = false; + } + final Canvas canvas = mCanvas; + canvas.clipRect(mDirtyRect, Op.REPLACE); + + if (mKeyboard == null) return; + + final Paint paint = mPaint; + final Drawable keyBackground = mKeyBackground; + final Rect clipRegion = mClipRegion; + final Rect padding = mPadding; + final int kbdPaddingLeft = mPaddingLeft; + final int kbdPaddingTop = mPaddingTop; + final Key[] keys = mKeys; + final Key invalidKey = mInvalidatedKey; + + paint.setColor(mKeyTextColor); + boolean drawSingleKey = false; + if (invalidKey != null && canvas.getClipBounds(clipRegion)) { + // Is clipRegion completely contained within the invalidated key? + if (invalidKey.x + kbdPaddingLeft - 1 <= clipRegion.left && + invalidKey.y + kbdPaddingTop - 1 <= clipRegion.top && + invalidKey.x + invalidKey.width + kbdPaddingLeft + 1 >= clipRegion.right && + invalidKey.y + invalidKey.height + kbdPaddingTop + 1 >= clipRegion.bottom) { + drawSingleKey = true; + } + } + canvas.drawColor(0x00000000, PorterDuff.Mode.CLEAR); + final int keyCount = keys.length; + for (int i = 0; i < keyCount; i++) { + final Key key = keys[i]; + if (drawSingleKey && invalidKey != key) { + continue; + } + int[] drawableState = key.getCurrentDrawableState(); + keyBackground.setState(drawableState); + + // Switch the character to uppercase if shift is pressed + String label = key.label == null? null : adjustCase(key.label).toString(); + + final Rect bounds = keyBackground.getBounds(); + if (key.width != bounds.right || + key.height != bounds.bottom) { + keyBackground.setBounds(0, 0, key.width, key.height); + } + canvas.translate(key.x + kbdPaddingLeft, key.y + kbdPaddingTop); + keyBackground.draw(canvas); + + if (label != null) { + // For characters, use large font. For labels like "Done", use small font. + if (label.length() > 1 && key.codes.length < 2) { + paint.setTextSize(mLabelTextSize); + paint.setTypeface(Typeface.DEFAULT_BOLD); + } else { + paint.setTextSize(mKeyTextSize); + paint.setTypeface(Typeface.DEFAULT); + } + // Draw a drop shadow for the text + paint.setShadowLayer(mShadowRadius, 0, 0, mShadowColor); + // Draw the text + canvas.drawText(label, + (key.width - padding.left - padding.right) / 2 + + padding.left, + (key.height - padding.top - padding.bottom) / 2 + + (paint.getTextSize() - paint.descent()) / 2 + padding.top, + paint); + // Turn off drop shadow + paint.setShadowLayer(0, 0, 0, 0); + } else if (key.icon != null) { + final int drawableX = (key.width - padding.left - padding.right + - key.icon.getIntrinsicWidth()) / 2 + padding.left; + final int drawableY = (key.height - padding.top - padding.bottom + - key.icon.getIntrinsicHeight()) / 2 + padding.top; + canvas.translate(drawableX, drawableY); + key.icon.setBounds(0, 0, + key.icon.getIntrinsicWidth(), key.icon.getIntrinsicHeight()); + key.icon.draw(canvas); + canvas.translate(-drawableX, -drawableY); + } + canvas.translate(-key.x - kbdPaddingLeft, -key.y - kbdPaddingTop); + } + mInvalidatedKey = null; + // Overlay a dark rectangle to dim the keyboard + if (mMiniKeyboardOnScreen) { + paint.setColor((int) (mBackgroundDimAmount * 0xFF) << 24); + canvas.drawRect(0, 0, getWidth(), getHeight(), paint); + } + + if (DEBUG && mShowTouchPoints) { + paint.setAlpha(128); + paint.setColor(0xFFFF0000); + canvas.drawCircle(mStartX, mStartY, 3, paint); + canvas.drawLine(mStartX, mStartY, mLastX, mLastY, paint); + paint.setColor(0xFF0000FF); + canvas.drawCircle(mLastX, mLastY, 3, paint); + paint.setColor(0xFF00FF00); + canvas.drawCircle((mStartX + mLastX) / 2, (mStartY + mLastY) / 2, 2, paint); + } + + mDrawPending = false; + mDirtyRect.setEmpty(); + } + + private int getKeyIndices(int x, int y, int[] allKeys) { + final Key[] keys = mKeys; + int primaryIndex = NOT_A_KEY; + int closestKey = NOT_A_KEY; + int closestKeyDist = mProximityThreshold + 1; + java.util.Arrays.fill(mDistances, Integer.MAX_VALUE); + int [] nearestKeyIndices = mKeyboard.getNearestKeys(x, y); + final int keyCount = nearestKeyIndices.length; + for (int i = 0; i < keyCount; i++) { + final Key key = keys[nearestKeyIndices[i]]; + int dist = 0; + boolean isInside = key.isInside(x,y); + if (isInside) { + primaryIndex = nearestKeyIndices[i]; + } + + if (((mProximityCorrectOn + && (dist = key.squaredDistanceFrom(x, y)) < mProximityThreshold) + || isInside) + && key.codes[0] > 32) { + // Find insertion point + final int nCodes = key.codes.length; + if (dist < closestKeyDist) { + closestKeyDist = dist; + closestKey = nearestKeyIndices[i]; + } + + if (allKeys == null) continue; + + for (int j = 0; j < mDistances.length; j++) { + if (mDistances[j] > dist) { + // Make space for nCodes codes + System.arraycopy(mDistances, j, mDistances, j + nCodes, + mDistances.length - j - nCodes); + System.arraycopy(allKeys, j, allKeys, j + nCodes, + allKeys.length - j - nCodes); + for (int c = 0; c < nCodes; c++) { + allKeys[j + c] = key.codes[c]; + mDistances[j + c] = dist; + } + break; + } + } + } + } + if (primaryIndex == NOT_A_KEY) { + primaryIndex = closestKey; + } + return primaryIndex; + } + + private void detectAndSendKey(int index, int x, int y, long eventTime) { + if (index != NOT_A_KEY && index < mKeys.length) { + final Key key = mKeys[index]; + if (key.text != null) { + mKeyboardActionListener.onText(key.text); + mKeyboardActionListener.onRelease(NOT_A_KEY); + } else { + int code = key.codes[0]; + //TextEntryState.keyPressedAt(key, x, y); + int[] codes = new int[MAX_NEARBY_KEYS]; + Arrays.fill(codes, NOT_A_KEY); + getKeyIndices(x, y, codes); + // Multi-tap + if (mInMultiTap) { + if (mTapCount != -1) { + mKeyboardActionListener.onKey(Keyboard.KEYCODE_DELETE, KEY_DELETE); + } else { + mTapCount = 0; + } + code = key.codes[mTapCount]; + } + mKeyboardActionListener.onKey(code, codes); + mKeyboardActionListener.onRelease(code); + } + mLastSentIndex = index; + mLastTapTime = eventTime; + } + } + + /** + * Handle multi-tap keys by producing the key label for the current multi-tap state. + */ + private CharSequence getPreviewText(Key key) { + if (mInMultiTap) { + // Multi-tap + mPreviewLabel.setLength(0); + mPreviewLabel.append((char) key.codes[mTapCount < 0 ? 0 : mTapCount]); + return adjustCase(mPreviewLabel); + } else { + return adjustCase(key.label); + } + } + + private void showPreview(int keyIndex) { + int oldKeyIndex = mCurrentKeyIndex; + final PopupWindow previewPopup = mPreviewPopup; + + mCurrentKeyIndex = keyIndex; + // Release the old key and press the new key + final Key[] keys = mKeys; + if (oldKeyIndex != mCurrentKeyIndex) { + if (oldKeyIndex != NOT_A_KEY && keys.length > oldKeyIndex) { + Key oldKey = keys[oldKeyIndex]; + oldKey.onReleased(mCurrentKeyIndex == NOT_A_KEY); + invalidateKey(oldKeyIndex); + final int keyCode = oldKey.codes[0]; + sendAccessibilityEventForUnicodeCharacter(AccessibilityEvent.TYPE_VIEW_HOVER_EXIT, + keyCode); + // TODO: We need to implement AccessibilityNodeProvider for this view. + sendAccessibilityEventForUnicodeCharacter( + AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED, keyCode); + } + if (mCurrentKeyIndex != NOT_A_KEY && keys.length > mCurrentKeyIndex) { + Key newKey = keys[mCurrentKeyIndex]; + newKey.onPressed(); + invalidateKey(mCurrentKeyIndex); + final int keyCode = newKey.codes[0]; + sendAccessibilityEventForUnicodeCharacter(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER, + keyCode); + // TODO: We need to implement AccessibilityNodeProvider for this view. + sendAccessibilityEventForUnicodeCharacter( + AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED, keyCode); + } + } + // If key changed and preview is on ... + if (oldKeyIndex != mCurrentKeyIndex && mShowPreview) { + mHandler.removeMessages(MSG_SHOW_PREVIEW); + if (previewPopup.isShowing()) { + if (keyIndex == NOT_A_KEY) { + mHandler.sendMessageDelayed(mHandler + .obtainMessage(MSG_REMOVE_PREVIEW), + DELAY_AFTER_PREVIEW); + } + } + if (keyIndex != NOT_A_KEY) { + if (previewPopup.isShowing() && mPreviewText.getVisibility() == VISIBLE) { + // Show right away, if it's already visible and finger is moving around + showKey(keyIndex); + } else { + mHandler.sendMessageDelayed( + mHandler.obtainMessage(MSG_SHOW_PREVIEW, keyIndex, 0), + DELAY_BEFORE_PREVIEW); + } + } + } + } + + private void showKey(final int keyIndex) { + final PopupWindow previewPopup = mPreviewPopup; + final Key[] keys = mKeys; + if (keyIndex < 0 || keyIndex >= mKeys.length) return; + Key key = keys[keyIndex]; + if (key.icon != null) { + mPreviewText.setCompoundDrawables(null, null, null, + key.iconPreview != null ? key.iconPreview : key.icon); + mPreviewText.setText(null); + } else { + mPreviewText.setCompoundDrawables(null, null, null, null); + mPreviewText.setText(getPreviewText(key)); + if (key.label.length() > 1 && key.codes.length < 2) { + mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mKeyTextSize); + mPreviewText.setTypeface(Typeface.DEFAULT_BOLD); + } else { + mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mPreviewTextSizeLarge); + mPreviewText.setTypeface(Typeface.DEFAULT); + } + } + mPreviewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + int popupWidth = Math.max(mPreviewText.getMeasuredWidth(), key.width + + mPreviewText.getPaddingLeft() + mPreviewText.getPaddingRight()); + final int popupHeight = mPreviewHeight; + LayoutParams lp = mPreviewText.getLayoutParams(); + if (lp != null) { + lp.width = popupWidth; + lp.height = popupHeight; + } + if (!mPreviewCentered) { + mPopupPreviewX = key.x - mPreviewText.getPaddingLeft() + mPaddingLeft; + mPopupPreviewY = key.y - popupHeight + mPreviewOffset; + } else { + // TODO: Fix this if centering is brought back + mPopupPreviewX = 160 - mPreviewText.getMeasuredWidth() / 2; + mPopupPreviewY = - mPreviewText.getMeasuredHeight(); + } + mHandler.removeMessages(MSG_REMOVE_PREVIEW); + getLocationInWindow(mCoordinates); + mCoordinates[0] += mMiniKeyboardOffsetX; // Offset may be zero + mCoordinates[1] += mMiniKeyboardOffsetY; // Offset may be zero + + // Set the preview background state + mPreviewText.getBackground().setState( + key.popupResId != 0 ? LONG_PRESSABLE_STATE_SET : EMPTY_STATE_SET); + mPopupPreviewX += mCoordinates[0]; + mPopupPreviewY += mCoordinates[1]; + + // If the popup cannot be shown above the key, put it on the side + getLocationOnScreen(mCoordinates); + if (mPopupPreviewY + mCoordinates[1] < 0) { + // If the key you're pressing is on the left side of the keyboard, show the popup on + // the right, offset by enough to see at least one key to the left/right. + if (key.x + key.width <= getWidth() / 2) { + mPopupPreviewX += (int) (key.width * 2.5); + } else { + mPopupPreviewX -= (int) (key.width * 2.5); + } + mPopupPreviewY += popupHeight; + } + + if (previewPopup.isShowing()) { + previewPopup.update(mPopupPreviewX, mPopupPreviewY, + popupWidth, popupHeight); + } else { + previewPopup.setWidth(popupWidth); + previewPopup.setHeight(popupHeight); + previewPopup.showAtLocation(mPopupParent, Gravity.NO_GRAVITY, + mPopupPreviewX, mPopupPreviewY); + } + mPreviewText.setVisibility(VISIBLE); + } + + private void sendAccessibilityEventForUnicodeCharacter(int eventType, int code) { + if (mAccessibilityManager.isEnabled()) { + AccessibilityEvent event = AccessibilityEvent.obtain(eventType); + onInitializeAccessibilityEvent(event); + final String text; + switch (code) { + case Keyboard.KEYCODE_ALT: + text = mContext.getString(R.string.keyboardview_keycode_alt); + break; + case Keyboard.KEYCODE_CANCEL: + text = mContext.getString(R.string.keyboardview_keycode_cancel); + break; + case Keyboard.KEYCODE_DELETE: + text = mContext.getString(R.string.keyboardview_keycode_delete); + break; + case Keyboard.KEYCODE_DONE: + text = mContext.getString(R.string.keyboardview_keycode_done); + break; + case Keyboard.KEYCODE_MODE_CHANGE: + text = mContext.getString(R.string.keyboardview_keycode_mode_change); + break; + case Keyboard.KEYCODE_SHIFT: + text = mContext.getString(R.string.keyboardview_keycode_shift); + break; + case '\n': + text = mContext.getString(R.string.keyboardview_keycode_enter); + break; + default: + text = String.valueOf((char) code); + } + event.getText().add(text); + mAccessibilityManager.sendAccessibilityEvent(event); + } + } + + /** + * Requests a redraw of the entire keyboard. Calling {@link #invalidate} is not sufficient + * because the keyboard renders the keys to an off-screen buffer and an invalidate() only + * draws the cached buffer. + * @see #invalidateKey(int) + */ + public void invalidateAllKeys() { + mDirtyRect.union(0, 0, getWidth(), getHeight()); + mDrawPending = true; + invalidate(); + } + + /** + * Invalidates a key so that it will be redrawn on the next repaint. Use this method if only + * one key is changing it's content. Any changes that affect the position or size of the key + * may not be honored. + * @param keyIndex the index of the key in the attached {@link Keyboard}. + * @see #invalidateAllKeys + */ + public void invalidateKey(int keyIndex) { + if (mKeys == null) return; + if (keyIndex < 0 || keyIndex >= mKeys.length) { + return; + } + final Key key = mKeys[keyIndex]; + mInvalidatedKey = key; + mDirtyRect.union(key.x + mPaddingLeft, key.y + mPaddingTop, + key.x + key.width + mPaddingLeft, key.y + key.height + mPaddingTop); + onBufferDraw(); + invalidate(key.x + mPaddingLeft, key.y + mPaddingTop, + key.x + key.width + mPaddingLeft, key.y + key.height + mPaddingTop); + } + + private boolean openPopupIfRequired(MotionEvent me) { + // Check if we have a popup layout specified first. + if (mPopupLayout == 0) { + return false; + } + if (mCurrentKey < 0 || mCurrentKey >= mKeys.length) { + return false; + } + + Key popupKey = mKeys[mCurrentKey]; + boolean result = onLongPress(popupKey); + if (result) { + mAbortKey = true; + showPreview(NOT_A_KEY); + } + return result; + } + + /** + * Called when a key is long pressed. By default this will open any popup keyboard associated + * with this key through the attributes popupLayout and popupCharacters. + * @param popupKey the key that was long pressed + * @return true if the long press is handled, false otherwise. Subclasses should call the + * method on the base class if the subclass doesn't wish to handle the call. + */ + protected boolean onLongPress(Key popupKey) { + int popupKeyboardId = popupKey.popupResId; + + if (popupKeyboardId != 0) { + mMiniKeyboardContainer = mMiniKeyboardCache.get(popupKey); + if (mMiniKeyboardContainer == null) { + LayoutInflater inflater = (LayoutInflater) getContext().getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + mMiniKeyboardContainer = inflater.inflate(mPopupLayout, null); + mMiniKeyboard = (KeyboardView) mMiniKeyboardContainer.findViewById( + com.android.internal.R.id.keyboardView); + View closeButton = mMiniKeyboardContainer.findViewById( + com.android.internal.R.id.closeButton); + if (closeButton != null) closeButton.setOnClickListener(this); + mMiniKeyboard.setOnKeyboardActionListener(new OnKeyboardActionListener() { + public void onKey(int primaryCode, int[] keyCodes) { + mKeyboardActionListener.onKey(primaryCode, keyCodes); + dismissPopupKeyboard(); + } + + public void onText(CharSequence text) { + mKeyboardActionListener.onText(text); + dismissPopupKeyboard(); + } + + public void swipeLeft() { } + public void swipeRight() { } + public void swipeUp() { } + public void swipeDown() { } + public void onPress(int primaryCode) { + mKeyboardActionListener.onPress(primaryCode); + } + public void onRelease(int primaryCode) { + mKeyboardActionListener.onRelease(primaryCode); + } + }); + //mInputView.setSuggest(mSuggest); + Keyboard keyboard; + if (popupKey.popupCharacters != null) { + keyboard = new Keyboard(getContext(), popupKeyboardId, + popupKey.popupCharacters, -1, getPaddingLeft() + getPaddingRight()); + } else { + keyboard = new Keyboard(getContext(), popupKeyboardId); + } + mMiniKeyboard.setKeyboard(keyboard); + mMiniKeyboard.setPopupParent(this); + mMiniKeyboardContainer.measure( + MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST), + MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.AT_MOST)); + + mMiniKeyboardCache.put(popupKey, mMiniKeyboardContainer); + } else { + mMiniKeyboard = (KeyboardView) mMiniKeyboardContainer.findViewById( + com.android.internal.R.id.keyboardView); + } + getLocationInWindow(mCoordinates); + mPopupX = popupKey.x + mPaddingLeft; + mPopupY = popupKey.y + mPaddingTop; + mPopupX = mPopupX + popupKey.width - mMiniKeyboardContainer.getMeasuredWidth(); + mPopupY = mPopupY - mMiniKeyboardContainer.getMeasuredHeight(); + final int x = mPopupX + mMiniKeyboardContainer.getPaddingRight() + mCoordinates[0]; + final int y = mPopupY + mMiniKeyboardContainer.getPaddingBottom() + mCoordinates[1]; + mMiniKeyboard.setPopupOffset(x < 0 ? 0 : x, y); + mMiniKeyboard.setShifted(isShifted()); + mPopupKeyboard.setContentView(mMiniKeyboardContainer); + mPopupKeyboard.setWidth(mMiniKeyboardContainer.getMeasuredWidth()); + mPopupKeyboard.setHeight(mMiniKeyboardContainer.getMeasuredHeight()); + mPopupKeyboard.showAtLocation(this, Gravity.NO_GRAVITY, x, y); + mMiniKeyboardOnScreen = true; + //mMiniKeyboard.onTouchEvent(getTranslatedEvent(me)); + invalidateAllKeys(); + return true; + } + return false; + } + + @Override + public boolean onHoverEvent(MotionEvent event) { + if (mAccessibilityManager.isTouchExplorationEnabled() && event.getPointerCount() == 1) { + final int action = event.getAction(); + switch (action) { + case MotionEvent.ACTION_HOVER_ENTER: { + event.setAction(MotionEvent.ACTION_DOWN); + } break; + case MotionEvent.ACTION_HOVER_MOVE: { + event.setAction(MotionEvent.ACTION_MOVE); + } break; + case MotionEvent.ACTION_HOVER_EXIT: { + event.setAction(MotionEvent.ACTION_UP); + } break; + } + return onTouchEvent(event); + } + return true; + } + + @Override + public boolean onTouchEvent(MotionEvent me) { + // Convert multi-pointer up/down events to single up/down events to + // deal with the typical multi-pointer behavior of two-thumb typing + final int pointerCount = me.getPointerCount(); + final int action = me.getAction(); + boolean result = false; + final long now = me.getEventTime(); + + if (pointerCount != mOldPointerCount) { + if (pointerCount == 1) { + // Send a down event for the latest pointer + MotionEvent down = MotionEvent.obtain(now, now, MotionEvent.ACTION_DOWN, + me.getX(), me.getY(), me.getMetaState()); + result = onModifiedTouchEvent(down, false); + down.recycle(); + // If it's an up action, then deliver the up as well. + if (action == MotionEvent.ACTION_UP) { + result = onModifiedTouchEvent(me, true); + } + } else { + // Send an up event for the last pointer + MotionEvent up = MotionEvent.obtain(now, now, MotionEvent.ACTION_UP, + mOldPointerX, mOldPointerY, me.getMetaState()); + result = onModifiedTouchEvent(up, true); + up.recycle(); + } + } else { + if (pointerCount == 1) { + result = onModifiedTouchEvent(me, false); + mOldPointerX = me.getX(); + mOldPointerY = me.getY(); + } else { + // Don't do anything when 2 pointers are down and moving. + result = true; + } + } + mOldPointerCount = pointerCount; + + return result; + } + + private boolean onModifiedTouchEvent(MotionEvent me, boolean possiblePoly) { + int touchX = (int) me.getX() - mPaddingLeft; + int touchY = (int) me.getY() - mPaddingTop; + if (touchY >= -mVerticalCorrection) + touchY += mVerticalCorrection; + final int action = me.getAction(); + final long eventTime = me.getEventTime(); + int keyIndex = getKeyIndices(touchX, touchY, null); + mPossiblePoly = possiblePoly; + + // Track the last few movements to look for spurious swipes. + if (action == MotionEvent.ACTION_DOWN) mSwipeTracker.clear(); + mSwipeTracker.addMovement(me); + + // Ignore all motion events until a DOWN. + if (mAbortKey + && action != MotionEvent.ACTION_DOWN && action != MotionEvent.ACTION_CANCEL) { + return true; + } + + if (mGestureDetector.onTouchEvent(me)) { + showPreview(NOT_A_KEY); + mHandler.removeMessages(MSG_REPEAT); + mHandler.removeMessages(MSG_LONGPRESS); + return true; + } + + // Needs to be called after the gesture detector gets a turn, as it may have + // displayed the mini keyboard + if (mMiniKeyboardOnScreen && action != MotionEvent.ACTION_CANCEL) { + return true; + } + + switch (action) { + case MotionEvent.ACTION_DOWN: + mAbortKey = false; + mStartX = touchX; + mStartY = touchY; + mLastCodeX = touchX; + mLastCodeY = touchY; + mLastKeyTime = 0; + mCurrentKeyTime = 0; + mLastKey = NOT_A_KEY; + mCurrentKey = keyIndex; + mDownKey = keyIndex; + mDownTime = me.getEventTime(); + mLastMoveTime = mDownTime; + checkMultiTap(eventTime, keyIndex); + mKeyboardActionListener.onPress(keyIndex != NOT_A_KEY ? + mKeys[keyIndex].codes[0] : 0); + if (mCurrentKey >= 0 && mKeys[mCurrentKey].repeatable) { + mRepeatKeyIndex = mCurrentKey; + Message msg = mHandler.obtainMessage(MSG_REPEAT); + mHandler.sendMessageDelayed(msg, REPEAT_START_DELAY); + repeatKey(); + // Delivering the key could have caused an abort + if (mAbortKey) { + mRepeatKeyIndex = NOT_A_KEY; + break; + } + } + if (mCurrentKey != NOT_A_KEY) { + Message msg = mHandler.obtainMessage(MSG_LONGPRESS, me); + mHandler.sendMessageDelayed(msg, LONGPRESS_TIMEOUT); + } + showPreview(keyIndex); + break; + + case MotionEvent.ACTION_MOVE: + boolean continueLongPress = false; + if (keyIndex != NOT_A_KEY) { + if (mCurrentKey == NOT_A_KEY) { + mCurrentKey = keyIndex; + mCurrentKeyTime = eventTime - mDownTime; + } else { + if (keyIndex == mCurrentKey) { + mCurrentKeyTime += eventTime - mLastMoveTime; + continueLongPress = true; + } else if (mRepeatKeyIndex == NOT_A_KEY) { + resetMultiTap(); + mLastKey = mCurrentKey; + mLastCodeX = mLastX; + mLastCodeY = mLastY; + mLastKeyTime = + mCurrentKeyTime + eventTime - mLastMoveTime; + mCurrentKey = keyIndex; + mCurrentKeyTime = 0; + } + } + } + if (!continueLongPress) { + // Cancel old longpress + mHandler.removeMessages(MSG_LONGPRESS); + // Start new longpress if key has changed + if (keyIndex != NOT_A_KEY) { + Message msg = mHandler.obtainMessage(MSG_LONGPRESS, me); + mHandler.sendMessageDelayed(msg, LONGPRESS_TIMEOUT); + } + } + showPreview(mCurrentKey); + mLastMoveTime = eventTime; + break; + + case MotionEvent.ACTION_UP: + removeMessages(); + if (keyIndex == mCurrentKey) { + mCurrentKeyTime += eventTime - mLastMoveTime; + } else { + resetMultiTap(); + mLastKey = mCurrentKey; + mLastKeyTime = mCurrentKeyTime + eventTime - mLastMoveTime; + mCurrentKey = keyIndex; + mCurrentKeyTime = 0; + } + if (mCurrentKeyTime < mLastKeyTime && mCurrentKeyTime < DEBOUNCE_TIME + && mLastKey != NOT_A_KEY) { + mCurrentKey = mLastKey; + touchX = mLastCodeX; + touchY = mLastCodeY; + } + showPreview(NOT_A_KEY); + Arrays.fill(mKeyIndices, NOT_A_KEY); + // If we're not on a repeating key (which sends on a DOWN event) + if (mRepeatKeyIndex == NOT_A_KEY && !mMiniKeyboardOnScreen && !mAbortKey) { + detectAndSendKey(mCurrentKey, touchX, touchY, eventTime); + } + invalidateKey(keyIndex); + mRepeatKeyIndex = NOT_A_KEY; + break; + case MotionEvent.ACTION_CANCEL: + removeMessages(); + dismissPopupKeyboard(); + mAbortKey = true; + showPreview(NOT_A_KEY); + invalidateKey(mCurrentKey); + break; + } + mLastX = touchX; + mLastY = touchY; + return true; + } + + private boolean repeatKey() { + Key key = mKeys[mRepeatKeyIndex]; + detectAndSendKey(mCurrentKey, key.x, key.y, mLastTapTime); + return true; + } + + protected void swipeRight() { + mKeyboardActionListener.swipeRight(); + } + + protected void swipeLeft() { + mKeyboardActionListener.swipeLeft(); + } + + protected void swipeUp() { + mKeyboardActionListener.swipeUp(); + } + + protected void swipeDown() { + mKeyboardActionListener.swipeDown(); + } + + public void closing() { + if (mPreviewPopup.isShowing()) { + mPreviewPopup.dismiss(); + } + removeMessages(); + + dismissPopupKeyboard(); + mBuffer = null; + mCanvas = null; + mMiniKeyboardCache.clear(); + } + + private void removeMessages() { + if (mHandler != null) { + mHandler.removeMessages(MSG_REPEAT); + mHandler.removeMessages(MSG_LONGPRESS); + mHandler.removeMessages(MSG_SHOW_PREVIEW); + } + } + + @Override + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + closing(); + } + + private void dismissPopupKeyboard() { + if (mPopupKeyboard.isShowing()) { + mPopupKeyboard.dismiss(); + mMiniKeyboardOnScreen = false; + invalidateAllKeys(); + } + } + + public boolean handleBack() { + if (mPopupKeyboard.isShowing()) { + dismissPopupKeyboard(); + return true; + } + return false; + } + + private void resetMultiTap() { + mLastSentIndex = NOT_A_KEY; + mTapCount = 0; + mLastTapTime = -1; + mInMultiTap = false; + } + + private void checkMultiTap(long eventTime, int keyIndex) { + if (keyIndex == NOT_A_KEY) return; + Key key = mKeys[keyIndex]; + if (key.codes.length > 1) { + mInMultiTap = true; + if (eventTime < mLastTapTime + MULTITAP_INTERVAL + && keyIndex == mLastSentIndex) { + mTapCount = (mTapCount + 1) % key.codes.length; + return; + } else { + mTapCount = -1; + return; + } + } + if (eventTime > mLastTapTime + MULTITAP_INTERVAL || keyIndex != mLastSentIndex) { + resetMultiTap(); + } + } + + private static class SwipeTracker { + + static final int NUM_PAST = 4; + static final int LONGEST_PAST_TIME = 200; + + final float mPastX[] = new float[NUM_PAST]; + final float mPastY[] = new float[NUM_PAST]; + final long mPastTime[] = new long[NUM_PAST]; + + float mYVelocity; + float mXVelocity; + + public void clear() { + mPastTime[0] = 0; + } + + public void addMovement(MotionEvent ev) { + long time = ev.getEventTime(); + final int N = ev.getHistorySize(); + for (int i=0; i<N; i++) { + addPoint(ev.getHistoricalX(i), ev.getHistoricalY(i), + ev.getHistoricalEventTime(i)); + } + addPoint(ev.getX(), ev.getY(), time); + } + + private void addPoint(float x, float y, long time) { + int drop = -1; + int i; + final long[] pastTime = mPastTime; + for (i=0; i<NUM_PAST; i++) { + if (pastTime[i] == 0) { + break; + } else if (pastTime[i] < time-LONGEST_PAST_TIME) { + drop = i; + } + } + if (i == NUM_PAST && drop < 0) { + drop = 0; + } + if (drop == i) drop--; + final float[] pastX = mPastX; + final float[] pastY = mPastY; + if (drop >= 0) { + final int start = drop+1; + final int count = NUM_PAST-drop-1; + System.arraycopy(pastX, start, pastX, 0, count); + System.arraycopy(pastY, start, pastY, 0, count); + System.arraycopy(pastTime, start, pastTime, 0, count); + i -= (drop+1); + } + pastX[i] = x; + pastY[i] = y; + pastTime[i] = time; + i++; + if (i < NUM_PAST) { + pastTime[i] = 0; + } + } + + public void computeCurrentVelocity(int units) { + computeCurrentVelocity(units, Float.MAX_VALUE); + } + + public void computeCurrentVelocity(int units, float maxVelocity) { + final float[] pastX = mPastX; + final float[] pastY = mPastY; + final long[] pastTime = mPastTime; + + final float oldestX = pastX[0]; + final float oldestY = pastY[0]; + final long oldestTime = pastTime[0]; + float accumX = 0; + float accumY = 0; + int N=0; + while (N < NUM_PAST) { + if (pastTime[N] == 0) { + break; + } + N++; + } + + for (int i=1; i < N; i++) { + final int dur = (int)(pastTime[i] - oldestTime); + if (dur == 0) continue; + float dist = pastX[i] - oldestX; + float vel = (dist/dur) * units; // pixels/frame. + if (accumX == 0) accumX = vel; + else accumX = (accumX + vel) * .5f; + + dist = pastY[i] - oldestY; + vel = (dist/dur) * units; // pixels/frame. + if (accumY == 0) accumY = vel; + else accumY = (accumY + vel) * .5f; + } + mXVelocity = accumX < 0.0f ? Math.max(accumX, -maxVelocity) + : Math.min(accumX, maxVelocity); + mYVelocity = accumY < 0.0f ? Math.max(accumY, -maxVelocity) + : Math.min(accumY, maxVelocity); + } + + public float getXVelocity() { + return mXVelocity; + } + + public float getYVelocity() { + return mYVelocity; + } + } +} diff --git a/android/inputmethodservice/SoftInputWindow.java b/android/inputmethodservice/SoftInputWindow.java new file mode 100644 index 00000000..795117e4 --- /dev/null +++ b/android/inputmethodservice/SoftInputWindow.java @@ -0,0 +1,193 @@ +/* + * 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.inputmethodservice; + +import android.app.Dialog; +import android.content.Context; +import android.graphics.Rect; +import android.os.IBinder; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.WindowManager; + +/** + * A SoftInputWindow is a Dialog that is intended to be used for a top-level input + * method window. It will be displayed along the edge of the screen, moving + * the application user interface away from it so that the focused item is + * always visible. + * @hide + */ +public class SoftInputWindow extends Dialog { + final String mName; + final Callback mCallback; + final KeyEvent.Callback mKeyEventCallback; + final KeyEvent.DispatcherState mDispatcherState; + final int mWindowType; + final int mGravity; + final boolean mTakesFocus; + private final Rect mBounds = new Rect(); + + public interface Callback { + public void onBackPressed(); + } + + public void setToken(IBinder token) { + WindowManager.LayoutParams lp = getWindow().getAttributes(); + lp.token = token; + getWindow().setAttributes(lp); + } + + /** + * Create a SoftInputWindow that uses a custom style. + * + * @param context The Context in which the DockWindow should run. In + * particular, it uses the window manager and theme from this context + * to present its UI. + * @param theme A style resource describing the theme to use for the window. + * See <a href="{@docRoot}reference/available-resources.html#stylesandthemes">Style + * and Theme Resources</a> for more information about defining and + * using styles. This theme is applied on top of the current theme in + * <var>context</var>. If 0, the default dialog theme will be used. + */ + public SoftInputWindow(Context context, String name, int theme, Callback callback, + KeyEvent.Callback keyEventCallback, KeyEvent.DispatcherState dispatcherState, + int windowType, int gravity, boolean takesFocus) { + super(context, theme); + mName = name; + mCallback = callback; + mKeyEventCallback = keyEventCallback; + mDispatcherState = dispatcherState; + mWindowType = windowType; + mGravity = gravity; + mTakesFocus = takesFocus; + initDockWindow(); + } + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + mDispatcherState.reset(); + } + + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + getWindow().getDecorView().getHitRect(mBounds); + + if (ev.isWithinBoundsNoHistory(mBounds.left, mBounds.top, + mBounds.right - 1, mBounds.bottom - 1)) { + return super.dispatchTouchEvent(ev); + } else { + MotionEvent temp = ev.clampNoHistory(mBounds.left, mBounds.top, + mBounds.right - 1, mBounds.bottom - 1); + boolean handled = super.dispatchTouchEvent(temp); + temp.recycle(); + return handled; + } + } + + /** + * Set which boundary of the screen the DockWindow sticks to. + * + * @param gravity The boundary of the screen to stick. See {#link + * android.view.Gravity.LEFT}, {#link android.view.Gravity.TOP}, + * {#link android.view.Gravity.BOTTOM}, {#link + * android.view.Gravity.RIGHT}. + */ + public void setGravity(int gravity) { + WindowManager.LayoutParams lp = getWindow().getAttributes(); + lp.gravity = gravity; + updateWidthHeight(lp); + getWindow().setAttributes(lp); + } + + public int getGravity() { + return getWindow().getAttributes().gravity; + } + + private void updateWidthHeight(WindowManager.LayoutParams lp) { + if (lp.gravity == Gravity.TOP || lp.gravity == Gravity.BOTTOM) { + lp.width = WindowManager.LayoutParams.MATCH_PARENT; + lp.height = WindowManager.LayoutParams.WRAP_CONTENT; + } else { + lp.width = WindowManager.LayoutParams.WRAP_CONTENT; + lp.height = WindowManager.LayoutParams.MATCH_PARENT; + } + } + + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (mKeyEventCallback != null && mKeyEventCallback.onKeyDown(keyCode, event)) { + return true; + } + return super.onKeyDown(keyCode, event); + } + + public boolean onKeyLongPress(int keyCode, KeyEvent event) { + if (mKeyEventCallback != null && mKeyEventCallback.onKeyLongPress(keyCode, event)) { + return true; + } + return super.onKeyLongPress(keyCode, event); + } + + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (mKeyEventCallback != null && mKeyEventCallback.onKeyUp(keyCode, event)) { + return true; + } + return super.onKeyUp(keyCode, event); + } + + public boolean onKeyMultiple(int keyCode, int count, KeyEvent event) { + if (mKeyEventCallback != null && mKeyEventCallback.onKeyMultiple(keyCode, count, event)) { + return true; + } + return super.onKeyMultiple(keyCode, count, event); + } + + public void onBackPressed() { + if (mCallback != null) { + mCallback.onBackPressed(); + } else { + super.onBackPressed(); + } + } + + private void initDockWindow() { + WindowManager.LayoutParams lp = getWindow().getAttributes(); + + lp.type = mWindowType; + lp.setTitle(mName); + + lp.gravity = mGravity; + updateWidthHeight(lp); + + getWindow().setAttributes(lp); + + int windowSetFlags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; + int windowModFlags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | + WindowManager.LayoutParams.FLAG_DIM_BEHIND; + + if (!mTakesFocus) { + windowSetFlags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; + } else { + windowSetFlags |= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; + windowModFlags |= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; + } + + getWindow().setFlags(windowSetFlags, windowModFlags); + } +} |