From 9066cfe9886ac131c34d59ed0e2d287b0e3c0087 Mon Sep 17 00:00:00 2001 From: The Android Open Source Project Date: Tue, 3 Mar 2009 19:31:44 -0800 Subject: auto import from //depot/cupcake/@135843 --- .../inputmethodservice/InputMethodService.java | 1880 ++++++++++++++++++++ 1 file changed, 1880 insertions(+) create mode 100644 core/java/android/inputmethodservice/InputMethodService.java (limited to 'core/java/android/inputmethodservice/InputMethodService.java') diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java new file mode 100644 index 000000000000..1e2e2f3e7b56 --- /dev/null +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -0,0 +1,1880 @@ +/* + * 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.FILL_PARENT; +import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; + +import android.app.Dialog; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.IBinder; +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.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.inputmethod.CompletionInfo; +import android.view.inputmethod.ExtractedText; +import android.view.inputmethod.ExtractedTextRequest; +import android.view.inputmethod.InputBinding; +import android.view.inputmethod.InputConnection; +import android.view.inputmethod.InputMethod; +import android.view.inputmethod.InputMethodManager; +import android.view.inputmethod.EditorInfo; +import android.widget.Button; +import android.widget.FrameLayout; + +import java.io.FileDescriptor; +import java.io.PrintWriter; + +/** + * 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. + * + *

In addition to the normal Service lifecycle methods, this class + * introduces some new specific callbacks that most subclasses will want + * to make use of:

+ * + * + *

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.

+ * + *

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:

+ * + * + * + * + * + *

Soft Input View

+ * + *

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.

+ * + *

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.

+ * + * + * + *

Candidates View

+ * + *

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.

+ * + *

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.

+ * + * + * + *

Fullscreen Mode

+ * + *

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.

+ * + *

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.

+ * + *

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. + * + * + * + *

Generating Text

+ * + *

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.

+ * + *

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.

+ * + *

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.

