Automatic sources dropoff on 2020-06-10 18:32:38.095721

The change is generated with prebuilt drop tool.

Change-Id: I24cbf6ba6db262a1ae1445db1427a08fee35b3b4
diff --git a/android/inputmethodservice/AbstractInputMethodService.java b/android/inputmethodservice/AbstractInputMethodService.java
new file mode 100644
index 0000000..b0fca00
--- /dev/null
+++ b/android/inputmethodservice/AbstractInputMethodService.java
@@ -0,0 +1,263 @@
+/*
+ * 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.MainThread;
+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()}.
+         */
+        @MainThread
+        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.
+         */
+        @MainThread
+        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.
+         */
+        @MainThread
+        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;
+    }
+
+    /**
+     * Called when the user took some actions that should be taken into consideration to update the
+     * MRU list for input method rotation.
+     *
+     * @hide
+     */
+    public void notifyUserActionIfNecessary() {
+    }
+}
diff --git a/android/inputmethodservice/CompactExtractEditLayout.java b/android/inputmethodservice/CompactExtractEditLayout.java
new file mode 100644
index 0000000..4925d25
--- /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 0000000..fe63c1e
--- /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 0000000..af69f0f
--- /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 0000000..a2c1d18
--- /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 0000000..e9de274
--- /dev/null
+++ b/android/inputmethodservice/IInputMethodSessionWrapper.java
@@ -0,0 +1,266 @@
+/*
+ * 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.compat.annotation.UnsupportedAppUsage;
+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_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;
+    private static final int DO_NOTIFY_IME_HIDDEN = 120;
+    private static final int DO_REMOVE_IME_SURFACE = 130;
+
+    @UnsupportedAppUsage
+    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_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;
+            }
+            case DO_NOTIFY_IME_HIDDEN: {
+                mInputMethodSession.notifyImeHidden();
+                return;
+            }
+            case DO_REMOVE_IME_SURFACE: {
+                mInputMethodSession.removeImeSurface();
+                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 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 notifyImeHidden() {
+        mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_NOTIFY_IME_HIDDEN));
+    }
+
+    @Override
+    public void removeImeSurface() {
+        mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_REMOVE_IME_SURFACE));
+    }
+
+    @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) {
+            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 0000000..a298c85
--- /dev/null
+++ b/android/inputmethodservice/IInputMethodWrapper.java
@@ -0,0 +1,403 @@
+/*
+ * 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.annotation.BinderThread;
+import android.annotation.MainThread;
+import android.annotation.Nullable;
+import android.compat.annotation.UnsupportedAppUsage;
+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 com.android.internal.inputmethod.IInputMethodPrivilegedOperations;
+import com.android.internal.inputmethod.CancellationGroup;
+import com.android.internal.os.HandlerCaller;
+import com.android.internal.os.SomeArgs;
+import com.android.internal.view.IInlineSuggestionsRequestCallback;
+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.InlineSuggestionsRequestInfo;
+import com.android.internal.view.InputConnectionWrapper;
+
+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_INITIALIZE_INTERNAL = 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;
+    private static final int DO_CREATE_INLINE_SUGGESTIONS_REQUEST = 90;
+
+    final WeakReference<AbstractInputMethodService> mTarget;
+    final Context mContext;
+    @UnsupportedAppUsage
+    final HandlerCaller mCaller;
+    final WeakReference<InputMethod> mInputMethod;
+    final int mTargetSdkVersion;
+
+    /**
+     * This is not {@null} only between {@link #bindInput(InputBinding)} and {@link #unbindInput()}
+     * so that {@link InputConnectionWrapper} can query if {@link #unbindInput()} has already been
+     * called or not, mainly to avoid unnecessary blocking operations.
+     *
+     * <p>This field must be set and cleared only from the binder thread(s), where the system
+     * guarantees that {@link #bindInput(InputBinding)},
+     * {@link #startInput(IBinder, IInputContext, int, EditorInfo, boolean, boolean)}, and
+     * {@link #unbindInput()} are called with the same order as the original calls
+     * in {@link com.android.server.inputmethod.InputMethodManagerService}.
+     * See {@link IBinder#FLAG_ONEWAY} for detailed semantics.</p>
+     */
+    @Nullable
+    CancellationGroup mCancellationGroup = null;
+
+    // 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<>(context);
+        mContext = context.getApplicationContext();
+        mCaller = new HandlerCaller(mContext, null, this, true /*asyncHandler*/);
+        mInputMethod = new WeakReference<>(inputMethod);
+        mTargetSdkVersion = context.getApplicationInfo().targetSdkVersion;
+    }
+
+    @MainThread
+    @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_INITIALIZE_INTERNAL: {
+                SomeArgs args = (SomeArgs) msg.obj;
+                try {
+                    inputMethod.initializeInternal((IBinder) args.arg1, msg.arg1,
+                            (IInputMethodPrivilegedOperations) args.arg2);
+                } finally {
+                    args.recycle();
+                }
+                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 IBinder startInputToken = (IBinder) args.arg1;
+                final IInputContext inputContext = (IInputContext) args.arg2;
+                final EditorInfo info = (EditorInfo) args.arg3;
+                final CancellationGroup cancellationGroup = (CancellationGroup) args.arg4;
+                SomeArgs moreArgs = (SomeArgs) args.arg5;
+                final InputConnection ic = inputContext != null
+                        ? new InputConnectionWrapper(
+                                mTarget, inputContext, moreArgs.argi3, cancellationGroup)
+                        : null;
+                info.makeCompatible(mTargetSdkVersion);
+                inputMethod.dispatchStartInputWithToken(
+                        ic,
+                        info,
+                        moreArgs.argi1 == 1 /* restarting */,
+                        startInputToken,
+                        moreArgs.argi2 == 1 /* shouldPreRenderIme */);
+                args.recycle();
+                moreArgs.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: {
+                final SomeArgs args = (SomeArgs)msg.obj;
+                inputMethod.showSoftInputWithToken(
+                        msg.arg1, (ResultReceiver) args.arg2, (IBinder) args.arg1);
+                args.recycle();
+                return;
+            }
+            case DO_HIDE_SOFT_INPUT: {
+                final SomeArgs args = (SomeArgs) msg.obj;
+                inputMethod.hideSoftInputWithToken(msg.arg1, (ResultReceiver) args.arg2,
+                        (IBinder) args.arg1);
+                args.recycle();
+                return;
+            }
+            case DO_CHANGE_INPUTMETHOD_SUBTYPE:
+                inputMethod.changeInputMethodSubtype((InputMethodSubtype)msg.obj);
+                return;
+            case DO_CREATE_INLINE_SUGGESTIONS_REQUEST:
+                final SomeArgs args = (SomeArgs) msg.obj;
+                inputMethod.onCreateInlineSuggestionsRequest(
+                        (InlineSuggestionsRequestInfo) args.arg1,
+                        (IInlineSuggestionsRequestCallback) args.arg2);
+                args.recycle();
+                return;
+
+        }
+        Log.w(TAG, "Unhandled message code: " + msg.what);
+    }
+
+    @BinderThread
+    @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");
+        }
+    }
+
+    @BinderThread
+    @Override
+    public void initializeInternal(IBinder token, int displayId,
+            IInputMethodPrivilegedOperations privOps) {
+        mCaller.executeOrSendMessage(
+                mCaller.obtainMessageIOO(DO_INITIALIZE_INTERNAL, displayId, token, privOps));
+    }
+
+    @BinderThread
+    @Override
+    public void onCreateInlineSuggestionsRequest(InlineSuggestionsRequestInfo requestInfo,
+            IInlineSuggestionsRequestCallback cb) {
+        mCaller.executeOrSendMessage(
+                mCaller.obtainMessageOO(DO_CREATE_INLINE_SUGGESTIONS_REQUEST, requestInfo, cb));
+    }
+
+    @BinderThread
+    @Override
+    public void bindInput(InputBinding binding) {
+        if (mCancellationGroup != null) {
+            Log.e(TAG, "bindInput must be paired with unbindInput.");
+        }
+        mCancellationGroup = new CancellationGroup();
+        // This IInputContext is guaranteed to implement all the methods.
+        final int missingMethodFlags = 0;
+        InputConnection ic = new InputConnectionWrapper(mTarget,
+                IInputContext.Stub.asInterface(binding.getConnectionToken()), missingMethodFlags,
+                mCancellationGroup);
+        InputBinding nu = new InputBinding(ic, binding);
+        mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_SET_INPUT_CONTEXT, nu));
+    }
+
+    @BinderThread
+    @Override
+    public void unbindInput() {
+        if (mCancellationGroup != null) {
+            // Signal the flag then forget it.
+            mCancellationGroup.cancelAll();
+            mCancellationGroup = null;
+        } else {
+            Log.e(TAG, "unbindInput must be paired with bindInput.");
+        }
+        mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_UNSET_INPUT_CONTEXT));
+    }
+
+    @BinderThread
+    @Override
+    public void startInput(IBinder startInputToken, IInputContext inputContext,
+            @InputConnectionInspector.MissingMethodFlags final int missingMethods,
+            EditorInfo attribute, boolean restarting, boolean shouldPreRenderIme) {
+        if (mCancellationGroup == null) {
+            Log.e(TAG, "startInput must be called after bindInput.");
+            mCancellationGroup = new CancellationGroup();
+        }
+        SomeArgs args = SomeArgs.obtain();
+        args.argi1 = restarting ? 1 : 0;
+        args.argi2 = shouldPreRenderIme ? 1 : 0;
+        args.argi3 = missingMethods;
+        mCaller.executeOrSendMessage(mCaller.obtainMessageOOOOO(DO_START_INPUT, startInputToken,
+                inputContext, attribute, mCancellationGroup, args));
+    }
+
+    @BinderThread
+    @Override
+    public void createSession(InputChannel channel, IInputSessionCallback callback) {
+        mCaller.executeOrSendMessage(mCaller.obtainMessageOO(DO_CREATE_SESSION,
+                channel, callback));
+    }
+
+    @BinderThread
+    @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);
+        }
+    }
+
+    @BinderThread
+    @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);
+        }
+    }
+
+    @BinderThread
+    @Override
+    public void showSoftInput(IBinder showInputToken, int flags, ResultReceiver resultReceiver) {
+        mCaller.executeOrSendMessage(mCaller.obtainMessageIOO(DO_SHOW_SOFT_INPUT,
+                flags, showInputToken, resultReceiver));
+    }
+
+    @BinderThread
+    @Override
+    public void hideSoftInput(IBinder hideInputToken, int flags, ResultReceiver resultReceiver) {
+        mCaller.executeOrSendMessage(mCaller.obtainMessageIOO(DO_HIDE_SOFT_INPUT,
+                flags, hideInputToken, resultReceiver));
+    }
+
+    @BinderThread
+    @Override
+    public void changeInputMethodSubtype(InputMethodSubtype subtype) {
+        mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_CHANGE_INPUTMETHOD_SUBTYPE,
+                subtype));
+    }
+}
diff --git a/android/inputmethodservice/InlineSuggestionSession.java b/android/inputmethodservice/InlineSuggestionSession.java
new file mode 100644
index 0000000..2619788
--- /dev/null
+++ b/android/inputmethodservice/InlineSuggestionSession.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2020 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.inputmethodservice.InputMethodService.DEBUG;
+
+import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
+
+import android.annotation.BinderThread;
+import android.annotation.MainThread;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.autofill.AutofillId;
+import android.view.inputmethod.InlineSuggestionsRequest;
+import android.view.inputmethod.InlineSuggestionsResponse;
+
+import com.android.internal.view.IInlineSuggestionsRequestCallback;
+import com.android.internal.view.IInlineSuggestionsResponseCallback;
+import com.android.internal.view.InlineSuggestionsRequestInfo;
+
+import java.lang.ref.WeakReference;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/**
+ * Maintains an inline suggestion session with the autofill manager service.
+ *
+ * <p> Each session correspond to one request from the Autofill manager service to create an
+ * {@link InlineSuggestionsRequest}. It's responsible for calling back to the Autofill manager
+ * service with {@link InlineSuggestionsRequest} and receiving {@link InlineSuggestionsResponse}
+ * from it.
+ * <p>
+ * TODO(b/151123764): currently the session may receive responses for different views on the same
+ * screen, but we will fix it so each session corresponds to one view.
+ *
+ * <p> All the methods are expected to be called from the main thread, to ensure thread safety.
+ */
+class InlineSuggestionSession {
+    private static final String TAG = "ImsInlineSuggestionSession";
+
+    @NonNull
+    private final Handler mMainThreadHandler;
+    @NonNull
+    private final InlineSuggestionSessionController mInlineSuggestionSessionController;
+    @NonNull
+    private final InlineSuggestionsRequestInfo mRequestInfo;
+    @NonNull
+    private final IInlineSuggestionsRequestCallback mCallback;
+    @NonNull
+    private final Function<Bundle, InlineSuggestionsRequest> mRequestSupplier;
+    @NonNull
+    private final Supplier<IBinder> mHostInputTokenSupplier;
+    @NonNull
+    private final Consumer<InlineSuggestionsResponse> mResponseConsumer;
+
+
+    /**
+     * Indicates whether {@link #makeInlineSuggestionRequestUncheck()} has been called or not,
+     * because it should only be called at most once.
+     */
+    @Nullable
+    private boolean mCallbackInvoked = false;
+    @Nullable
+    private InlineSuggestionsResponseCallbackImpl mResponseCallback;
+
+    InlineSuggestionSession(@NonNull InlineSuggestionsRequestInfo requestInfo,
+            @NonNull IInlineSuggestionsRequestCallback callback,
+            @NonNull Function<Bundle, InlineSuggestionsRequest> requestSupplier,
+            @NonNull Supplier<IBinder> hostInputTokenSupplier,
+            @NonNull Consumer<InlineSuggestionsResponse> responseConsumer,
+            @NonNull InlineSuggestionSessionController inlineSuggestionSessionController,
+            @NonNull Handler mainThreadHandler) {
+        mRequestInfo = requestInfo;
+        mCallback = callback;
+        mRequestSupplier = requestSupplier;
+        mHostInputTokenSupplier = hostInputTokenSupplier;
+        mResponseConsumer = responseConsumer;
+        mInlineSuggestionSessionController = inlineSuggestionSessionController;
+        mMainThreadHandler = mainThreadHandler;
+    }
+
+    @MainThread
+    InlineSuggestionsRequestInfo getRequestInfo() {
+        return mRequestInfo;
+    }
+
+    @MainThread
+    IInlineSuggestionsRequestCallback getRequestCallback() {
+        return mCallback;
+    }
+
+    /**
+     * Returns true if the session should send Ime status updates to Autofill.
+     *
+     * <p> The session only starts to send Ime status updates to Autofill after the sending back
+     * an {@link InlineSuggestionsRequest}.
+     */
+    @MainThread
+    boolean shouldSendImeStatus() {
+        return mResponseCallback != null;
+    }
+
+    /**
+     * Returns true if {@link #makeInlineSuggestionRequestUncheck()} is called. It doesn't not
+     * necessarily mean an {@link InlineSuggestionsRequest} was sent, because it may call {@link
+     * IInlineSuggestionsRequestCallback#onInlineSuggestionsUnsupported()}.
+     *
+     * <p> The callback should be invoked at most once for each session.
+     */
+    @MainThread
+    boolean isCallbackInvoked() {
+        return mCallbackInvoked;
+    }
+
+    /**
+     * Invalidates the current session so it doesn't process any further {@link
+     * InlineSuggestionsResponse} from Autofill.
+     *
+     * <p> This method should be called when the session is de-referenced from the {@link
+     * InlineSuggestionSessionController}.
+     */
+    @MainThread
+    void invalidate() {
+        if (mResponseCallback != null) {
+            mResponseCallback.invalidate();
+            mResponseCallback = null;
+        }
+    }
+
+    /**
+     * Gets the {@link InlineSuggestionsRequest} from IME and send it back to the Autofill if it's
+     * not null.
+     *
+     * <p>Calling this method implies that the input is started on the view corresponding to the
+     * session.
+     */
+    @MainThread
+    void makeInlineSuggestionRequestUncheck() {
+        if (mCallbackInvoked) {
+            return;
+        }
+        try {
+            final InlineSuggestionsRequest request = mRequestSupplier.apply(
+                    mRequestInfo.getUiExtras());
+            if (request == null) {
+                if (DEBUG) {
+                    Log.d(TAG, "onCreateInlineSuggestionsRequest() returned null request");
+                }
+                mCallback.onInlineSuggestionsUnsupported();
+            } else {
+                request.setHostInputToken(mHostInputTokenSupplier.get());
+                request.filterContentTypes();
+                mResponseCallback = new InlineSuggestionsResponseCallbackImpl(this);
+                mCallback.onInlineSuggestionsRequest(request, mResponseCallback);
+            }
+        } catch (RemoteException e) {
+            Log.w(TAG, "makeInlinedSuggestionsRequest() remote exception:" + e);
+        }
+        mCallbackInvoked = true;
+    }
+
+    @MainThread
+    void handleOnInlineSuggestionsResponse(@NonNull AutofillId fieldId,
+            @NonNull InlineSuggestionsResponse response) {
+        if (!mInlineSuggestionSessionController.match(fieldId)) {
+            return;
+        }
+        if (DEBUG) {
+            Log.d(TAG, "IME receives response: " + response.getInlineSuggestions().size());
+        }
+        mResponseConsumer.accept(response);
+    }
+
+    /**
+     * Internal implementation of {@link IInlineSuggestionsResponseCallback}.
+     */
+    private static final class InlineSuggestionsResponseCallbackImpl extends
+            IInlineSuggestionsResponseCallback.Stub {
+        private final WeakReference<InlineSuggestionSession> mSession;
+        private volatile boolean mInvalid = false;
+
+        private InlineSuggestionsResponseCallbackImpl(InlineSuggestionSession session) {
+            mSession = new WeakReference<>(session);
+        }
+
+        void invalidate() {
+            mInvalid = true;
+        }
+
+        @BinderThread
+        @Override
+        public void onInlineSuggestionsResponse(AutofillId fieldId,
+                InlineSuggestionsResponse response) {
+            if (mInvalid) {
+                return;
+            }
+            final InlineSuggestionSession session = mSession.get();
+            if (session != null) {
+                session.mMainThreadHandler.sendMessage(
+                        obtainMessage(InlineSuggestionSession::handleOnInlineSuggestionsResponse,
+                                session, fieldId, response));
+            }
+        }
+    }
+}
diff --git a/android/inputmethodservice/InlineSuggestionSessionController.java b/android/inputmethodservice/InlineSuggestionSessionController.java
new file mode 100644
index 0000000..c9f9059
--- /dev/null
+++ b/android/inputmethodservice/InlineSuggestionSessionController.java
@@ -0,0 +1,261 @@
+/*
+ * Copyright (C) 2020 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.inputmethodservice.InputMethodService.DEBUG;
+
+import android.annotation.MainThread;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.autofill.AutofillId;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InlineSuggestionsRequest;
+import android.view.inputmethod.InlineSuggestionsResponse;
+
+import com.android.internal.view.IInlineSuggestionsRequestCallback;
+import com.android.internal.view.InlineSuggestionsRequestInfo;
+
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/**
+ * Manages the interaction with the autofill manager service for the inline suggestion sessions.
+ *
+ * <p>
+ * The class maintains the inline suggestion session with the autofill service. There is at most one
+ * active inline suggestion session at any given time.
+ *
+ * <p>
+ * The class receives the IME status change events (input start/finish, input view start/finish, and
+ * show input requested result), and send them through IPC to the {@link
+ * com.android.server.inputmethod.InputMethodManagerService}, which sends them to {@link
+ * com.android.server.autofill.InlineSuggestionSession} in the Autofill manager service. If there is
+ * no open inline suggestion session, no event will be send to autofill manager service.
+ *
+ * <p>
+ * All the methods are expected to be called from the main thread, to ensure thread safety.
+ */
+class InlineSuggestionSessionController {
+    private static final String TAG = "InlineSuggestionSessionController";
+
+    private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper(), null, true);
+
+    @NonNull
+    private final Function<Bundle, InlineSuggestionsRequest> mRequestSupplier;
+    @NonNull
+    private final Supplier<IBinder> mHostInputTokenSupplier;
+    @NonNull
+    private final Consumer<InlineSuggestionsResponse> mResponseConsumer;
+
+    /* The following variables track the IME status */
+    @Nullable
+    private String mImeClientPackageName;
+    @Nullable
+    private AutofillId mImeClientFieldId;
+    private boolean mImeInputStarted;
+    private boolean mImeInputViewStarted;
+
+    @Nullable
+    private InlineSuggestionSession mSession;
+
+    InlineSuggestionSessionController(
+            @NonNull Function<Bundle, InlineSuggestionsRequest> requestSupplier,
+            @NonNull Supplier<IBinder> hostInputTokenSupplier,
+            @NonNull Consumer<InlineSuggestionsResponse> responseConsumer) {
+        mRequestSupplier = requestSupplier;
+        mHostInputTokenSupplier = hostInputTokenSupplier;
+        mResponseConsumer = responseConsumer;
+    }
+
+    /**
+     * Called upon IME receiving a create inline suggestion request. Must be called in the main
+     * thread to ensure thread safety.
+     */
+    @MainThread
+    void onMakeInlineSuggestionsRequest(@NonNull InlineSuggestionsRequestInfo requestInfo,
+            @NonNull IInlineSuggestionsRequestCallback callback) {
+        if (DEBUG) Log.d(TAG, "onMakeInlineSuggestionsRequest: " + requestInfo);
+        // Creates a new session for the new create request from Autofill.
+        if (mSession != null) {
+            mSession.invalidate();
+        }
+        mSession = new InlineSuggestionSession(requestInfo, callback, mRequestSupplier,
+                mHostInputTokenSupplier, mResponseConsumer, this, mMainThreadHandler);
+
+        // If the input is started on the same view, then initiate the callback to the Autofill.
+        // Otherwise wait for the input to start.
+        if (mImeInputStarted && match(mSession.getRequestInfo())) {
+            mSession.makeInlineSuggestionRequestUncheck();
+            // ... then update the Autofill whether the input view is started.
+            if (mImeInputViewStarted) {
+                try {
+                    mSession.getRequestCallback().onInputMethodStartInputView();
+                } catch (RemoteException e) {
+                    Log.w(TAG, "onInputMethodStartInputView() remote exception:" + e);
+                }
+            }
+        }
+    }
+
+    /**
+     * Called from IME main thread before calling {@link InputMethodService#onStartInput(EditorInfo,
+     * boolean)}. This method should be quick as it makes a unblocking IPC.
+     */
+    @MainThread
+    void notifyOnStartInput(@Nullable String imeClientPackageName,
+            @Nullable AutofillId imeFieldId) {
+        if (DEBUG) Log.d(TAG, "notifyOnStartInput: " + imeClientPackageName + ", " + imeFieldId);
+        if (imeClientPackageName == null || imeFieldId == null) {
+            return;
+        }
+        mImeInputStarted = true;
+        mImeClientPackageName = imeClientPackageName;
+        mImeClientFieldId = imeFieldId;
+
+        if (mSession != null) {
+            // Initiates the callback to Autofill if there is a pending matching session.
+            // Otherwise updates the session with the Ime status.
+            if (!mSession.isCallbackInvoked() && match(mSession.getRequestInfo())) {
+                mSession.makeInlineSuggestionRequestUncheck();
+            } else if (mSession.shouldSendImeStatus()) {
+                try {
+                    mSession.getRequestCallback().onInputMethodStartInput(mImeClientFieldId);
+                } catch (RemoteException e) {
+                    Log.w(TAG, "onInputMethodStartInput() remote exception:" + e);
+                }
+            }
+        }
+    }
+
+    /**
+     * Called from IME main thread after getting results from
+     * {@link InputMethodService#dispatchOnShowInputRequested(int,
+     * boolean)}. This method should be quick as it makes a unblocking IPC.
+     */
+    @MainThread
+    void notifyOnShowInputRequested(boolean requestResult) {
+        if (DEBUG) Log.d(TAG, "notifyShowInputRequested");
+        if (mSession != null && mSession.shouldSendImeStatus()) {
+            try {
+                mSession.getRequestCallback().onInputMethodShowInputRequested(requestResult);
+            } catch (RemoteException e) {
+                Log.w(TAG, "onInputMethodShowInputRequested() remote exception:" + e);
+            }
+        }
+    }
+
+    /**
+     * Called from IME main thread before calling
+     * {@link InputMethodService#onStartInputView(EditorInfo,
+     * boolean)} . This method should be quick as it makes a unblocking IPC.
+     */
+    @MainThread
+    void notifyOnStartInputView() {
+        if (DEBUG) Log.d(TAG, "notifyOnStartInputView");
+        mImeInputViewStarted = true;
+        if (mSession != null && mSession.shouldSendImeStatus()) {
+            try {
+                mSession.getRequestCallback().onInputMethodStartInputView();
+            } catch (RemoteException e) {
+                Log.w(TAG, "onInputMethodStartInputView() remote exception:" + e);
+            }
+        }
+    }
+
+    /**
+     * Called from IME main thread before calling
+     * {@link InputMethodService#onFinishInputView(boolean)}.
+     * This method should be quick as it makes a unblocking IPC.
+     */
+    @MainThread
+    void notifyOnFinishInputView() {
+        if (DEBUG) Log.d(TAG, "notifyOnFinishInputView");
+        mImeInputViewStarted = false;
+        if (mSession != null && mSession.shouldSendImeStatus()) {
+            try {
+                mSession.getRequestCallback().onInputMethodFinishInputView();
+            } catch (RemoteException e) {
+                Log.w(TAG, "onInputMethodFinishInputView() remote exception:" + e);
+            }
+        }
+    }
+
+    /**
+     * Called from IME main thread before calling {@link InputMethodService#onFinishInput()}. This
+     * method should be quick as it makes a unblocking IPC.
+     */
+    @MainThread
+    void notifyOnFinishInput() {
+        if (DEBUG) Log.d(TAG, "notifyOnFinishInput");
+        mImeClientPackageName = null;
+        mImeClientFieldId = null;
+        mImeInputViewStarted = false;
+        mImeInputStarted = false;
+        if (mSession != null && mSession.shouldSendImeStatus()) {
+            try {
+                mSession.getRequestCallback().onInputMethodFinishInput();
+            } catch (RemoteException e) {
+                Log.w(TAG, "onInputMethodFinishInput() remote exception:" + e);
+            }
+        }
+    }
+
+    /**
+     * Returns true if the current Ime focused field matches the session {@code requestInfo}.
+     */
+    @MainThread
+    boolean match(@Nullable InlineSuggestionsRequestInfo requestInfo) {
+        return match(requestInfo, mImeClientPackageName, mImeClientFieldId);
+    }
+
+    /**
+     * Returns true if the current Ime focused field matches the {@code autofillId}.
+     */
+    @MainThread
+    boolean match(@Nullable AutofillId autofillId) {
+        return match(autofillId, mImeClientFieldId);
+    }
+
+    private static boolean match(
+            @Nullable InlineSuggestionsRequestInfo inlineSuggestionsRequestInfo,
+            @Nullable String imeClientPackageName, @Nullable AutofillId imeClientFieldId) {
+        if (inlineSuggestionsRequestInfo == null || imeClientPackageName == null
+                || imeClientFieldId == null) {
+            return false;
+        }
+        return inlineSuggestionsRequestInfo.getComponentName().getPackageName().equals(
+                imeClientPackageName) && match(inlineSuggestionsRequestInfo.getAutofillId(),
+                imeClientFieldId);
+    }
+
+    private static boolean match(@Nullable AutofillId autofillId,
+            @Nullable AutofillId imeClientFieldId) {
+        // The IME doesn't have information about the virtual view id for the child views in the
+        // web view, so we are only comparing the parent view id here. This means that for cases
+        // where there are two input fields in the web view, they will have the same view id
+        // (although different virtual child id), and we will not be able to distinguish them.
+        return autofillId != null && imeClientFieldId != null
+                && autofillId.getViewId() == imeClientFieldId.getViewId();
+    }
+}
diff --git a/android/inputmethodservice/InputMethodService.java b/android/inputmethodservice/InputMethodService.java
new file mode 100644
index 0000000..d3464fd
--- /dev/null
+++ b/android/inputmethodservice/InputMethodService.java
@@ -0,0 +1,3337 @@
+/*
+ * 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 static android.view.ViewRootImpl.NEW_INSETS_MODE_NONE;
+import static android.view.WindowInsets.Type.navigationBars;
+import static android.view.WindowInsets.Type.statusBars;
+import static android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.annotation.AnyThread;
+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.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.content.pm.PackageManager;
+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.Binder;
+import android.os.Build;
+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.ViewRootImpl;
+import android.view.ViewTreeObserver;
+import android.view.Window;
+import android.view.WindowInsets;
+import android.view.WindowInsets.Side;
+import android.view.WindowManager;
+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.InlineSuggestionsRequest;
+import android.view.inputmethod.InlineSuggestionsResponse;
+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 com.android.internal.annotations.GuardedBy;
+import com.android.internal.inputmethod.IInputContentUriToken;
+import com.android.internal.inputmethod.IInputMethodPrivilegedOperations;
+import com.android.internal.inputmethod.InputMethodPrivilegedOperations;
+import com.android.internal.inputmethod.InputMethodPrivilegedOperationsRegistry;
+import com.android.internal.view.IInlineSuggestionsRequestCallback;
+import com.android.internal.view.InlineSuggestionsRequestInfo;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Collections;
+
+/**
+ * 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;
+
+    /**
+     * Allows the system to optimize the back button affordance based on the presence of software
+     * keyboard.
+     *
+     * <p>For instance, on devices that have navigation bar and software-rendered back button, the
+     * system may use a different icon while {@link #isInputViewShown()} returns {@code true}, to
+     * indicate that the back button has "dismiss" affordance.</p>
+     *
+     * <p>Note that {@link KeyEvent#KEYCODE_BACK} events continue to be sent to
+     * {@link #onKeyDown(int, KeyEvent)} even when this mode is specified. The default
+     * implementation of {@link #onKeyDown(int, KeyEvent)} for {@link KeyEvent#KEYCODE_BACK} does
+     * not take this mode into account.</p>
+     *
+     * <p>For API level {@link android.os.Build.VERSION_CODES#O_MR1} and lower devices, this is the
+     * only mode you can safely specify without worrying about the compatibility.</p>
+     *
+     * @see #setBackDisposition(int)
+     */
+    public static final int BACK_DISPOSITION_DEFAULT = 0;
+
+    /**
+     * Deprecated flag.
+     *
+     * <p>To avoid compatibility issues, IME developers should not use this flag.</p>
+     *
+     * @deprecated on {@link android.os.Build.VERSION_CODES#P} and later devices, this flag is
+     *             handled as a synonym of {@link #BACK_DISPOSITION_DEFAULT}. On
+     *             {@link android.os.Build.VERSION_CODES#O_MR1} and prior devices, expected behavior
+     *             of this mode had not been well defined. Most likely the end result would be the
+     *             same as {@link #BACK_DISPOSITION_DEFAULT}. Either way it is not recommended to
+     *             use this mode
+     * @see #setBackDisposition(int)
+     */
+    @Deprecated
+    public static final int BACK_DISPOSITION_WILL_NOT_DISMISS = 1;
+
+    /**
+     * Deprecated flag.
+     *
+     * <p>To avoid compatibility issues, IME developers should not use this flag.</p>
+     *
+     * @deprecated on {@link android.os.Build.VERSION_CODES#P} and later devices, this flag is
+     *             handled as a synonym of {@link #BACK_DISPOSITION_DEFAULT}. On
+     *             {@link android.os.Build.VERSION_CODES#O_MR1} and prior devices, expected behavior
+     *             of this mode had not been well defined. In AOSP implementation running on devices
+     *             that have navigation bar, specifying this flag could change the software back
+     *             button to "Dismiss" icon no matter whether the software keyboard is shown or not,
+     *             but there would be no easy way to restore the icon state even after IME lost the
+     *             connection to the application. To avoid user confusions, do not specify this mode
+     *             anyway
+     * @see #setBackDisposition(int)
+     */
+    @Deprecated
+    public static final int BACK_DISPOSITION_WILL_DISMISS = 2;
+
+    /**
+     * Asks the system to not adjust the back button affordance even when the software keyboard is
+     * shown.
+     *
+     * <p>This mode is useful for UI modes where IME's main soft input window is used for some
+     * supplemental UI, such as floating candidate window for languages such as Chinese and
+     * Japanese, where users expect the back button is, or at least looks to be, handled by the
+     * target application rather than the UI shown by the IME even while {@link #isInputViewShown()}
+     * returns {@code true}.</p>
+     *
+     * <p>Note that {@link KeyEvent#KEYCODE_BACK} events continue to be sent to
+     * {@link #onKeyDown(int, KeyEvent)} even when this mode is specified. The default
+     * implementation of {@link #onKeyDown(int, KeyEvent)} for {@link KeyEvent#KEYCODE_BACK} does
+     * not take this mode into account.</p>
+     *
+     * @see #setBackDisposition(int)
+     */
+    public static final int BACK_DISPOSITION_ADJUST_NOTHING = 3;
+
+    /**
+     * Enum flag to be used for {@link #setBackDisposition(int)}.
+     *
+     * @hide
+     */
+    @Retention(SOURCE)
+    @IntDef(value = {BACK_DISPOSITION_DEFAULT, BACK_DISPOSITION_WILL_NOT_DISMISS,
+            BACK_DISPOSITION_WILL_DISMISS, BACK_DISPOSITION_ADJUST_NOTHING},
+            prefix = "BACK_DISPOSITION_")
+    public @interface BackDispositionMode {}
+
+    /**
+     * @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;
+
+    /**
+     * @hide
+     * The IME is active and ready with views but set invisible.
+     * This flag cannot be combined with {@link #IME_VISIBLE}.
+     */
+    public static final int IME_INVISIBLE = 0x4;
+
+    // Min and max values for back disposition.
+    private static final int BACK_DISPOSITION_MIN = BACK_DISPOSITION_DEFAULT;
+    private static final int BACK_DISPOSITION_MAX = BACK_DISPOSITION_ADJUST_NOTHING;
+
+    InputMethodManager mImm;
+    private InputMethodPrivilegedOperations mPrivOps = new InputMethodPrivilegedOperations();
+
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    int mTheme = 0;
+    
+    LayoutInflater mInflater;
+    TypedArray mThemeAttrs;
+    @UnsupportedAppUsage
+    View mRootView;
+    SoftInputWindow mWindow;
+    boolean mInitialized;
+    boolean mViewsCreated;
+    // IME views visibility.
+    boolean mDecorViewVisible;
+    boolean mDecorViewWasVisible;
+    boolean mInShowWindow;
+    // True if pre-rendering of IME views/window is supported.
+    boolean mCanPreRender;
+    // If IME is pre-rendered.
+    boolean mIsPreRendered;
+    // IME window visibility.
+    // Use (mDecorViewVisible && mWindowVisible) to check if IME is visible to the user.
+    boolean mWindowVisible;
+
+    ViewGroup mFullscreenArea;
+    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 mFullscreenApplied;
+    boolean mIsFullscreen;
+    @UnsupportedAppUsage
+    View mExtractView;
+    boolean mExtractViewHidden;
+    @UnsupportedAppUsage
+    ExtractEditText mExtractEditText;
+    ViewGroup mExtractAccessories;
+    View mExtractAction;
+    ExtractedText mExtractedText;
+    int mExtractedToken;
+    
+    View mInputView;
+    boolean mIsInputViewShown;
+    
+    int mStatusIcon;
+
+    @BackDispositionMode
+    int mBackDisposition;
+
+    private Object mLock = new Object();
+    @GuardedBy("mLock")
+    private boolean mNotifyUserActionSent;
+
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    final Insets mTmpInsets = new Insets();
+    final int[] mTmpLocation = new int[2];
+
+    private InlineSuggestionSessionController mInlineSuggestionSessionController;
+
+    private boolean mAutomotiveHideNavBarForKeyboard;
+    private boolean mIsAutomotive;
+
+    /**
+     * An opaque {@link Binder} token of window requesting {@link InputMethodImpl#showSoftInput}
+     * The original app window token is passed from client app window.
+     * {@link com.android.server.inputmethod.InputMethodManagerService} creates a unique dummy
+     * token to identify this window.
+     * This dummy token is only valid for a single call to {@link InputMethodImpl#showSoftInput},
+     * after which it is set null until next call.
+     */
+    private IBinder mCurShowInputToken;
+
+    /**
+     * An opaque {@link Binder} token of window requesting {@link InputMethodImpl#hideSoftInput}
+     * The original app window token is passed from client app window.
+     * {@link com.android.server.inputmethod.InputMethodManagerService} creates a unique dummy
+     * token to identify this window.
+     * This dummy token is only valid for a single call to {@link InputMethodImpl#hideSoftInput},
+     * after which it is set {@code null} until next call.
+     */
+    private IBinder mCurHideInputToken;
+
+    final ViewTreeObserver.OnComputeInternalInsetsListener mInsetsComputer = info -> {
+        onComputeInsets(mTmpInsets);
+        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 {
+            info.contentInsets.top = mTmpInsets.contentTopInsets;
+            info.visibleInsets.top = mTmpInsets.visibleTopInsets;
+            info.touchableRegion.set(mTmpInsets.touchableRegion);
+            info.setTouchableInsets(mTmpInsets.touchableInsets);
+        }
+
+        if (mInputFrame != null) {
+            setImeExclusionRect(mTmpInsets.visibleTopInsets);
+        }
+    };
+
+    final View.OnClickListener mActionClickListener = 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 {
+
+        private boolean mSystemCallingShowSoftInput;
+        private boolean mSystemCallingHideSoftInput;
+
+        /**
+         * {@inheritDoc}
+         * @hide
+         */
+        @MainThread
+        @Override
+        public final void initializeInternal(@NonNull IBinder token, int displayId,
+                IInputMethodPrivilegedOperations privilegedOperations) {
+            if (InputMethodPrivilegedOperationsRegistry.isRegistered(token)) {
+                Log.w(TAG, "The token has already registered, ignore this initialization.");
+                return;
+            }
+            mPrivOps.set(privilegedOperations);
+            InputMethodPrivilegedOperationsRegistry.put(token, mPrivOps);
+            updateInputMethodDisplay(displayId);
+            attachToken(token);
+        }
+
+        /**
+         * {@inheritDoc}
+         * @hide
+         */
+        @MainThread
+        @Override
+        public void onCreateInlineSuggestionsRequest(
+                @NonNull InlineSuggestionsRequestInfo requestInfo,
+                @NonNull IInlineSuggestionsRequestCallback cb) {
+            if (DEBUG) {
+                Log.d(TAG, "InputMethodService received onCreateInlineSuggestionsRequest()");
+            }
+            mInlineSuggestionSessionController.onMakeInlineSuggestionsRequest(requestInfo, cb);
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @MainThread
+        @Override
+        public void attachToken(IBinder token) {
+            if (mToken != null) {
+                throw new IllegalStateException(
+                        "attachToken() must be called at most once. token=" + token);
+            }
+            mToken = token;
+            mWindow.setToken(token);
+        }
+
+        /**
+         * {@inheritDoc}
+         * @hide
+         */
+        @MainThread
+        @Override
+        public void updateInputMethodDisplay(int displayId) {
+            // Update display for adding IME window to the right display.
+            // TODO(b/111364446) Need to address context lifecycle issue if need to re-create
+            // for update resources & configuration correctly when show soft input
+            // in non-default display.
+            updateDisplay(displayId);
+        }
+
+        /**
+         * {@inheritDoc}
+         *
+         * <p>Calls {@link InputMethodService#onBindInput()} when done.</p>
+         */
+        @MainThread
+        @Override
+        public void bindInput(InputBinding binding) {
+            mInputBinding = binding;
+            mInputConnection = binding.getConnection();
+            if (DEBUG) Log.v(TAG, "bindInput(): binding=" + binding
+                    + " ic=" + mInputConnection);
+            reportFullscreenMode();
+            initialize();
+            onBindInput();
+        }
+
+        /**
+         * {@inheritDoc}
+         *
+         * <p>Calls {@link InputMethodService#onUnbindInput()} when done.</p>
+         */
+        @MainThread
+        @Override
+        public void unbindInput() {
+            if (DEBUG) Log.v(TAG, "unbindInput(): binding=" + mInputBinding
+                    + " ic=" + mInputConnection);
+            // Unbind input is per process per display.
+            // TODO(b/150902448): free-up IME surface when target is changing.
+            //  e.g. DisplayContent#setInputMethodTarget()
+            removeImeSurface();
+            onUnbindInput();
+            mInputBinding = null;
+            mInputConnection = null;
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @MainThread
+        @Override
+        public void startInput(InputConnection ic, EditorInfo attribute) {
+            if (DEBUG) Log.v(TAG, "startInput(): editor=" + attribute);
+            doStartInput(ic, attribute, false);
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @MainThread
+        @Override
+        public void restartInput(InputConnection ic, EditorInfo attribute) {
+            if (DEBUG) Log.v(TAG, "restartInput(): editor=" + attribute);
+            doStartInput(ic, attribute, true);
+        }
+
+        /**
+         * {@inheritDoc}
+         * @hide
+         */
+        @MainThread
+        @Override
+        public final void dispatchStartInputWithToken(@Nullable InputConnection inputConnection,
+                @NonNull EditorInfo editorInfo, boolean restarting,
+                @NonNull IBinder startInputToken, boolean shouldPreRenderIme) {
+            mPrivOps.reportStartInput(startInputToken);
+            mCanPreRender = shouldPreRenderIme;
+            if (DEBUG) Log.v(TAG, "Will Pre-render IME: " + mCanPreRender);
+
+            if (restarting) {
+                restartInput(inputConnection, editorInfo);
+            } else {
+                startInput(inputConnection, editorInfo);
+            }
+        }
+
+        /**
+         * {@inheritDoc}
+         * @hide
+         */
+        @MainThread
+        @Override
+        public void hideSoftInputWithToken(int flags, ResultReceiver resultReceiver,
+                IBinder hideInputToken) {
+            mSystemCallingHideSoftInput = true;
+            mCurHideInputToken = hideInputToken;
+            hideSoftInput(flags, resultReceiver);
+            mCurHideInputToken = null;
+            mSystemCallingHideSoftInput = false;
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @MainThread
+        @Override
+        public void hideSoftInput(int flags, ResultReceiver resultReceiver) {
+            if (DEBUG) Log.v(TAG, "hideSoftInput()");
+            if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.R
+                    && !mSystemCallingHideSoftInput) {
+                Log.e(TAG, "IME shouldn't call hideSoftInput on itself."
+                        + " Use requestHideSelf(int) itself");
+                return;
+            }
+            final boolean wasVisible = mIsPreRendered
+                    ? mDecorViewVisible && mWindowVisible : isInputViewShown();
+            applyVisibilityInInsetsConsumerIfNecessary(false /* setVisible */);
+            if (mIsPreRendered) {
+                if (DEBUG) {
+                    Log.v(TAG, "Making IME window invisible");
+                }
+                setImeWindowStatus(IME_ACTIVE | IME_INVISIBLE, mBackDisposition);
+                onPreRenderedWindowVisibilityChanged(false /* setVisible */);
+            } else {
+                mShowInputFlags = 0;
+                mShowInputRequested = false;
+                doHideWindow();
+            }
+            final boolean isVisible = mIsPreRendered
+                    ? mDecorViewVisible && mWindowVisible : isInputViewShown();
+            final boolean visibilityChanged = isVisible != wasVisible;
+            if (resultReceiver != null) {
+                resultReceiver.send(visibilityChanged
+                        ? InputMethodManager.RESULT_HIDDEN
+                        : (wasVisible ? InputMethodManager.RESULT_UNCHANGED_SHOWN
+                                : InputMethodManager.RESULT_UNCHANGED_HIDDEN), null);
+            }
+        }
+
+        /**
+         * {@inheritDoc}
+         * @hide
+         */
+        @MainThread
+        @Override
+        public void showSoftInputWithToken(int flags, ResultReceiver resultReceiver,
+                IBinder showInputToken) {
+            mSystemCallingShowSoftInput = true;
+            mCurShowInputToken = showInputToken;
+            showSoftInput(flags, resultReceiver);
+            mCurShowInputToken = null;
+            mSystemCallingShowSoftInput = false;
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @MainThread
+        @Override
+        public void showSoftInput(int flags, ResultReceiver resultReceiver) {
+            if (DEBUG) Log.v(TAG, "showSoftInput()");
+            // TODO(b/148086656): Disallow IME developers from calling InputMethodImpl methods.
+            if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.R
+                    && !mSystemCallingShowSoftInput) {
+                Log.e(TAG," IME shouldn't call showSoftInput on itself."
+                        + " Use requestShowSelf(int) itself");
+                return;
+            }
+            final boolean wasVisible = mIsPreRendered
+                    ? mDecorViewVisible && mWindowVisible : isInputViewShown();
+            if (dispatchOnShowInputRequested(flags, false)) {
+                if (mIsPreRendered) {
+                    if (DEBUG) {
+                        Log.v(TAG, "Making IME window visible");
+                    }
+                    onPreRenderedWindowVisibilityChanged(true /* setVisible */);
+                } else {
+                    showWindow(true);
+                }
+                applyVisibilityInInsetsConsumerIfNecessary(true /* setVisible */);
+            }
+            // If user uses hard keyboard, IME button should always be shown.
+            setImeWindowStatus(mapToImeWindowStatus(), mBackDisposition);
+            final boolean isVisible = mIsPreRendered
+                    ? mDecorViewVisible && mWindowVisible : isInputViewShown();
+            final boolean visibilityChanged = isVisible != wasVisible;
+            if (resultReceiver != null) {
+                resultReceiver.send(visibilityChanged
+                        ? InputMethodManager.RESULT_SHOWN
+                        : (wasVisible ? InputMethodManager.RESULT_UNCHANGED_SHOWN
+                                : InputMethodManager.RESULT_UNCHANGED_HIDDEN), null);
+            }
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @MainThread
+        @Override
+        public void changeInputMethodSubtype(InputMethodSubtype subtype) {
+            dispatchOnCurrentInputMethodSubtypeChanged(subtype);
+        }
+
+        /**
+         * {@inheritDoc}
+         * @hide
+         */
+        @Override
+        public void setCurrentShowInputToken(IBinder showInputToken) {
+            mCurShowInputToken = showInputToken;
+        }
+
+        /**
+         * {@inheritDoc}
+         * @hide
+         */
+        @Override
+        public void setCurrentHideInputToken(IBinder hideInputToken) {
+            mCurHideInputToken = hideInputToken;
+        }
+    }
+
+    /**
+     * Called when Autofill is requesting an {@link InlineSuggestionsRequest} from the IME.
+     *
+     * <p>The Autofill Framework will first request the IME to create and send an
+     * {@link InlineSuggestionsRequest} back. Once Autofill Framework receives a valid request and
+     * also receives valid inline suggestions, they will be returned via
+     * {@link #onInlineSuggestionsResponse(InlineSuggestionsResponse)}.</p>
+     *
+     * <p>IME Lifecycle - The request will wait to be created after inputStarted</p>
+     *
+     * <p>If the IME wants to support displaying inline suggestions, they must set
+     * supportsInlineSuggestions in its XML and implement this method to return a valid
+     * {@link InlineSuggestionsRequest}.</p>
+     *
+     * @param uiExtras the extras that contain the UI renderer related information
+     * @return an {@link InlineSuggestionsRequest} to be sent to Autofill.
+     */
+    @Nullable
+    public InlineSuggestionsRequest onCreateInlineSuggestionsRequest(@NonNull Bundle uiExtras) {
+        return null;
+    }
+
+    /**
+     * Called when Autofill responds back with {@link InlineSuggestionsResponse} containing
+     * inline suggestions.
+     *
+     * <p>Should be implemented by subclasses.</p>
+     *
+     * @param response {@link InlineSuggestionsResponse} passed back by Autofill.
+     * @return Whether the IME will use and render  the inline suggestions.
+     */
+    public boolean onInlineSuggestionsResponse(@NonNull InlineSuggestionsResponse response) {
+        return false;
+    }
+
+    /**
+     * Returns the {@link IBinder} input token from the host view root.
+     */
+    @Nullable
+    private IBinder getHostInputToken() {
+        ViewRootImpl viewRoot = null;
+        if (mRootView != null) {
+            viewRoot = mRootView.getViewRootImpl();
+        }
+        return viewRoot == null ? null : viewRoot.getInputToken();
+    }
+
+    private void notifyImeHidden() {
+        requestHideSelf(0);
+    }
+
+    private void removeImeSurface() {
+        if (!mShowInputRequested && !mWindowVisible) {
+            // hiding a window removes its surface.
+            mWindow.hide();
+        }
+    }
+
+    private void setImeWindowStatus(int visibilityFlags, int backDisposition) {
+        mPrivOps.setImeWindowStatus(visibilityFlags, backDisposition);
+    }
+
+    /** Set region of the keyboard to be avoided from back gesture */
+    private void setImeExclusionRect(int visibleTopInsets) {
+        View inputFrameRootView = mInputFrame.getRootView();
+        Rect r = new Rect(0, visibleTopInsets, inputFrameRootView.getWidth(),
+                inputFrameRootView.getHeight());
+        inputFrameRootView.setSystemGestureExclusionRects(Collections.singletonList(r));
+    }
+
+    /**
+     * 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);
+        }
+
+        /**
+         * Notify IME that window is hidden.
+         * @hide
+         */
+        public final void notifyImeHidden() {
+            InputMethodService.this.notifyImeHidden();
+        }
+
+        /**
+         * Notify IME that surface can be now removed.
+         * @hide
+         */
+        public final void removeImeSurface() {
+            InputMethodService.this.removeImeSurface();
+        }
+    }
+    
+    /**
+     * 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);
+        }
+
+        @UnsupportedAppUsage
+        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  + "}";
+        }
+    }
+    @UnsupportedAppUsage
+    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);
+
+        mIsAutomotive = isAutomotive();
+        mAutomotiveHideNavBarForKeyboard = getApplicationContext().getResources().getBoolean(
+                com.android.internal.R.bool.config_automotiveHideNavBarForKeyboard);
+
+        // TODO(b/111364446) Need to address context lifecycle issue if need to re-create
+        // for update resources & configuration correctly when show soft input
+        // in non-default display.
+        mInflater = (LayoutInflater)getSystemService(
+                Context.LAYOUT_INFLATER_SERVICE);
+        mWindow = new SoftInputWindow(this, "InputMethod", mTheme, null, null, mDispatcherState,
+                WindowManager.LayoutParams.TYPE_INPUT_METHOD, Gravity.BOTTOM, false);
+        mWindow.getWindow().getAttributes().setFitInsetsTypes(statusBars() | navigationBars());
+        mWindow.getWindow().getAttributes().setFitInsetsSides(Side.all() & ~Side.BOTTOM);
+        mWindow.getWindow().getAttributes().setFitInsetsIgnoringVisibility(true);
+
+        // IME layout should always be inset by navigation bar, no matter its current visibility,
+        // unless automotive requests it, since automotive may hide the navigation bar.
+        mWindow.getWindow().getDecorView().setOnApplyWindowInsetsListener(
+                (v, insets) -> v.onApplyWindowInsets(
+                        new WindowInsets.Builder(insets).setInsets(
+                                navigationBars(),
+                                mIsAutomotive && mAutomotiveHideNavBarForKeyboard
+                                        ? android.graphics.Insets.NONE
+                                        : insets.getInsetsIgnoringVisibility(navigationBars())
+                                )
+                                .build()));
+
+        // For ColorView in DecorView to work, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS needs to be set
+        // by default (but IME developers can opt this out later if they want a new behavior).
+        mWindow.getWindow().setFlags(
+                FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
+
+        initViews();
+        mWindow.getWindow().setLayout(MATCH_PARENT, WRAP_CONTENT);
+
+        mInlineSuggestionSessionController = new InlineSuggestionSessionController(
+                this::onCreateInlineSuggestionsRequest, this::getHostInputToken,
+                this::onInlineSuggestionsResponse);
+    }
+
+    /**
+     * 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;
+        mViewsCreated = false;
+        mShowInputRequested = false;
+        mShowInputFlags = 0;
+
+        mThemeAttrs = obtainStyledAttributes(android.R.styleable.InputMethodService);
+        mRootView = mInflater.inflate(
+                com.android.internal.R.layout.input_method, null);
+        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 = mRootView.findViewById(com.android.internal.R.id.fullscreenArea);
+        mExtractViewHidden = false;
+        mExtractFrame = mRootView.findViewById(android.R.id.extractArea);
+        mExtractView = null;
+        mExtractEditText = null;
+        mExtractAccessories = null;
+        mExtractAction = null;
+        mFullscreenApplied = false;
+
+        mCandidatesFrame = mRootView.findViewById(android.R.id.candidatesArea);
+        mInputFrame = 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();
+        mWindow.dismissForDestroyIfNecessary();
+        if (mSettingsObserver != null) {
+            mSettingsObserver.unregister();
+            mSettingsObserver = null;
+        }
+        if (mToken != null) {
+            // This is completely optional, but allows us to show more explicit error messages
+            // when IME developers are doing something unsupported.
+            InputMethodPrivilegedOperationsRegistry.remove(mToken);
+        }
+    }
+
+    /**
+     * 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 = mDecorViewVisible;
+        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();
+            setImeWindowStatus(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;
+    }
+
+    /**
+     * Sets the disposition mode that indicates the expected affordance for the back button.
+     *
+     * <p>Keep in mind that specifying this flag does not change the the default behavior of
+     * {@link #onKeyDown(int, KeyEvent)}.  It is IME developers' responsibility for making sure that
+     * their custom implementation of {@link #onKeyDown(int, KeyEvent)} is consistent with the mode
+     * specified to this API.</p>
+     *
+     * @see #getBackDisposition()
+     * @param disposition disposition mode to be set
+     */
+    public void setBackDisposition(@BackDispositionMode int disposition) {
+        if (disposition == mBackDisposition) {
+            return;
+        }
+        if (disposition > BACK_DISPOSITION_MAX || disposition < BACK_DISPOSITION_MIN) {
+            Log.e(TAG, "Invalid back disposition value (" + disposition + ") specified.");
+            return;
+        }
+        mBackDisposition = disposition;
+        setImeWindowStatus(mapToImeWindowStatus(), mBackDisposition);
+    }
+
+    /**
+     * Retrieves the current disposition mode that indicates the expected back button affordance.
+     *
+     * @see #setBackDisposition(int)
+     * @return currently selected disposition mode
+     */
+    @BackDispositionMode
+    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() {
+        final WindowManager windowManager = getSystemService(WindowManager.class);
+        final Rect windowBounds = windowManager.getCurrentWindowMetrics().getBounds();
+        return windowBounds.width();
+    }
+    
+    /**
+     * 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;
+    }
+
+    /**
+     * Force switch to the last used input method and subtype. If the last input method didn't have
+     * any subtypes, the framework will simply switch to the last input method with no subtype
+     * specified.
+     * @return true if the current input method and subtype was successfully switched to the last
+     * used input method and subtype.
+     */
+    public final boolean switchToPreviousInputMethod() {
+        return mPrivOps.switchToPreviousInputMethod();
+    }
+
+    /**
+     * Force switch to the next input method and subtype. If there is no IME enabled except
+     * current IME and subtype, do nothing.
+     * @param onlyCurrentIme if true, the framework will find the next subtype which
+     * belongs to the current IME
+     * @return true if the current input method and subtype was successfully switched to the next
+     * input method and subtype.
+     */
+    public final boolean switchToNextInputMethod(boolean onlyCurrentIme) {
+        return mPrivOps.switchToNextInputMethod(onlyCurrentIme);
+    }
+
+    /**
+     * Returns true if the current IME needs to offer the users ways to switch to a next input
+     * method (e.g. a globe key.).
+     * When an IME sets supportsSwitchingToNextInputMethod and this method returns true,
+     * the IME has to offer ways to to invoke {@link #switchToNextInputMethod} accordingly.
+     * <p> Note that the system determines the most appropriate next input method
+     * and subtype in order to provide the consistent user experience in switching
+     * between IMEs and subtypes.
+     */
+    public final boolean shouldOfferSwitchingToNextInputMethod() {
+        return mPrivOps.shouldOfferSwitchingToNextInputMethod();
+    }
+
+    public boolean getCurrentInputStarted() {
+        return mInputStarted;
+    }
+    
+    public EditorInfo getCurrentInputEditorInfo() {
+        return mInputEditorInfo;
+    }
+
+    private void reportFullscreenMode() {
+        mPrivOps.reportFullscreenMode(mIsFullscreen);
+    }
+
+    /**
+     * 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;
+            reportFullscreenMode();
+            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 (mDecorViewWasVisible && 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 && mDecorViewVisible) {
+            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 mCanPreRender ? mWindowVisible : mIsInputViewShown && mDecorViewVisible;
+    }
+
+    /**
+     * 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 && mDecorViewVisible != 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;
+        mPrivOps.updateStatusIcon(getPackageName(), iconResId);
+    }
+
+    public void hideStatusIcon() {
+        mStatusIcon = 0;
+        mPrivOps.updateStatusIcon(null, 0);
+    }
+
+    /**
+     * 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 to start.
+     */
+    public void switchInputMethod(String id) {
+        mPrivOps.setInputMethod(id);
+    }
+
+    /**
+     * Force switch to a new input method, as identified by {@code 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 to start.
+     * @param subtype The new subtype of the new input method to be switched to.
+     */
+    public final void switchInputMethod(String id, InputMethodSubtype subtype) {
+        mPrivOps.setInputMethodAndSubtype(id, subtype);
+    }
+
+    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 = view.findViewById(
+                    com.android.internal.R.id.inputExtractEditText);
+            mExtractEditText.setIME(this);
+            mExtractAction = view.findViewById(
+                    com.android.internal.R.id.inputExtractAction);
+            if (mExtractAction != null) {
+                mExtractAccessories = 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);
+        mInlineSuggestionSessionController.notifyOnShowInputRequested(result);
+        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
+                + " mViewsCreated=" + mViewsCreated
+                + " mDecorViewVisible=" + mDecorViewVisible
+                + " mWindowVisible=" + mWindowVisible
+                + " mInputStarted=" + mInputStarted
+                + " mShowInputFlags=" + mShowInputFlags);
+
+        if (mInShowWindow) {
+            Log.w(TAG, "Re-entrance in to showWindow");
+            return;
+        }
+
+        mDecorViewWasVisible = mDecorViewVisible;
+        mInShowWindow = true;
+        boolean isPreRenderedAndInvisible = mIsPreRendered && !mWindowVisible;
+        final int previousImeWindowStatus =
+                (mDecorViewVisible ? IME_ACTIVE : 0) | (isInputViewShown()
+                        ? (isPreRenderedAndInvisible ? IME_INVISIBLE : IME_VISIBLE) : 0);
+        startViews(prepareWindow(showInput));
+        final int nextImeWindowStatus = mapToImeWindowStatus();
+        if (previousImeWindowStatus != nextImeWindowStatus) {
+            setImeWindowStatus(nextImeWindowStatus, mBackDisposition);
+        }
+
+        // compute visibility
+        onWindowShown();
+        mIsPreRendered = mCanPreRender;
+        if (mIsPreRendered) {
+            onPreRenderedWindowVisibilityChanged(true /* setVisible */);
+        } else {
+            // Pre-rendering not supported.
+            if (DEBUG) Log.d(TAG, "No pre-rendering supported");
+            mWindowVisible = true;
+        }
+
+        // request draw for the IME surface.
+        // When IME is not pre-rendered, this will actually show the IME.
+        if ((previousImeWindowStatus & IME_ACTIVE) == 0) {
+            if (DEBUG) Log.v(TAG, "showWindow: draw decorView!");
+            mWindow.show();
+        }
+        maybeNotifyPreRendered();
+        mDecorViewWasVisible = true;
+        mInShowWindow = false;
+    }
+
+    /**
+     * Notify {@link android.view.ImeInsetsSourceConsumer} if IME has been pre-rendered
+     * for current EditorInfo, when pre-rendering is enabled.
+     */
+    private void maybeNotifyPreRendered() {
+        if (!mCanPreRender || !mIsPreRendered) {
+            return;
+        }
+        mPrivOps.reportPreRendered(getCurrentInputEditorInfo());
+    }
+
+
+    private boolean prepareWindow(boolean showInput) {
+        boolean doShowInput = false;
+        mDecorViewVisible = true;
+        if (!mShowInputRequested && mInputStarted && showInput) {
+            doShowInput = true;
+            mShowInputRequested = true;
+        }
+
+        if (DEBUG) Log.v(TAG, "showWindow: updating UI");
+        initialize();
+        updateFullscreenMode();
+        updateInputViewShown();
+
+        if (!mViewsCreated) {
+            mViewsCreated = 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);
+            }
+        }
+        return doShowInput;
+    }
+
+    private void startViews(boolean doShowInput) {
+        if (mShowInputRequested) {
+            if (!mInputViewStarted) {
+                if (DEBUG) Log.v(TAG, "CALL: onStartInputView");
+                mInputViewStarted = true;
+                mInlineSuggestionSessionController.notifyOnStartInputView();
+                onStartInputView(mInputEditorInfo, false);
+            }
+        } else if (!mCandidatesViewStarted) {
+            if (DEBUG) Log.v(TAG, "CALL: onStartCandidatesView");
+            mCandidatesViewStarted = true;
+            onStartCandidatesView(mInputEditorInfo, false);
+        }
+        if (doShowInput) startExtractingText(false);
+    }
+
+    private void onPreRenderedWindowVisibilityChanged(boolean setVisible) {
+        mWindowVisible = setVisible;
+        mShowInputFlags = setVisible ? mShowInputFlags : 0;
+        mShowInputRequested = setVisible;
+        mDecorViewVisible = setVisible;
+        if (setVisible) {
+            onWindowShown();
+        }
+    }
+
+    /**
+     * Apply the IME visibility in {@link android.view.ImeInsetsSourceConsumer} when
+     * {@link ViewRootImpl.sNewInsetsMode} is enabled.
+     * @param setVisible {@code true} to make it visible, false to hide it.
+     */
+    private void applyVisibilityInInsetsConsumerIfNecessary(boolean setVisible) {
+        if (!isVisibilityAppliedUsingInsetsConsumer()) {
+            return;
+        }
+        mPrivOps.applyImeVisibility(setVisible
+                ? mCurShowInputToken : mCurHideInputToken, setVisible);
+    }
+
+    private boolean isVisibilityAppliedUsingInsetsConsumer() {
+        return ViewRootImpl.sNewInsetsMode > NEW_INSETS_MODE_NONE;
+    }
+
+    private void finishViews(boolean finishingInput) {
+        if (mInputViewStarted) {
+            if (DEBUG) Log.v(TAG, "CALL: onFinishInputView");
+            mInlineSuggestionSessionController.notifyOnFinishInputView();
+            onFinishInputView(finishingInput);
+        } else if (mCandidatesViewStarted) {
+            if (DEBUG) Log.v(TAG, "CALL: onFinishCandidatesView");
+            onFinishCandidatesView(finishingInput);
+        }
+        mInputViewStarted = false;
+        mCandidatesViewStarted = false;
+    }
+
+    private void doHideWindow() {
+        setImeWindowStatus(0, mBackDisposition);
+        hideWindow();
+    }
+
+    public void hideWindow() {
+        if (DEBUG) Log.v(TAG, "CALL: hideWindow");
+        mIsPreRendered = false;
+        mWindowVisible = false;
+        finishViews(false /* finishingInput */);
+        if (mDecorViewVisible) {
+            // When insets API is enabled, it is responsible for client and server side
+            // visibility of IME window.
+            if (isVisibilityAppliedUsingInsetsConsumer()) {
+                if (mInputView != null) {
+                    mInputView.dispatchWindowVisibilityChanged(View.GONE);
+                }
+            } else {
+                mWindow.hide();
+            }
+            mDecorViewVisible = false;
+            onWindowHidden();
+            mDecorViewWasVisible = false;
+        }
+        updateFullscreenMode();
+    }
+
+    /**
+     * Called immediately before the input method window is shown to the user.
+     * You could override this to prepare for the window to be shown
+     * (update view structure 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
+    }
+
+    /**
+     * 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 (DEBUG) Log.v(TAG, "CALL: doFinishInput");
+        finishViews(true /* finishingInput */);
+        if (mInputStarted) {
+            mInlineSuggestionSessionController.notifyOnFinishInput();
+            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();
+        mInlineSuggestionSessionController.notifyOnStartInput(
+                attribute == null ? null : attribute.packageName,
+                attribute == null ? null : attribute.autofillId);
+        if (DEBUG) Log.v(TAG, "CALL: onStartInput");
+        onStartInput(attribute, restarting);
+        if (mDecorViewVisible) {
+            if (mShowInputRequested) {
+                if (DEBUG) Log.v(TAG, "CALL: onStartInputView");
+                mInputViewStarted = true;
+                mInlineSuggestionSessionController.notifyOnStartInputView();
+                onStartInputView(mInputEditorInfo, restarting);
+                startExtractingText(true);
+            } else if (mCandidatesVisibility == View.VISIBLE) {
+                if (DEBUG) Log.v(TAG, "CALL: onStartCandidatesView");
+                mCandidatesViewStarted = true;
+                onStartCandidatesView(mInputEditorInfo, restarting);
+            }
+        } else if (mCanPreRender && mInputEditorInfo != null && mStartedInputConnection != null) {
+            // Pre-render IME views and window when real EditorInfo is available.
+            // pre-render IME window and keep it invisible.
+            if (DEBUG) Log.v(TAG, "Pre-Render IME for " + mInputEditorInfo.fieldName);
+            if (mInShowWindow) {
+                Log.w(TAG, "Re-entrance in to showWindow");
+                return;
+            }
+
+            mDecorViewWasVisible = mDecorViewVisible;
+            mInShowWindow = true;
+            startViews(prepareWindow(true /* showInput */));
+
+            // compute visibility
+            mIsPreRendered = true;
+            onPreRenderedWindowVisibilityChanged(false /* setVisible */);
+
+            // request draw for the IME surface.
+            // When IME is not pre-rendered, this will actually show the IME.
+            if (DEBUG) Log.v(TAG, "showWindow: draw decorView!");
+            mWindow.show();
+            maybeNotifyPreRendered();
+            mDecorViewWasVisible = true;
+            mInShowWindow = false;
+        } else {
+            mIsPreRendered = false;
+        }
+    }
+    
+    /**
+     * 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.
+     * @see InputMethodManager#viewClicked(View)
+     * @deprecated The method may not be called for composite {@link View} that works as a giant
+     *             "Canvas", which can host its own UI hierarchy and sub focus state.
+     *             {@link android.webkit.WebView} is a good example. Application / IME developers
+     *             should not rely on this method. If your goal is just being notified when an
+     *             on-going input is interrupted, simply monitor {@link #onFinishInput()}.
+     */
+    @Deprecated
+    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.
+     *
+     * @see InputMethodManager#HIDE_IMPLICIT_ONLY
+     * @see InputMethodManager#HIDE_NOT_ALWAYS
+     * @param flags Provides additional operating flags.
+     */
+    public void requestHideSelf(int flags) {
+        mPrivOps.hideMySoftInput(flags);
+    }
+
+    /**
+     * Show the input method's soft input area, so the user sees the input method window and can
+     * interact with it.
+     *
+     * @see InputMethodManager#SHOW_IMPLICIT
+     * @see InputMethodManager#SHOW_FORCED
+     * @param flags Provides additional operating flags.
+     */
+    public final void requestShowSelf(int flags) {
+        mPrivOps.showMySoftInput(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 (mDecorViewVisible) {
+            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;
+    }
+
+    /**
+     * Called back when a {@link KeyEvent} is forwarded from the target application.
+     *
+     * <p>The default implementation intercepts {@link 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.</p>
+     *
+     * <p>The default implementation does not take flags specified to
+     * {@link #setBackDisposition(int)} into account, even on API version
+     * {@link android.os.Build.VERSION_CODES#P} and later devices.  IME developers are responsible
+     * for making sure that their special handling for {@link KeyEvent#KEYCODE_BACK} are consistent
+     * with the flag they specified to {@link #setBackDisposition(int)}.</p>
+     *
+     * @param keyCode The value in {@code event.getKeyCode()}
+     * @param event Description of the key event
+     *
+     * @return {@code true} if the event is consumed by the IME and the application no longer needs
+     *         to consume it.  Return {@code false} when the event should be handled as if the IME
+     *         had not seen the event at all.
+     */
+    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
+     */
+    @UnsupportedAppUsage
+    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
+     */
+    @UnsupportedAppUsage
+    public void onExtractedReplaceText(int start, int end, CharSequence text) {
+        InputConnection conn = getCurrentInputConnection();
+        if (conn != null) {
+            conn.setComposingRegion(start, end);
+            conn.commitText(text, 1);
+        }
+    }
+
+    /**
+     * @hide
+     */
+    @UnsupportedAppUsage
+    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);
+            }
+        }
+    }
+
+    private void dispatchOnCurrentInputMethodSubtypeChanged(InputMethodSubtype newSubtype) {
+        synchronized (mLock) {
+            mNotifyUserActionSent = false;
+        }
+        onCurrentInputMethodSubtypeChanged(newSubtype);
+    }
+
+    // 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);
+        }
+    }
+
+    /**
+     * Aimed to return the previous input method's {@link Insets#contentTopInsets}, but its actual
+     * semantics has never been well defined.
+     *
+     * <p>Note that the previous document clearly mentioned that this method could return {@code 0}
+     * at any time for whatever reason.  Now this method is just always returning {@code 0}.</p>
+     *
+     * @return on Android {@link android.os.Build.VERSION_CODES#Q} and later devices this method
+     *         always returns {@code 0}
+     * @deprecated the actual behavior of this method has never been well defined.  You cannot use
+     *             this method in a reliable and predictable way
+     */
+    @Deprecated
+    public int getInputMethodWindowRecommendedHeight() {
+        Log.w(TAG, "getInputMethodWindowRecommendedHeight() is deprecated and now always returns 0."
+                + " Do not use this method.");
+        return 0;
+    }
+
+    /**
+     * 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, int, Bundle)} will be called.
+     * @hide
+     */
+    @Override
+    public final void exposeContent(@NonNull InputContentInfo inputContentInfo,
+            @NonNull InputConnection inputConnection) {
+        if (inputConnection == null) {
+            return;
+        }
+        if (getCurrentInputConnection() != inputConnection) {
+            return;
+        }
+        exposeContentInternal(inputContentInfo, getCurrentInputEditorInfo());
+    }
+
+    /**
+     * {@inheritDoc}
+     * @hide
+     */
+    @AnyThread
+    @Override
+    public final void notifyUserActionIfNecessary() {
+        synchronized (mLock) {
+            if (mNotifyUserActionSent) {
+                return;
+            }
+            mPrivOps.notifyUserAction();
+            mNotifyUserActionSent = true;
+        }
+    }
+
+    /**
+     * Allow the receiver of {@link InputContentInfo} to obtain a temporary read-only access
+     * permission to the content.
+     *
+     * <p>See {@link android.inputmethodservice.InputMethodService#exposeContent(InputContentInfo,
+     * InputConnection)} for details.</p>
+     *
+     * @param inputContentInfo Content to be temporarily exposed from the input method to the
+     * application.
+     * This cannot be {@code null}.
+     * @param editorInfo The editor that receives {@link InputContentInfo}.
+     */
+    private void exposeContentInternal(@NonNull InputContentInfo inputContentInfo,
+            @NonNull EditorInfo editorInfo) {
+        final Uri contentUri = inputContentInfo.getContentUri();
+        final IInputContentUriToken uriToken =
+                mPrivOps.createInputContentUriToken(contentUri, editorInfo.packageName);
+        if (uriToken == null) {
+            Log.e(TAG, "createInputContentAccessToken failed. contentUri=" + contentUri.toString()
+                    + " packageName=" + editorInfo.packageName);
+            return;
+        }
+        inputContentInfo.setUriToken(uriToken);
+    }
+
+    private int mapToImeWindowStatus() {
+        return IME_ACTIVE
+                | (isInputViewShown()
+                        ? (mCanPreRender ? (mWindowVisible ? IME_VISIBLE : IME_INVISIBLE)
+                        : IME_VISIBLE) : 0);
+    }
+
+    private boolean isAutomotive() {
+        return getApplicationContext().getPackageManager().hasSystemFeature(
+                PackageManager.FEATURE_AUTOMOTIVE);
+    }
+
+    /**
+     * 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("  mViewsCreated=" + mViewsCreated);
+        p.println("  mDecorViewVisible=" + mDecorViewVisible
+                + " mDecorViewWasVisible=" + mDecorViewWasVisible
+                + " mWindowVisible=" + mWindowVisible
+                + " 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);
+
+        if (mInputEditorInfo != null) {
+            p.println("  mInputEditorInfo:");
+            mInputEditorInfo.dump(p, "    ");
+        } else {
+            p.println("  mInputEditorInfo: null");
+        }
+        
+        p.println("  mShowInputRequested=" + mShowInputRequested
+                + " mLastShowInputRequested=" + mLastShowInputRequested
+                + " mCanPreRender=" + mCanPreRender
+                + " mIsPreRendered=" + mIsPreRendered
+                + " 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(" mSettingsObserver=" + mSettingsObserver);
+    }
+}
diff --git a/android/inputmethodservice/Keyboard.java b/android/inputmethodservice/Keyboard.java
new file mode 100644
index 0000000..c85fcd9
--- /dev/null
+++ b/android/inputmethodservice/Keyboard.java
@@ -0,0 +1,918 @@
+/*
+ * 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.annotation.XmlRes;
+import android.compat.annotation.UnsupportedAppUsage;
+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.os.Build;
+import android.text.TextUtils;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.TypedValue;
+import android.util.Xml;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+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>
+ * &lt;Keyboard
+ *         android:keyWidth="%10p"
+ *         android:keyHeight="50px"
+ *         android:horizontalGap="2px"
+ *         android:verticalGap="2px" &gt;
+ *     &lt;Row android:keyWidth="32px" &gt;
+ *         &lt;Key android:keyLabel="A" /&gt;
+ *         ...
+ *     &lt;/Row&gt;
+ *     ...
+ * &lt;/Keyboard&gt;
+ * </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
+ * @deprecated This class is deprecated because this is just a convenient UI widget class that
+ *             application developers can re-implement on top of existing public APIs.  If you have
+ *             already depended on this class, consider copying the implementation from AOSP into
+ *             your project or re-implementing a similar widget by yourselves
+ */
+@Deprecated
+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 */
+    @UnsupportedAppUsage
+    private int mTotalHeight;
+    
+    /** 
+     * Total width of the keyboard, including left side gaps and keys, but not any gaps on the
+     * right side.
+     */
+    @UnsupportedAppUsage
+    private int mTotalWidth;
+    
+    /** List of keys in this keyboard */
+    private List<Key> mKeys;
+    
+    /** List of modifier keys such as Shift & Alt, if any */
+    @UnsupportedAppUsage
+    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);
+    }
+
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    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 0000000..b780b21
--- /dev/null
+++ b/android/inputmethodservice/KeyboardView.java
@@ -0,0 +1,1572 @@
+/*
+ * 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.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Paint.Align;
+import android.graphics.PorterDuff;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+import android.inputmethodservice.Keyboard.Key;
+import android.media.AudioManager;
+import android.os.Handler;
+import android.os.Message;
+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_keyPreviewHeight
+ * @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
+ *
+ * @deprecated This class is deprecated because this is just a convenient UI widget class that
+ *             application developers can re-implement on top of existing public APIs.  If you have
+ *             already depended on this class, consider copying the implementation from AOSP into
+ *             your project or re-implementing a similar widget by yourselves
+ */
+@Deprecated
+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;
+    @UnsupportedAppUsage
+    private int mLabelTextSize;
+    private int mKeyTextSize;
+    private int mKeyTextColor;
+    private float mShadowRadius;
+    private int mShadowColor;
+    private float mBackgroundDimAmount;
+
+    @UnsupportedAppUsage
+    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;
+
+    @UnsupportedAppUsage
+    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;
+        }
+
+        if (mKeyboard == null) return;
+
+        mCanvas.save();
+        final Canvas canvas = mCanvas;
+        canvas.clipRect(mDirtyRect);
+
+        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);
+        }
+        mCanvas.restore();
+        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);
+                }
+            }
+        }
+    }
+
+    @UnsupportedAppUsage
+    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);
+    }
+
+    @UnsupportedAppUsage
+    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;
+    }
+
+    @UnsupportedAppUsage
+    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/MultiClientInputMethodClientCallbackAdaptor.java b/android/inputmethodservice/MultiClientInputMethodClientCallbackAdaptor.java
new file mode 100644
index 0000000..dbb669b
--- /dev/null
+++ b/android/inputmethodservice/MultiClientInputMethodClientCallbackAdaptor.java
@@ -0,0 +1,482 @@
+/*
+ * Copyright (C) 2018 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.Nullable;
+import android.annotation.WorkerThread;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.Debug;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.ResultReceiver;
+import android.util.Log;
+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.WindowManager.LayoutParams.SoftInputModeFlags;
+import android.view.inputmethod.CompletionInfo;
+import android.view.inputmethod.CursorAnchorInfo;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.ExtractedText;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.inputmethod.IMultiClientInputMethodSession;
+import com.android.internal.inputmethod.CancellationGroup;
+import com.android.internal.os.SomeArgs;
+import com.android.internal.util.function.pooled.PooledLambda;
+import com.android.internal.view.IInputContext;
+import com.android.internal.view.IInputMethodSession;
+import com.android.internal.view.InputConnectionWrapper;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * Re-dispatches all the incoming per-client events to the specified {@link Looper} thread.
+ *
+ * <p>There are three types of per-client callbacks.</p>
+ *
+ * <ul>
+ *     <li>{@link IInputMethodSession} - from the IME client</li>
+ *     <li>{@link IMultiClientInputMethodSession} - from MultiClientInputMethodManagerService</li>
+ *     <li>{@link InputChannel} - from the IME client</li>
+ * </ul>
+ *
+ * <p>This class serializes all the incoming events among those channels onto
+ * {@link MultiClientInputMethodServiceDelegate.ClientCallback} on the specified {@link Looper}
+ * thread.</p>
+ */
+final class MultiClientInputMethodClientCallbackAdaptor {
+    static final boolean DEBUG = false;
+    static final String TAG = MultiClientInputMethodClientCallbackAdaptor.class.getSimpleName();
+
+    private final Object mSessionLock = new Object();
+    @GuardedBy("mSessionLock")
+    CallbackImpl mCallbackImpl;
+    @GuardedBy("mSessionLock")
+    InputChannel mReadChannel;
+    @GuardedBy("mSessionLock")
+    KeyEvent.DispatcherState mDispatcherState;
+    @GuardedBy("mSessionLock")
+    Handler mHandler;
+    @GuardedBy("mSessionLock")
+    @Nullable
+    InputEventReceiver mInputEventReceiver;
+
+    private final CancellationGroup mCancellationGroup = new CancellationGroup();
+
+    IInputMethodSession.Stub createIInputMethodSession() {
+        synchronized (mSessionLock) {
+            return new InputMethodSessionImpl(
+                    mSessionLock, mCallbackImpl, mHandler, mCancellationGroup);
+        }
+    }
+
+    IMultiClientInputMethodSession.Stub createIMultiClientInputMethodSession() {
+        synchronized (mSessionLock) {
+            return new MultiClientInputMethodSessionImpl(
+                    mSessionLock, mCallbackImpl, mHandler, mCancellationGroup);
+        }
+    }
+
+    MultiClientInputMethodClientCallbackAdaptor(
+            MultiClientInputMethodServiceDelegate.ClientCallback clientCallback, Looper looper,
+            KeyEvent.DispatcherState dispatcherState, InputChannel readChannel) {
+        synchronized (mSessionLock) {
+            mCallbackImpl = new CallbackImpl(this, clientCallback);
+            mDispatcherState = dispatcherState;
+            mHandler = new Handler(looper, null, true);
+            mReadChannel = readChannel;
+            mInputEventReceiver = new ImeInputEventReceiver(mReadChannel, mHandler.getLooper(),
+                    mCancellationGroup, mDispatcherState, mCallbackImpl.mOriginalCallback);
+        }
+    }
+
+    private static final class KeyEventCallbackAdaptor implements KeyEvent.Callback {
+        private final MultiClientInputMethodServiceDelegate.ClientCallback mLocalCallback;
+
+        KeyEventCallbackAdaptor(
+                MultiClientInputMethodServiceDelegate.ClientCallback callback) {
+            mLocalCallback = callback;
+        }
+
+        @Override
+        public boolean onKeyDown(int keyCode, KeyEvent event) {
+            return mLocalCallback.onKeyDown(keyCode, event);
+        }
+
+        @Override
+        public boolean onKeyLongPress(int keyCode, KeyEvent event) {
+            return mLocalCallback.onKeyLongPress(keyCode, event);
+        }
+
+        @Override
+        public boolean onKeyUp(int keyCode, KeyEvent event) {
+            return mLocalCallback.onKeyUp(keyCode, event);
+        }
+
+        @Override
+        public boolean onKeyMultiple(int keyCode, int count, KeyEvent event) {
+            return mLocalCallback.onKeyMultiple(keyCode, event);
+        }
+    }
+
+    private static final class ImeInputEventReceiver extends InputEventReceiver {
+        private final CancellationGroup mCancellationGroupOnFinishSession;
+        private final KeyEvent.DispatcherState mDispatcherState;
+        private final MultiClientInputMethodServiceDelegate.ClientCallback mClientCallback;
+        private final KeyEventCallbackAdaptor mKeyEventCallbackAdaptor;
+
+        ImeInputEventReceiver(InputChannel readChannel, Looper looper,
+                CancellationGroup cancellationGroupOnFinishSession,
+                KeyEvent.DispatcherState dispatcherState,
+                MultiClientInputMethodServiceDelegate.ClientCallback callback) {
+            super(readChannel, looper);
+            mCancellationGroupOnFinishSession = cancellationGroupOnFinishSession;
+            mDispatcherState = dispatcherState;
+            mClientCallback = callback;
+            mKeyEventCallbackAdaptor = new KeyEventCallbackAdaptor(callback);
+        }
+
+        @Override
+        public void onInputEvent(InputEvent event) {
+            if (mCancellationGroupOnFinishSession.isCanceled()) {
+                // The session has been finished.
+                finishInputEvent(event, false);
+                return;
+            }
+            boolean handled = false;
+            try {
+                if (event instanceof KeyEvent) {
+                    final KeyEvent keyEvent = (KeyEvent) event;
+                    handled = keyEvent.dispatch(mKeyEventCallbackAdaptor, mDispatcherState,
+                            mKeyEventCallbackAdaptor);
+                } else {
+                    final MotionEvent motionEvent = (MotionEvent) event;
+                    if (motionEvent.isFromSource(InputDevice.SOURCE_CLASS_TRACKBALL)) {
+                        handled = mClientCallback.onTrackballEvent(motionEvent);
+                    } else {
+                        handled = mClientCallback.onGenericMotionEvent(motionEvent);
+                    }
+                }
+            } finally {
+                finishInputEvent(event, handled);
+            }
+        }
+    }
+
+    private static final class InputMethodSessionImpl extends IInputMethodSession.Stub {
+        private final Object mSessionLock;
+        @GuardedBy("mSessionLock")
+        private CallbackImpl mCallbackImpl;
+        @GuardedBy("mSessionLock")
+        private Handler mHandler;
+        private final CancellationGroup mCancellationGroupOnFinishSession;
+
+        InputMethodSessionImpl(Object lock, CallbackImpl callback, Handler handler,
+                CancellationGroup cancellationGroupOnFinishSession) {
+            mSessionLock = lock;
+            mCallbackImpl = callback;
+            mHandler = handler;
+            mCancellationGroupOnFinishSession = cancellationGroupOnFinishSession;
+        }
+
+        @Override
+        public void updateExtractedText(int token, ExtractedText text) {
+            reportNotSupported();
+        }
+
+        @Override
+        public void updateSelection(int oldSelStart, int oldSelEnd,
+                int newSelStart, int newSelEnd,
+                int candidatesStart, int candidatesEnd) {
+            synchronized (mSessionLock) {
+                if (mCallbackImpl == null || mHandler == null) {
+                    return;
+                }
+                final SomeArgs args = SomeArgs.obtain();
+                args.argi1 = oldSelStart;
+                args.argi2 = oldSelEnd;
+                args.argi3 = newSelStart;
+                args.argi4 = newSelEnd;
+                args.argi5 = candidatesStart;
+                args.argi6 = candidatesEnd;
+                mHandler.sendMessage(PooledLambda.obtainMessage(
+                        CallbackImpl::updateSelection, mCallbackImpl, args));
+            }
+        }
+
+        @Override
+        public void viewClicked(boolean focusChanged) {
+            reportNotSupported();
+        }
+
+        @Override
+        public void updateCursor(Rect newCursor) {
+            reportNotSupported();
+        }
+
+        @Override
+        public void displayCompletions(CompletionInfo[] completions) {
+            synchronized (mSessionLock) {
+                if (mCallbackImpl == null || mHandler == null) {
+                    return;
+                }
+                mHandler.sendMessage(PooledLambda.obtainMessage(
+                        CallbackImpl::displayCompletions, mCallbackImpl, completions));
+            }
+        }
+
+        @Override
+        public void appPrivateCommand(String action, Bundle data) {
+            synchronized (mSessionLock) {
+                if (mCallbackImpl == null || mHandler == null) {
+                    return;
+                }
+                mHandler.sendMessage(PooledLambda.obtainMessage(
+                        CallbackImpl::appPrivateCommand, mCallbackImpl, action, data));
+            }
+        }
+
+        @Override
+        public void toggleSoftInput(int showFlags, int hideFlags) {
+            synchronized (mSessionLock) {
+                if (mCallbackImpl == null || mHandler == null) {
+                    return;
+                }
+                mHandler.sendMessage(PooledLambda.obtainMessage(
+                        CallbackImpl::toggleSoftInput, mCallbackImpl, showFlags,
+                        hideFlags));
+            }
+        }
+
+        @Override
+        public void finishSession() {
+            synchronized (mSessionLock) {
+                if (mCallbackImpl == null || mHandler == null) {
+                    return;
+                }
+                mCancellationGroupOnFinishSession.cancelAll();
+                mHandler.sendMessage(PooledLambda.obtainMessage(
+                        CallbackImpl::finishSession, mCallbackImpl));
+                mCallbackImpl = null;
+                mHandler = null;
+            }
+        }
+
+        @Override
+        public void updateCursorAnchorInfo(CursorAnchorInfo info) {
+            synchronized (mSessionLock) {
+                if (mCallbackImpl == null || mHandler == null) {
+                    return;
+                }
+                mHandler.sendMessage(PooledLambda.obtainMessage(
+                        CallbackImpl::updateCursorAnchorInfo, mCallbackImpl, info));
+            }
+        }
+
+        @Override
+        public final void notifyImeHidden() {
+            // no-op for multi-session since IME is responsible controlling navigation bar buttons.
+            reportNotSupported();
+        }
+
+        @Override
+        public void removeImeSurface() {
+            // no-op for multi-session
+            reportNotSupported();
+        }
+    }
+
+    private static final class MultiClientInputMethodSessionImpl
+            extends IMultiClientInputMethodSession.Stub {
+        private final Object mSessionLock;
+        @GuardedBy("mSessionLock")
+        private CallbackImpl mCallbackImpl;
+        @GuardedBy("mSessionLock")
+        private Handler mHandler;
+        private final CancellationGroup mCancellationGroupOnFinishSession;
+
+        MultiClientInputMethodSessionImpl(Object lock, CallbackImpl callback,
+                Handler handler, CancellationGroup cancellationGroupOnFinishSession) {
+            mSessionLock = lock;
+            mCallbackImpl = callback;
+            mHandler = handler;
+            mCancellationGroupOnFinishSession = cancellationGroupOnFinishSession;
+        }
+
+        @Override
+        public void startInputOrWindowGainedFocus(@Nullable IInputContext inputContext,
+                int missingMethods, @Nullable EditorInfo editorInfo, int controlFlags,
+                @SoftInputModeFlags int softInputMode, int windowHandle) {
+            synchronized (mSessionLock) {
+                if (mCallbackImpl == null || mHandler == null) {
+                    return;
+                }
+                final SomeArgs args = SomeArgs.obtain();
+                // TODO(Bug 119211536): Remove dependency on AbstractInputMethodService from ICW
+                final WeakReference<AbstractInputMethodService> fakeIMS =
+                        new WeakReference<>(null);
+                args.arg1 = (inputContext == null) ? null
+                        : new InputConnectionWrapper(fakeIMS, inputContext, missingMethods,
+                                mCancellationGroupOnFinishSession);
+                args.arg2 = editorInfo;
+                args.argi1 = controlFlags;
+                args.argi2 = softInputMode;
+                args.argi3 = windowHandle;
+                mHandler.sendMessage(PooledLambda.obtainMessage(
+                        CallbackImpl::startInputOrWindowGainedFocus, mCallbackImpl, args));
+            }
+        }
+
+        @Override
+        public void showSoftInput(int flags, ResultReceiver resultReceiver) {
+            synchronized (mSessionLock) {
+                if (mCallbackImpl == null || mHandler == null) {
+                    return;
+                }
+                mHandler.sendMessage(PooledLambda.obtainMessage(
+                        CallbackImpl::showSoftInput, mCallbackImpl, flags,
+                        resultReceiver));
+            }
+        }
+
+        @Override
+        public void hideSoftInput(int flags, ResultReceiver resultReceiver) {
+            synchronized (mSessionLock) {
+                if (mCallbackImpl == null || mHandler == null) {
+                    return;
+                }
+                mHandler.sendMessage(PooledLambda.obtainMessage(
+                        CallbackImpl::hideSoftInput, mCallbackImpl, flags,
+                        resultReceiver));
+            }
+        }
+    }
+
+    /**
+     * The maim part of adaptor to {@link MultiClientInputMethodServiceDelegate.ClientCallback}.
+     */
+    @WorkerThread
+    private static final class CallbackImpl {
+        private final MultiClientInputMethodClientCallbackAdaptor mCallbackAdaptor;
+        private final MultiClientInputMethodServiceDelegate.ClientCallback mOriginalCallback;
+        private boolean mFinished = false;
+
+        CallbackImpl(MultiClientInputMethodClientCallbackAdaptor callbackAdaptor,
+                MultiClientInputMethodServiceDelegate.ClientCallback callback) {
+            mCallbackAdaptor = callbackAdaptor;
+            mOriginalCallback = callback;
+        }
+
+        void updateSelection(SomeArgs args) {
+            try {
+                if (mFinished) {
+                    return;
+                }
+                mOriginalCallback.onUpdateSelection(args.argi1, args.argi2, args.argi3,
+                        args.argi4, args.argi5, args.argi6);
+            } finally {
+                args.recycle();
+            }
+        }
+
+        void displayCompletions(CompletionInfo[] completions) {
+            if (mFinished) {
+                return;
+            }
+            mOriginalCallback.onDisplayCompletions(completions);
+        }
+
+        void appPrivateCommand(String action, Bundle data) {
+            if (mFinished) {
+                return;
+            }
+            mOriginalCallback.onAppPrivateCommand(action, data);
+        }
+
+        void toggleSoftInput(int showFlags, int hideFlags) {
+            if (mFinished) {
+                return;
+            }
+            mOriginalCallback.onToggleSoftInput(showFlags, hideFlags);
+        }
+
+        void finishSession() {
+            if (mFinished) {
+                return;
+            }
+            mFinished = true;
+            mOriginalCallback.onFinishSession();
+            synchronized (mCallbackAdaptor.mSessionLock) {
+                mCallbackAdaptor.mDispatcherState = null;
+                if (mCallbackAdaptor.mReadChannel != null) {
+                    mCallbackAdaptor.mReadChannel.dispose();
+                    mCallbackAdaptor.mReadChannel = null;
+                }
+                mCallbackAdaptor.mInputEventReceiver = null;
+            }
+        }
+
+        void updateCursorAnchorInfo(CursorAnchorInfo info) {
+            if (mFinished) {
+                return;
+            }
+            mOriginalCallback.onUpdateCursorAnchorInfo(info);
+        }
+
+        void startInputOrWindowGainedFocus(SomeArgs args) {
+            try {
+                if (mFinished) {
+                    return;
+                }
+                final InputConnectionWrapper inputConnection = (InputConnectionWrapper) args.arg1;
+                final EditorInfo editorInfo = (EditorInfo) args.arg2;
+                final int startInputFlags = args.argi1;
+                final int softInputMode = args.argi2;
+                final int windowHandle = args.argi3;
+                mOriginalCallback.onStartInputOrWindowGainedFocus(inputConnection, editorInfo,
+                        startInputFlags, softInputMode, windowHandle);
+            } finally {
+                args.recycle();
+            }
+        }
+
+        void showSoftInput(int flags, ResultReceiver resultReceiver) {
+            if (mFinished) {
+                return;
+            }
+            mOriginalCallback.onShowSoftInput(flags, resultReceiver);
+        }
+
+        void hideSoftInput(int flags, ResultReceiver resultReceiver) {
+            if (mFinished) {
+                return;
+            }
+            mOriginalCallback.onHideSoftInput(flags, resultReceiver);
+        }
+    }
+
+    private static void reportNotSupported() {
+        if (DEBUG) {
+            Log.d(TAG, Debug.getCaller() + " is not supported");
+        }
+    }
+}
diff --git a/android/inputmethodservice/MultiClientInputMethodServiceDelegate.java b/android/inputmethodservice/MultiClientInputMethodServiceDelegate.java
new file mode 100644
index 0000000..4b02085
--- /dev/null
+++ b/android/inputmethodservice/MultiClientInputMethodServiceDelegate.java
@@ -0,0 +1,388 @@
+/*
+ * Copyright (C) 2018 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.Nullable;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.ResultReceiver;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.WindowManager.LayoutParams.SoftInputModeFlags;
+import android.view.inputmethod.CompletionInfo;
+import android.view.inputmethod.CursorAnchorInfo;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+
+import com.android.internal.inputmethod.StartInputFlags;
+
+/**
+ * Defines all the public APIs and interfaces that are necessary to implement multi-client IMEs.
+ *
+ * <p>Actual implementation is further delegated to
+ * {@link MultiClientInputMethodServiceDelegateImpl}.</p>
+ *
+ * @hide
+ */
+public final class MultiClientInputMethodServiceDelegate {
+    // @SdkConstant(SdkConstantType.SERVICE_ACTION)
+    public static final String SERVICE_INTERFACE =
+            "android.inputmethodservice.MultiClientInputMethodService";
+
+    /**
+     * Special value that is guaranteed to be not used for IME client ID.
+     */
+    public static final int INVALID_CLIENT_ID = -1;
+
+    /**
+     * Special value that is guaranteed to be not used for window handle.
+     */
+    public static final int INVALID_WINDOW_HANDLE = -1;
+
+    private final MultiClientInputMethodServiceDelegateImpl mImpl;
+
+    /**
+     * Top-level callbacks for this {@link MultiClientInputMethodServiceDelegate}.
+     */
+    public interface ServiceCallback {
+        /**
+         * Called when this {@link MultiClientInputMethodServiceDelegate} is recognized by the
+         * system and privileged operations like {@link #createInputMethodWindowToken(int)} are
+         * ready to be called.
+         */
+        void initialized();
+
+        /**
+         * Called when a new IME client is recognized by the system.
+         *
+         * <p>Once the IME receives this callback, the IME can start interacting with the IME client
+         * by calling {@link #acceptClient(int, ClientCallback, KeyEvent.DispatcherState, Looper)}.
+         * </p>
+         *
+         * @param clientId ID of the client.
+         * @param uid UID of the IME client.
+         * @param pid PID of the IME client.
+         * @param selfReportedDisplayId display ID reported from the IME client. Since the system
+         *        does not validate this display ID, and at any time the IME client can lose the
+         *        access to this display ID, the IME needs to call
+         *        {@link #isUidAllowedOnDisplay(int, int)} to check whether the IME client still
+         *        has access to this display or not.
+         */
+        void addClient(int clientId, int uid, int pid, int selfReportedDisplayId);
+
+        /**
+         * Called when an IME client is being destroyed.
+         *
+         * @param clientId ID of the client.
+         */
+        void removeClient(int clientId);
+    }
+
+    /**
+     * Per-client callbacks.
+     */
+    public interface ClientCallback {
+        /**
+         * Called when the associated IME client called {@link
+         * android.view.inputmethod.InputMethodManager#sendAppPrivateCommand(View, String, Bundle)}.
+         *
+         * @param action Name of the command to be performed.
+         * @param data Any data to include with the command.
+         * @see android.inputmethodservice.InputMethodService#onAppPrivateCommand(String, Bundle)
+         */
+        void onAppPrivateCommand(String action, Bundle data);
+
+        /**
+         * Called when the associated IME client called {@link
+         * android.view.inputmethod.InputMethodManager#displayCompletions(View, CompletionInfo[])}.
+         *
+         * @param completions Completion information provided from the IME client.
+         * @see android.inputmethodservice.InputMethodService#onDisplayCompletions(CompletionInfo[])
+         */
+        void onDisplayCompletions(CompletionInfo[] completions);
+
+        /**
+         * Called when this callback session is closed. No further callback should not happen on
+         * this callback object.
+         */
+        void onFinishSession();
+
+        /**
+         * Called when the associated IME client called {@link
+         * android.view.inputmethod.InputMethodManager#hideSoftInputFromWindow(IBinder, int)} or
+         * {@link android.view.inputmethod.InputMethodManager#hideSoftInputFromWindow(IBinder, int,
+         * ResultReceiver)}.
+         *
+         * @param flags The flag passed by the client.
+         * @param resultReceiver The {@link ResultReceiver} passed by the client.
+         * @see android.inputmethodservice.InputMethodService#onWindowHidden()
+         */
+        void onHideSoftInput(int flags, ResultReceiver resultReceiver);
+
+        /**
+         * Called when the associated IME client called {@link
+         * android.view.inputmethod.InputMethodManager#showSoftInput(View, int)} or {@link
+         * android.view.inputmethod.InputMethodManager#showSoftInput(View, int, ResultReceiver)}.
+         *
+         * @param flags The flag passed by the client.
+         * @param resultReceiver The {@link ResultReceiver} passed by the client.
+         * @see android.inputmethodservice.InputMethodService#onWindowShown()
+         */
+        void onShowSoftInput(int flags, ResultReceiver resultReceiver);
+
+        /**
+         * A generic callback when {@link InputConnection} is being established.
+         *
+         * @param inputConnection The {@link InputConnection} to be established.
+         * @param editorInfo The {@link EditorInfo} reported from the IME client.
+         * @param startInputFlags Any combinations of {@link StartInputFlags}.
+         * @param softInputMode SoftWindowMode specified to this window.
+         * @param targetWindowHandle A unique Window token.
+         * @see android.inputmethodservice.InputMethodService#onStartInput(EditorInfo, boolean)
+         */
+        void onStartInputOrWindowGainedFocus(
+                @Nullable InputConnection inputConnection,
+                @Nullable EditorInfo editorInfo,
+                @StartInputFlags int startInputFlags,
+                @SoftInputModeFlags int softInputMode,
+                int targetWindowHandle);
+
+        /**
+         * Called when the associated IME client called {@link
+         * android.view.inputmethod.InputMethodManager#toggleSoftInput(int, int)}.
+         *
+         * @param showFlags The flag passed by the client.
+         * @param hideFlags The flag passed by the client.
+         * @see android.inputmethodservice.InputMethodService#onToggleSoftInput(int, int)
+         */
+        void onToggleSoftInput(int showFlags, int hideFlags);
+
+        /**
+         * Called when the associated IME client called {@link
+         * android.view.inputmethod.InputMethodManager#updateCursorAnchorInfo(View,
+         * CursorAnchorInfo)}.
+         *
+         * @param info The {@link CursorAnchorInfo} passed by the client.
+         * @see android.inputmethodservice.InputMethodService#onUpdateCursorAnchorInfo(
+         *      CursorAnchorInfo)
+         */
+        void onUpdateCursorAnchorInfo(CursorAnchorInfo info);
+
+        /**
+         * Called when the associated IME client called {@link
+         * android.view.inputmethod.InputMethodManager#updateSelection(View, int, int, int, int)}.
+         *
+         * @param oldSelStart The previous selection start index.
+         * @param oldSelEnd The previous selection end index.
+         * @param newSelStart The new selection start index.
+         * @param newSelEnd The new selection end index.
+         * @param candidatesStart The new candidate start index.
+         * @param candidatesEnd The new candidate end index.
+         * @see android.inputmethodservice.InputMethodService#onUpdateSelection(int, int, int, int,
+         *      int, int)
+         */
+        void onUpdateSelection(int oldSelStart, int oldSelEnd, int newSelStart, int newSelEnd,
+                int candidatesStart, int candidatesEnd);
+
+        /**
+         * Called to give a chance for the IME to intercept generic motion events before they are
+         * processed by the application.
+         *
+         * @param event {@link MotionEvent} that is about to be handled by the IME client.
+         * @return {@code true} to tell the IME client that the IME handled this event.
+         * @see android.inputmethodservice.InputMethodService#onGenericMotionEvent(MotionEvent)
+         */
+        boolean onGenericMotionEvent(MotionEvent event);
+
+        /**
+         * Called to give a chance for the IME to intercept key down events before they are
+         * processed by the application.
+         *
+         * @param keyCode The value in {@link KeyEvent#getKeyCode()}.
+         * @param event {@link KeyEvent} for this key down event.
+         * @return {@code true} to tell the IME client that the IME handled this event.
+         * @see android.inputmethodservice.InputMethodService#onKeyDown(int, KeyEvent)
+         */
+        boolean onKeyDown(int keyCode, KeyEvent event);
+
+        /**
+         * Called to give a chance for the IME to intercept key long press events before they are
+         * processed by the application.
+         *
+         * @param keyCode The value in {@link KeyEvent#getKeyCode()}.
+         * @param event {@link KeyEvent} for this key long press event.
+         * @return {@code true} to tell the IME client that the IME handled this event.
+         * @see android.inputmethodservice.InputMethodService#onKeyLongPress(int, KeyEvent)
+         */
+        boolean onKeyLongPress(int keyCode, KeyEvent event);
+
+        /**
+         * Called to give a chance for the IME to intercept key multiple events before they are
+         * processed by the application.
+         *
+         * @param keyCode The value in {@link KeyEvent#getKeyCode()}.
+         * @param event {@link KeyEvent} for this key multiple event.
+         * @return {@code true} to tell the IME client that the IME handled this event.
+         * @see android.inputmethodservice.InputMethodService#onKeyMultiple(int, int, KeyEvent)
+         */
+        boolean onKeyMultiple(int keyCode, KeyEvent event);
+
+        /**
+         * Called to give a chance for the IME to intercept key up events before they are processed
+         * by the application.
+         *
+         * @param keyCode The value in {@link KeyEvent#getKeyCode()}.
+         * @param event {@link KeyEvent} for this key up event.
+         * @return {@code true} to tell the IME client that the IME handled this event.
+         * @see android.inputmethodservice.InputMethodService#onKeyUp(int, KeyEvent)
+         */
+        boolean onKeyUp(int keyCode, KeyEvent event);
+
+        /**
+         * Called to give a chance for the IME to intercept generic motion events before they are
+         * processed by the application.
+         *
+         * @param event {@link MotionEvent} that is about to be handled by the IME client.
+         * @return {@code true} to tell the IME client that the IME handled this event.
+         * @see android.inputmethodservice.InputMethodService#onTrackballEvent(MotionEvent)
+         */
+        boolean onTrackballEvent(MotionEvent event);
+    }
+
+    private MultiClientInputMethodServiceDelegate(Context context,
+            ServiceCallback serviceCallback) {
+        mImpl = new MultiClientInputMethodServiceDelegateImpl(context, serviceCallback);
+    }
+
+    /**
+     * Must be called by the multi-client IME implementer to create
+     * {@link MultiClientInputMethodServiceDelegate}.
+     *
+     * @param context {@link Context} with which the delegate should interact with the system.
+     * @param serviceCallback {@link ServiceCallback} to receive service-level callbacks.
+     * @return A new instance of {@link MultiClientInputMethodServiceDelegate}.
+     */
+    public static MultiClientInputMethodServiceDelegate create(Context context,
+            ServiceCallback serviceCallback) {
+        return new MultiClientInputMethodServiceDelegate(context, serviceCallback);
+    }
+
+    /**
+     * Must be called by the multi-client IME service when {@link android.app.Service#onDestroy()}
+     * is called.
+     */
+    public void onDestroy() {
+        mImpl.onDestroy();
+    }
+
+    /**
+     * Must be called by the multi-client IME service when
+     * {@link android.app.Service#onBind(Intent)} is called.
+     *
+     * @param intent {@link Intent} passed to {@link android.app.Service#onBind(Intent)}.
+     * @return An {@link IBinder} object that needs to be returned from
+     *         {@link android.app.Service#onBind(Intent)}.
+     */
+    public IBinder onBind(Intent intent) {
+        return mImpl.onBind(intent);
+    }
+
+    /**
+     * Must be called by the multi-client IME service when
+     * {@link android.app.Service#onUnbind(Intent)} is called.
+     *
+     * @param intent {@link Intent} passed to {@link android.app.Service#onUnbind(Intent)}.
+     * @return A boolean value that needs to be returned from
+     *         {@link android.app.Service#onUnbind(Intent)}.
+     */
+    public boolean onUnbind(Intent intent) {
+        return mImpl.onUnbind(intent);
+    }
+
+    /**
+     * Must be called by the multi-client IME service to create a special window token for IME
+     * window.
+     *
+     * <p>This method is available only after {@link ServiceCallback#initialized()}.</p>
+     *
+     * @param displayId display ID on which the IME window will be shown.
+     * @return Window token to be specified to the IME window/
+     */
+    public IBinder createInputMethodWindowToken(int displayId) {
+        return mImpl.createInputMethodWindowToken(displayId);
+    }
+
+    /**
+     * Must be called by the multi-client IME service to notify the system when the IME is ready to
+     * accept callback events from the specified IME client.
+     *
+     * @param clientId The IME client ID specified in
+     *                 {@link ServiceCallback#addClient(int, int, int, int)}.
+     * @param clientCallback The {@link ClientCallback} to receive callback events from this IME
+     *                       client.
+     * @param dispatcherState {@link KeyEvent.DispatcherState} to be used when receiving key-related
+     *                        callbacks in {@link ClientCallback}.
+     * @param looper {@link Looper} on which {@link ClientCallback} will be called back.
+     */
+    public void acceptClient(int clientId, ClientCallback clientCallback,
+            KeyEvent.DispatcherState dispatcherState, Looper looper) {
+        mImpl.acceptClient(clientId, clientCallback, dispatcherState, looper);
+    }
+
+    /**
+     * Must be called by the multi-client IME service to notify the system when the IME is ready to
+     * interact with the window in the IME client.
+     *
+     * @param clientId The IME client ID specified in
+     *                 {@link ServiceCallback#addClient(int, int, int, int)}.
+     * @param targetWindowHandle The window handle specified in
+     *                           {@link ClientCallback#onStartInputOrWindowGainedFocus}.
+     * @param imeWindowToken The IME window token returned from
+     *                       {@link #createInputMethodWindowToken(int)}.
+     */
+    public void reportImeWindowTarget(int clientId, int targetWindowHandle,
+            IBinder imeWindowToken) {
+        mImpl.reportImeWindowTarget(clientId, targetWindowHandle, imeWindowToken);
+    }
+
+    /**
+     * Can be called by the multi-client IME service to check if the given {@code uid} is allowed
+     * to access to {@code displayId}.
+     *
+     * @param displayId Display ID to be queried.
+     * @param uid UID to be queried.
+     * @return {@code true} if {@code uid} is allowed to access to {@code displayId}.
+     */
+    public boolean isUidAllowedOnDisplay(int displayId, int uid) {
+        return mImpl.isUidAllowedOnDisplay(displayId, uid);
+    }
+
+    /**
+     * Can be called by MSIME to activate/deactivate a client when it is gaining/losing focus
+     * respectively.
+     *
+     * @param clientId client ID to activate/deactivate.
+     * @param active {@code true} to activate a client.
+     */
+    public void setActive(int clientId, boolean active) {
+        mImpl.setActive(clientId, active);
+    }
+}
diff --git a/android/inputmethodservice/MultiClientInputMethodServiceDelegateImpl.java b/android/inputmethodservice/MultiClientInputMethodServiceDelegateImpl.java
new file mode 100644
index 0000000..04db8d6
--- /dev/null
+++ b/android/inputmethodservice/MultiClientInputMethodServiceDelegateImpl.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2018 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 java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.annotation.IntDef;
+import android.content.Context;
+import android.content.Intent;
+import android.os.IBinder;
+import android.os.Looper;
+import android.util.Log;
+import android.view.InputChannel;
+import android.view.KeyEvent;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.inputmethod.IMultiClientInputMethod;
+import com.android.internal.inputmethod.IMultiClientInputMethodPrivilegedOperations;
+import com.android.internal.inputmethod.MultiClientInputMethodPrivilegedOperations;
+
+import java.lang.annotation.Retention;
+import java.lang.ref.WeakReference;
+
+final class MultiClientInputMethodServiceDelegateImpl {
+    private static final String TAG = "MultiClientInputMethodServiceDelegateImpl";
+
+    private final Object mLock = new Object();
+
+    @Retention(SOURCE)
+    @IntDef({InitializationPhase.INSTANTIATED,
+            InitializationPhase.ON_BIND_CALLED,
+            InitializationPhase.INITIALIZE_CALLED,
+            InitializationPhase.ON_UNBIND_CALLED,
+            InitializationPhase.ON_DESTROY_CALLED})
+    private @interface InitializationPhase {
+        int INSTANTIATED = 1;
+        int ON_BIND_CALLED = 2;
+        int INITIALIZE_CALLED = 3;
+        int ON_UNBIND_CALLED  = 4;
+        int ON_DESTROY_CALLED = 5;
+    }
+
+    @GuardedBy("mLock")
+    @InitializationPhase
+    private int mInitializationPhase;
+
+    private final MultiClientInputMethodPrivilegedOperations mPrivOps =
+            new MultiClientInputMethodPrivilegedOperations();
+
+    private final MultiClientInputMethodServiceDelegate.ServiceCallback mServiceCallback;
+
+    private final Context mContext;
+
+    MultiClientInputMethodServiceDelegateImpl(Context context,
+            MultiClientInputMethodServiceDelegate.ServiceCallback serviceCallback) {
+        mInitializationPhase = InitializationPhase.INSTANTIATED;
+        mContext = context;
+        mServiceCallback = serviceCallback;
+    }
+
+    void onDestroy() {
+        synchronized (mLock) {
+            switch (mInitializationPhase) {
+                case InitializationPhase.INSTANTIATED:
+                case InitializationPhase.ON_UNBIND_CALLED:
+                    mInitializationPhase = InitializationPhase.ON_DESTROY_CALLED;
+                    break;
+                default:
+                    Log.e(TAG, "unexpected state=" + mInitializationPhase);
+                    break;
+            }
+        }
+    }
+
+    private static final class ServiceImpl extends IMultiClientInputMethod.Stub {
+        private final WeakReference<MultiClientInputMethodServiceDelegateImpl> mImpl;
+
+        ServiceImpl(MultiClientInputMethodServiceDelegateImpl service) {
+            mImpl = new WeakReference<>(service);
+        }
+
+        @Override
+        public void initialize(IMultiClientInputMethodPrivilegedOperations privOps) {
+            final MultiClientInputMethodServiceDelegateImpl service = mImpl.get();
+            if (service == null) {
+                return;
+            }
+            synchronized (service.mLock) {
+                switch (service.mInitializationPhase) {
+                    case InitializationPhase.ON_BIND_CALLED:
+                        service.mPrivOps.set(privOps);
+                        service.mInitializationPhase = InitializationPhase.INITIALIZE_CALLED;
+                        service.mServiceCallback.initialized();
+                        break;
+                    default:
+                        Log.e(TAG, "unexpected state=" + service.mInitializationPhase);
+                        break;
+                }
+            }
+        }
+
+        @Override
+        public void addClient(int clientId, int uid, int pid, int selfReportedDisplayId) {
+            final MultiClientInputMethodServiceDelegateImpl service = mImpl.get();
+            if (service == null) {
+                return;
+            }
+            service.mServiceCallback.addClient(clientId, uid, pid, selfReportedDisplayId);
+        }
+
+        @Override
+        public void removeClient(int clientId) {
+            final MultiClientInputMethodServiceDelegateImpl service = mImpl.get();
+            if (service == null) {
+                return;
+            }
+            service.mServiceCallback.removeClient(clientId);
+        }
+    }
+
+    IBinder onBind(Intent intent) {
+        synchronized (mLock) {
+            switch (mInitializationPhase) {
+                case InitializationPhase.INSTANTIATED:
+                    mInitializationPhase = InitializationPhase.ON_BIND_CALLED;
+                    return new ServiceImpl(this);
+                default:
+                    Log.e(TAG, "unexpected state=" + mInitializationPhase);
+                    break;
+            }
+        }
+        return null;
+    }
+
+    boolean onUnbind(Intent intent) {
+        synchronized (mLock) {
+            switch (mInitializationPhase) {
+                case InitializationPhase.ON_BIND_CALLED:
+                case InitializationPhase.INITIALIZE_CALLED:
+                    mInitializationPhase = InitializationPhase.ON_UNBIND_CALLED;
+                    mPrivOps.dispose();
+                    break;
+                default:
+                    Log.e(TAG, "unexpected state=" + mInitializationPhase);
+                    break;
+            }
+        }
+        return false;
+    }
+
+    IBinder createInputMethodWindowToken(int displayId) {
+        return mPrivOps.createInputMethodWindowToken(displayId);
+    }
+
+    void acceptClient(int clientId,
+            MultiClientInputMethodServiceDelegate.ClientCallback clientCallback,
+            KeyEvent.DispatcherState dispatcherState, Looper looper) {
+        final InputChannel[] channels = InputChannel.openInputChannelPair("MSIMS-session");
+        final InputChannel writeChannel = channels[0];
+        final InputChannel readChannel = channels[1];
+        try {
+            final MultiClientInputMethodClientCallbackAdaptor callbackAdaptor =
+                    new MultiClientInputMethodClientCallbackAdaptor(clientCallback, looper,
+                            dispatcherState, readChannel);
+            mPrivOps.acceptClient(clientId, callbackAdaptor.createIInputMethodSession(),
+                    callbackAdaptor.createIMultiClientInputMethodSession(), writeChannel);
+        } finally {
+            writeChannel.dispose();
+        }
+    }
+
+    void reportImeWindowTarget(int clientId, int targetWindowHandle, IBinder imeWindowToken) {
+        mPrivOps.reportImeWindowTarget(clientId, targetWindowHandle, imeWindowToken);
+    }
+
+    boolean isUidAllowedOnDisplay(int displayId, int uid) {
+        return mPrivOps.isUidAllowedOnDisplay(displayId, uid);
+    }
+
+    void setActive(int clientId, boolean active) {
+        mPrivOps.setActive(clientId, active);
+    }
+}
diff --git a/android/inputmethodservice/SoftInputWindow.java b/android/inputmethodservice/SoftInputWindow.java
new file mode 100644
index 0000000..6efd03c
--- /dev/null
+++ b/android/inputmethodservice/SoftInputWindow.java
@@ -0,0 +1,365 @@
+/*
+ * 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 java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.annotation.IntDef;
+import android.app.Dialog;
+import android.content.Context;
+import android.graphics.Rect;
+import android.os.Debug;
+import android.os.IBinder;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.WindowManager;
+
+import java.lang.annotation.Retention;
+
+/**
+ * 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 {
+    private static final boolean DEBUG = false;
+    private static final String TAG = "SoftInputWindow";
+
+    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();
+
+    @Retention(SOURCE)
+    @IntDef(value = {SoftInputWindowState.TOKEN_PENDING, SoftInputWindowState.TOKEN_SET,
+            SoftInputWindowState.SHOWN_AT_LEAST_ONCE, SoftInputWindowState.REJECTED_AT_LEAST_ONCE})
+    private @interface SoftInputWindowState {
+        /**
+         * The window token is not set yet.
+         */
+        int TOKEN_PENDING = 0;
+        /**
+         * The window token was set, but the window is not shown yet.
+         */
+        int TOKEN_SET = 1;
+        /**
+         * The window was shown at least once.
+         */
+        int SHOWN_AT_LEAST_ONCE = 2;
+        /**
+         * {@link android.view.WindowManager.BadTokenException} was sent when calling
+         * {@link Dialog#show()} at least once.
+         */
+        int REJECTED_AT_LEAST_ONCE = 3;
+        /**
+         * The window is considered destroyed.  Any incoming request should be ignored.
+         */
+        int DESTROYED = 4;
+    }
+
+    @SoftInputWindowState
+    private int mWindowState = SoftInputWindowState.TOKEN_PENDING;
+
+    public interface Callback {
+        public void onBackPressed();
+    }
+
+    public void setToken(IBinder token) {
+        switch (mWindowState) {
+            case SoftInputWindowState.TOKEN_PENDING:
+                // Normal scenario.  Nothing to worry about.
+                WindowManager.LayoutParams lp = getWindow().getAttributes();
+                lp.token = token;
+                getWindow().setAttributes(lp);
+                updateWindowState(SoftInputWindowState.TOKEN_SET);
+
+                // As soon as we have a token, make sure the window is added (but not shown) by
+                // setting visibility to INVISIBLE and calling show() on Dialog. Note that
+                // WindowInsetsController.OnControllableInsetsChangedListener relies on the window
+                // being added to function.
+                getWindow().getDecorView().setVisibility(View.INVISIBLE);
+                show();
+                return;
+            case SoftInputWindowState.TOKEN_SET:
+            case SoftInputWindowState.SHOWN_AT_LEAST_ONCE:
+            case SoftInputWindowState.REJECTED_AT_LEAST_ONCE:
+                throw new IllegalStateException("setToken can be called only once");
+            case SoftInputWindowState.DESTROYED:
+                // Just ignore.  Since there are multiple event queues from the token is issued
+                // in the system server to the timing when it arrives here, it can be delivered
+                // after the is already destroyed.  No one should be blamed because of such an
+                // unfortunate but possible scenario.
+                Log.i(TAG, "Ignoring setToken() because window is already destroyed.");
+                return;
+            default:
+                throw new IllegalStateException("Unexpected state=" + mWindowState);
+        }
+    }
+
+    /**
+     * 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);
+    }
+
+    @Override
+    public final void show() {
+        switch (mWindowState) {
+            case SoftInputWindowState.TOKEN_PENDING:
+                throw new IllegalStateException("Window token is not set yet.");
+            case SoftInputWindowState.TOKEN_SET:
+            case SoftInputWindowState.SHOWN_AT_LEAST_ONCE:
+                // Normal scenario.  Nothing to worry about.
+                try {
+                    super.show();
+                    updateWindowState(SoftInputWindowState.SHOWN_AT_LEAST_ONCE);
+                } catch (WindowManager.BadTokenException e) {
+                    // Just ignore this exception.  Since show() can be requested from other
+                    // components such as the system and there could be multiple event queues before
+                    // the request finally arrives here, the system may have already invalidated the
+                    // window token attached to our window.  In such a scenario, receiving
+                    // BadTokenException here is an expected behavior.  We just ignore it and update
+                    // the state so that we do not touch this window later.
+                    Log.i(TAG, "Probably the IME window token is already invalidated."
+                            + " show() does nothing.");
+                    updateWindowState(SoftInputWindowState.REJECTED_AT_LEAST_ONCE);
+                }
+                return;
+            case SoftInputWindowState.REJECTED_AT_LEAST_ONCE:
+                // Just ignore.  In general we cannot completely avoid this kind of race condition.
+                Log.i(TAG, "Not trying to call show() because it was already rejected once.");
+                return;
+            case SoftInputWindowState.DESTROYED:
+                // Just ignore.  In general we cannot completely avoid this kind of race condition.
+                Log.i(TAG, "Ignoring show() because the window is already destroyed.");
+                return;
+            default:
+                throw new IllegalStateException("Unexpected state=" + mWindowState);
+        }
+    }
+
+    final void dismissForDestroyIfNecessary() {
+        switch (mWindowState) {
+            case SoftInputWindowState.TOKEN_PENDING:
+            case SoftInputWindowState.TOKEN_SET:
+                // nothing to do because the window has never been shown.
+                updateWindowState(SoftInputWindowState.DESTROYED);
+                return;
+            case SoftInputWindowState.SHOWN_AT_LEAST_ONCE:
+                // 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.
+                try {
+                    getWindow().setWindowAnimations(0);
+                    dismiss();
+                } catch (WindowManager.BadTokenException e) {
+                    // Just ignore this exception.  Since show() can be requested from other
+                    // components such as the system and there could be multiple event queues before
+                    // the request finally arrives here, the system may have already invalidated the
+                    // window token attached to our window.  In such a scenario, receiving
+                    // BadTokenException here is an expected behavior.  We just ignore it and update
+                    // the state so that we do not touch this window later.
+                    Log.i(TAG, "Probably the IME window token is already invalidated. "
+                            + "No need to dismiss it.");
+                }
+                // Either way, consider that the window is destroyed.
+                updateWindowState(SoftInputWindowState.DESTROYED);
+                return;
+            case SoftInputWindowState.REJECTED_AT_LEAST_ONCE:
+                // Just ignore.  In general we cannot completely avoid this kind of race condition.
+                Log.i(TAG,
+                        "Not trying to dismiss the window because it is most likely unnecessary.");
+                // Anyway, consider that the window is destroyed.
+                updateWindowState(SoftInputWindowState.DESTROYED);
+                return;
+            case SoftInputWindowState.DESTROYED:
+                throw new IllegalStateException(
+                        "dismissForDestroyIfNecessary can be called only once");
+            default:
+                throw new IllegalStateException("Unexpected state=" + mWindowState);
+        }
+    }
+
+    private void updateWindowState(@SoftInputWindowState int newState) {
+        if (DEBUG) {
+            if (mWindowState != newState) {
+                Log.d(TAG, "WindowState: " + stateToString(mWindowState) + " -> "
+                        + stateToString(newState) + " @ " + Debug.getCaller());
+            }
+        }
+        mWindowState = newState;
+    }
+
+    private static String stateToString(@SoftInputWindowState int state) {
+        switch (state) {
+            case SoftInputWindowState.TOKEN_PENDING:
+                return "TOKEN_PENDING";
+            case SoftInputWindowState.TOKEN_SET:
+                return "TOKEN_SET";
+            case SoftInputWindowState.SHOWN_AT_LEAST_ONCE:
+                return "SHOWN_AT_LEAST_ONCE";
+            case SoftInputWindowState.REJECTED_AT_LEAST_ONCE:
+                return "REJECTED_AT_LEAST_ONCE";
+            case SoftInputWindowState.DESTROYED:
+                return "DESTROYED";
+            default:
+                throw new IllegalStateException("Unknown state=" + state);
+        }
+    }
+}