+ */ +public class InputMethodService extends AbstractInputMethodService { + static final String TAG = "InputMethodService"; + static final boolean DEBUG = false; + + InputMethodManager mImm; + + LayoutInflater mInflater; + View mRootView; + SoftInputWindow mWindow; + boolean mInitialized; + boolean mWindowCreated; + boolean mWindowAdded; + boolean mWindowVisible; + FrameLayout mExtractFrame; + FrameLayout mCandidatesFrame; + FrameLayout mInputFrame; + + IBinder mToken; + + InputBinding mInputBinding; + InputConnection mInputConnection; + boolean mInputStarted; + boolean mInputViewStarted; + boolean mCandidatesViewStarted; + InputConnection mStartedInputConnection; + EditorInfo mInputEditorInfo; + + int mShowInputFlags; + boolean mShowInputRequested; + boolean mLastShowInputRequested; + int mCandidatesVisibility; + CompletionInfo[] mCurCompletions; + + boolean mShowInputForced; + + boolean mFullscreenApplied; + boolean mIsFullscreen; + View mExtractView; + ExtractEditText mExtractEditText; + ViewGroup mExtractAccessories; + Button mExtractAction; + ExtractedText mExtractedText; + int mExtractedToken; + + View mInputView; + boolean mIsInputViewShown; + + int mStatusIcon; + + 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 (isFullscreenMode()) { + // In 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.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME); + } else { + onComputeInsets(mTmpInsets); + info.contentInsets.top = mTmpInsets.contentTopInsets; + info.visibleInsets.top = mTmpInsets.visibleTopInsets; + 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); + InputConnection ic = getCurrentInputConnection(); + if (ic != null) ic.reportFullscreenMode(mIsFullscreen); + initialize(); + onBindInput(); + } + + /** + * Clear the current input binding. + */ + public void unbindInput() { + if (DEBUG) Log.v(TAG, "unbindInput(): binding=" + mInputBinding + + " ic=" + mInputConnection); + onUnbindInput(); + mInputStarted = false; + 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); + } + + /** + * Handle a request by the system to hide the soft input area. + */ + public void hideSoftInput() { + if (DEBUG) Log.v(TAG, "hideSoftInput()"); + mShowInputFlags = 0; + mShowInputRequested = false; + mShowInputForced = false; + hideWindow(); + } + + /** + * Handle a request by the system to show the soft input area. + */ + public void showSoftInput(int flags) { + if (DEBUG) Log.v(TAG, "showSoftInput()"); + mShowInputFlags = 0; + if (onShowInputRequested(flags, false)) { + showWindow(true); + } + } + } + + /** + * 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); + } + + /** + * 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); + } + } + + /** + * 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; + + /** + * 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; + + /** + * Determine which area of the window is touchable by the user. May + * be one of: {@link #TOUCHABLE_INSETS_FRAME}, + * {@link #TOUCHABLE_INSETS_CONTENT}, or {@link #TOUCHABLE_INSETS_VISIBLE}. + */ + public int touchableInsets; + } + + @Override public void onCreate() { + super.onCreate(); + mImm = (InputMethodManager)getSystemService(INPUT_METHOD_SERVICE); + mInflater = (LayoutInflater)getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + mWindow = new SoftInputWindow(this); + initViews(); + mWindow.getWindow().setLayout(FILL_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() { + } + + void initialize() { + if (!mInitialized) { + mInitialized = true; + onInitializeInterface(); + } + } + + void initViews() { + mInitialized = false; + mWindowCreated = false; + mShowInputRequested = false; + mShowInputForced = false; + + mRootView = mInflater.inflate( + com.android.internal.R.layout.input_method, null); + mWindow.setContentView(mRootView); + mRootView.getViewTreeObserver().addOnComputeInternalInsetsListener(mInsetsComputer); + if (Settings.System.getInt(getContentResolver(), + Settings.System.FANCY_IME_ANIMATIONS, 0) != 0) { + mWindow.getWindow().setWindowAnimations( + com.android.internal.R.style.Animation_InputMethodFancy); + } + 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); + if (mWindowAdded) { + mWindow.dismiss(); + } + } + + /** + * 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. + * + *

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); + + 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 (onShowInputRequested(showFlags, true)) { + showWindow(true); + if (completions != null) { + mCurCompletions = completions; + onDisplayCompletions(completions); + } + } else { + hideWindow(); + } + } 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. + hideWindow(); + } + } + } + + /** + * Implement to return our standard {@link InputMethodImpl}. Subclasses + * can override to provide their own customized version. + */ + public AbstractInputMethodImpl onCreateInputMethodInterface() { + return new InputMethodImpl(); + } + + /** + * Implement to return our standard {@link InputMethodSessionImpl}. Subclasses + * can override to provide their own customized version. + */ + public AbstractInputMethodSessionImpl onCreateInputMethodSessionInterface() { + return new InputMethodSessionImpl(); + } + + public LayoutInflater getLayoutInflater() { + return mInflater; + } + + public Dialog getWindow() { + return mWindow; + } + + /** + * 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. + * + *

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. + * + *

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; + InputConnection ic = getCurrentInputConnection(); + if (ic != null) ic.reportFullscreenMode(isFullscreen); + mFullscreenApplied = true; + initialize(); + Drawable bg = onCreateBackgroundDrawable(); + if (bg == null) { + // We need to give the window a real drawable, so that it + // correctly sets its mode. + bg = getResources().getDrawable(android.R.color.transparent); + } + mWindow.getWindow().setBackgroundDrawable(bg); + mExtractFrame.setVisibility(isFullscreen ? View.VISIBLE : View.GONE); + if (isFullscreen) { + if (mExtractView == null) { + View v = onCreateExtractTextView(); + if (v != null) { + setExtractView(v); + } + } + startExtractingText(false); + } + } + + 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. + * + *

The default implementation makes the layout for the window + * FILL_PARENT x FILL_PARENT when in fullscreen mode, and + * FILL_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) { + if (isFullscreen) { + mWindow.getWindow().setLayout(FILL_PARENT, FILL_PARENT); + } else { + mWindow.getWindow().setLayout(FILL_PARENT, WRAP_CONTENT); + } + } + + /** + * Return whether the input method is currently 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(); + return config.orientation == Configuration.ORIENTATION_LANDSCAPE; + } + + /** + * 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}. + * + *

Note that this method is not called when in fullscreen mode, 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 { + loc[1] = 0; + } + outInsets.contentTopInsets = loc[1]; + if (mCandidatesFrame.getVisibility() == View.VISIBLE) { + mCandidatesFrame.getLocationInWindow(loc); + } + outInsets.visibleTopInsets = loc[1]; + outInsets.touchableInsets = Insets.TOUCHABLE_INSETS_VISIBLE; + } + + /** + * 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 currently 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 only shows the input view when + * there is no hard keyboard or the keyboard is hidden. 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-evalauted and applied. + */ + public boolean onEvaluateInputViewShown() { + Configuration config = getResources().getConfiguration(); + return config.keyboard == Configuration.KEYBOARD_NOKEYS + || config.hardKeyboardHidden == Configuration.KEYBOARDHIDDEN_YES; + } + + /** + * Controls the visibility of the candidates display area. By default + * it is hidden. + */ + public void setCandidatesViewShown(boolean shown) { + int vis = shown ? View.VISIBLE : getCandidatesHiddenVisibility(); + if (mCandidatesVisibility != vis) { + mCandidatesFrame.setVisibility(vis); + mCandidatesVisibility = vis; + } + 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 { + hideWindow(); + } + } + } + + /** + * 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 in fullscreen mode, + * 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 isFullscreenMode() ? View.GONE : View.INVISIBLE; + } + + public void showStatusIcon(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 id. 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.FILL_PARENT, + ViewGroup.LayoutParams.FILL_PARENT)); + mExtractView = view; + if (view != null) { + mExtractEditText = (ExtractEditText)view.findViewById( + com.android.internal.R.id.inputExtractEditText); + mExtractEditText.setIME(this); + mExtractAction = (Button)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.FILL_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.FILL_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + mInputView = view; + } + + /** + * Called by the framework to create a Drawable for the background of + * the input method window. May return null for no background. The default + * implementation returns a non-null standard background only when in + * fullscreen mode. This is called each time the fullscreen mode changes. + */ + public Drawable onCreateBackgroundDrawable() { + if (isFullscreenMode()) { + return getResources().getDrawable( + com.android.internal.R.drawable.input_method_fullscreen_background); + } + return null; + } + + /** + * 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. + * + *

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. + * + *

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) { + } + + /** + * 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. + * + *

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. + * + * @boolean 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. + * + *

Note that this will not 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) { + } + + /** + * 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. + * + *

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. + * + * @boolean 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(int) InputMethod.showSoftInput(int)} + * 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(int) InputMethod.showSoftInput(int)}. + * @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; + } + Configuration config = getResources().getConfiguration(); + if (config.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; + } + } + if ((flags&InputMethod.SHOW_FORCED) != 0) { + mShowInputForced = true; + } + return true; + } + + public void showWindow(boolean showInput) { + if (DEBUG) Log.v(TAG, "Showing window: showInput=" + showInput + + " mShowInputRequested=" + mShowInputRequested + + " mWindowAdded=" + mWindowAdded + + " mWindowCreated=" + mWindowCreated + + " mWindowVisible=" + mWindowVisible + + " mInputStarted=" + mInputStarted); + boolean doShowInput = false; + boolean wasVisible = mWindowVisible; + mWindowVisible = true; + if (!mShowInputRequested) { + if (mInputStarted) { + if (showInput) { + doShowInput = true; + mShowInputRequested = true; + } + } + } else { + showInput = 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); + } + + if (!wasVisible) { + if (DEBUG) Log.v(TAG, "showWindow: showing!"); + onWindowShown(); + mWindow.show(); + } + } + + public void hideWindow() { + 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; + if (mWindowVisible) { + mWindow.hide(); + mWindowVisible = false; + onWindowHidden(); + } + } + + /** + * 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() { + } + + /** + * Called when the input method window has been hidden from the user, + * after previously being visible. + */ + public void onWindowHidden() { + } + + /** + * 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() { + } + + /** + * 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() { + } + + /** + * 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) { + } + + 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 + * not called when input restarts in the same editor. + * + *

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. + * + *

The default implementation here does nothing. + */ + public void onDisplayCompletions(CompletionInfo[] completions) { + } + + /** + * 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 (mExtractEditText != null && text != 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. + * + *

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 && mExtractedText != null) { + final int off = mExtractedText.startOffset; + eet.startInternalChanges(); + eet.setSelection(newSelStart-off, newSelEnd-off); + eet.finishInternalChanges(); + } + } + + /** + * 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. + */ + public void onUpdateCursor(Rect newCursor) { + } + + /** + * 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 dismissSoftInput(int flags) { + mImm.hideSoftInputFromInputMethod(mToken, flags); + } + + /** + * Override this to intercept key down events before they are processed by the + * application. If you return true, the application will not itself + * process the event. If you return true, the normal application processing + * will occur as if the IME had not seen the event at all. + * + *

The default implementation intercepts {@link KeyEvent#KEYCODE_BACK + * KeyEvent.KEYCODE_BACK} to hide the current IME UI if it is shown. In + * additional, in fullscreen mode only, it will consume DPAD movement + * events to move the cursor in the extracted text view, not allowing + * them to perform navigation in the underlying application. + */ + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (event.getKeyCode() == KeyEvent.KEYCODE_BACK + && event.getRepeatCount() == 0) { + if (mShowInputRequested) { + // If the soft input area is shown, back closes it and we + // consume the back key. + dismissSoftInput(0); + return true; + } else if (mWindowVisible) { + if (mCandidatesVisibility == View.VISIBLE) { + // If we are showing candidates even if no input area, then + // hide them. + setCandidatesViewShown(false); + return true; + } else { + // If we have the window visible for some other reason -- + // most likely to show candidates -- then just get rid + // of it. This really shouldn't happen, but just in case... + hideWindow(); + return true; + } + } + } + + return doMovementKey(keyCode, event, MOVEMENT_DOWN); + } + + /** + * 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 true, the normal application processing + * will occur as if the IME had not seen the event at all. + * + *

The default implementation always returns false, except when + * in fullscreen mode, where it will consume DPAD movement + * 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 true, the normal application processing + * will occur as if the IME had not seen the event at all. + * + *

The default implementation always returns false, except when + * in fullscreen mode, where it will consume DPAD movement + * 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) { + return doMovementKey(keyCode, event, MOVEMENT_UP); + } + + public boolean onTrackballEvent(MotionEvent event) { + return false; + } + + public void onAppPrivateCommand(String action, Bundle data) { + } + + 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 = mExtractEditText; + if (isFullscreenMode() && isInputViewShown() && 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, + (Spannable)eet.getText(), keyCode, event)) { + reportExtractedMovement(keyCode, 1); + return true; + } + } else if (count == MOVEMENT_UP) { + if (movement.onKeyUp(eet, + (Spannable)eet.getText(), keyCode, event)) { + return true; + } + } else { + if (movement.onKeyOther(eet, (Spannable)eet.getText(), event)) { + reportExtractedMovement(keyCode, count); + } else { + KeyEvent down = new KeyEvent(event, KeyEvent.ACTION_DOWN); + if (movement.onKeyDown(eet, + (Spannable)eet.getText(), keyCode, down)) { + KeyEvent up = new KeyEvent(event, KeyEvent.ACTION_UP); + movement.onKeyUp(eet, + (Spannable)eet.getText(), keyCode, up); + while (--count > 0) { + movement.onKeyDown(eet, + (Spannable)eet.getText(), keyCode, down); + movement.onKeyUp(eet, + (Spannable)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. + * + * @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, 0, 0, + KeyEvent.FLAG_SOFT_KEYBOARD|KeyEvent.FLAG_KEEP_TOUCH_MODE)); + ic.sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime, + KeyEvent.ACTION_UP, keyEventCode, 0, 0, 0, 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 not + * 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. + * + * @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((char) 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); + } + } + + /** + * 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); + default: + return getText(com.android.internal.R.string.ime_action_default); + } + } + + /** + * Called when it is time to update the actions available from a full-screen + * IME. 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 action in your own UI and handle it. + */ + public void onUpdateExtractingAccessories(EditorInfo ei) { + 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_ENTER_ACTION) != 0); + if (hasAction) { + mExtractAccessories.setVisibility(View.VISIBLE); + if (ei.actionLabel != null) { + mExtractAction.setText(ei.actionLabel); + } else { + mExtractAction.setText(getTextForImeAction(ei.imeOptions)); + } + mExtractAction.setOnClickListener(mActionClickListener); + } else { + mExtractAccessories.setVisibility(View.GONE); + 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 an confusing experience for the user. + */ + public void onExtractingInputChanged(EditorInfo ei) { + if (ei.inputType == InputType.TYPE_NULL) { + dismissSoftInput(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; + mExtractedText = getCurrentInputConnection().getExtractedText(req, + InputConnection.GET_EXTRACTED_TEXT_MONITOR); + + final EditorInfo ei = getCurrentInputEditorInfo(); + + try { + eet.startInternalChanges(); + onUpdateExtractingAccessories(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); + } + } + } + + /** + * 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 + + " mWindowVisible=" + mWindowVisible); + 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); + + if (mInputEditorInfo != null) { + p.println(" mInputEditorInfo:"); + mInputEditorInfo.dump(p, " "); + } else { + p.println(" mInputEditorInfo: null"); + } + + p.println(" mShowInputRequested=" + mShowInputRequested + + " mLastShowInputRequested=" + mLastShowInputRequested + + " mShowInputForced=" + mShowInputForced + + " mShowInputFlags=0x" + Integer.toHexString(mShowInputFlags)); + p.println(" mCandidatesVisibility=" + mCandidatesVisibility + + " mFullscreenApplied=" + mFullscreenApplied + + " mIsFullscreen=" + mIsFullscreen); + + 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); + } +} -- cgit v1.2.3