From 9f9afe526d1f8ad17c628fc9e1e839725ffe913e Mon Sep 17 00:00:00 2001 From: Yohei Yukawa <yukawa@google.com> Date: Wed, 30 Mar 2016 12:03:51 -0700 Subject: [PATCH] Add IC#closeConnection(). It turns out that BaseInputConnection has still depended on a private API named BaseInputConnection#reportFinish(), which was introduced 4 years ago to work around a UI freeze due to an unbalanced batch edit count [1]. Note that such an unbalanced batch edit count cannot always be avoidable. It can easily occur in the following situations. - The current IME crashed during batch edit. - The user changed the View focus during batch edit. - The current IME called IMM#switchToNextInputMethod() during batch edit. The remaining problem is that #reportFinish() is still an internal API and only subclasses of BaseInputConnection can implement it, and IMM calls it when and only when the current InputConnection is BaseInputConnection or its subclass. InputConnectionWrapper and any other InputConnection implementations will never receive such a callback to clean up InputConnection#{begin, end}BatchEdit(), which is considered to be a major contributor to UI freeze. To address the above issue, we unhide BaseInputConnection#reportFinish() as InputConnection#closeConnection() so that application developers can receive an appropriate callback to clean up internal state including unfinished batch edit. [1] I5525d776916f0c42d5e6d4a4282aed590d7f0e9a 9d69ecbf61a4a142c3f4cbb9d5659faa6f85e832 Bug: 24688781 Bug: 25332806 Change-Id: I234309c5880c9fe0b299b8bd0f8862796d4dda0d --- api/current.txt | 4 ++++ api/system-current.txt | 4 ++++ api/test-current.txt | 4 ++++ .../view/inputmethod/BaseInputConnection.java | 10 ++++---- .../view/inputmethod/InputConnection.java | 16 +++++++++++++ .../inputmethod/InputConnectionInspector.java | 23 ++++++++++++++++++ .../inputmethod/InputConnectionWrapper.java | 8 +++++++ .../view/inputmethod/InputMethodManager.java | 2 +- core/java/android/widget/AbsListView.java | 5 ++++ .../view/IInputConnectionWrapper.java | 24 +++++++++---------- .../internal/view/InputConnectionWrapper.java | 4 ++++ .../widget/EditableInputConnection.java | 8 ++----- 12 files changed, 87 insertions(+), 25 deletions(-) diff --git a/api/current.txt b/api/current.txt index 0857c8bb696e..5906f996a77e 100644 --- a/api/current.txt +++ b/api/current.txt @@ -44641,6 +44641,7 @@ package android.view.inputmethod { ctor public BaseInputConnection(android.view.View, boolean); method public boolean beginBatchEdit(); method public boolean clearMetaKeyStates(int); + method public void closeConnection(); method public boolean commitCompletion(android.view.inputmethod.CompletionInfo); method public boolean commitCorrection(android.view.inputmethod.CorrectionInfo); method public boolean commitText(java.lang.CharSequence, int); @@ -44808,6 +44809,7 @@ package android.view.inputmethod { public abstract interface InputConnection { method public abstract boolean beginBatchEdit(); method public abstract boolean clearMetaKeyStates(int); + method public abstract void closeConnection(); method public abstract boolean commitCompletion(android.view.inputmethod.CompletionInfo); method public abstract boolean commitCorrection(android.view.inputmethod.CorrectionInfo); method public abstract boolean commitText(java.lang.CharSequence, int); @@ -44840,6 +44842,7 @@ package android.view.inputmethod { ctor public InputConnectionWrapper(android.view.inputmethod.InputConnection, boolean); method public boolean beginBatchEdit(); method public boolean clearMetaKeyStates(int); + method public void closeConnection(); method public boolean commitCompletion(android.view.inputmethod.CompletionInfo); method public boolean commitCorrection(android.view.inputmethod.CorrectionInfo); method public boolean commitText(java.lang.CharSequence, int); @@ -66433,3 +66436,4 @@ package org.xmlpull.v1.sax2 { } } + diff --git a/api/system-current.txt b/api/system-current.txt index 99a4ad3c448a..a3ed210d7d16 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -47370,6 +47370,7 @@ package android.view.inputmethod { ctor public BaseInputConnection(android.view.View, boolean); method public boolean beginBatchEdit(); method public boolean clearMetaKeyStates(int); + method public void closeConnection(); method public boolean commitCompletion(android.view.inputmethod.CompletionInfo); method public boolean commitCorrection(android.view.inputmethod.CorrectionInfo); method public boolean commitText(java.lang.CharSequence, int); @@ -47537,6 +47538,7 @@ package android.view.inputmethod { public abstract interface InputConnection { method public abstract boolean beginBatchEdit(); method public abstract boolean clearMetaKeyStates(int); + method public abstract void closeConnection(); method public abstract boolean commitCompletion(android.view.inputmethod.CompletionInfo); method public abstract boolean commitCorrection(android.view.inputmethod.CorrectionInfo); method public abstract boolean commitText(java.lang.CharSequence, int); @@ -47569,6 +47571,7 @@ package android.view.inputmethod { ctor public InputConnectionWrapper(android.view.inputmethod.InputConnection, boolean); method public boolean beginBatchEdit(); method public boolean clearMetaKeyStates(int); + method public void closeConnection(); method public boolean commitCompletion(android.view.inputmethod.CompletionInfo); method public boolean commitCorrection(android.view.inputmethod.CorrectionInfo); method public boolean commitText(java.lang.CharSequence, int); @@ -69498,3 +69501,4 @@ package org.xmlpull.v1.sax2 { } } + diff --git a/api/test-current.txt b/api/test-current.txt index 6cf047a2d411..6a31de4dd635 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -44715,6 +44715,7 @@ package android.view.inputmethod { ctor public BaseInputConnection(android.view.View, boolean); method public boolean beginBatchEdit(); method public boolean clearMetaKeyStates(int); + method public void closeConnection(); method public boolean commitCompletion(android.view.inputmethod.CompletionInfo); method public boolean commitCorrection(android.view.inputmethod.CorrectionInfo); method public boolean commitText(java.lang.CharSequence, int); @@ -44882,6 +44883,7 @@ package android.view.inputmethod { public abstract interface InputConnection { method public abstract boolean beginBatchEdit(); method public abstract boolean clearMetaKeyStates(int); + method public abstract void closeConnection(); method public abstract boolean commitCompletion(android.view.inputmethod.CompletionInfo); method public abstract boolean commitCorrection(android.view.inputmethod.CorrectionInfo); method public abstract boolean commitText(java.lang.CharSequence, int); @@ -44914,6 +44916,7 @@ package android.view.inputmethod { ctor public InputConnectionWrapper(android.view.inputmethod.InputConnection, boolean); method public boolean beginBatchEdit(); method public boolean clearMetaKeyStates(int); + method public void closeConnection(); method public boolean commitCompletion(android.view.inputmethod.CompletionInfo); method public boolean commitCorrection(android.view.inputmethod.CorrectionInfo); method public boolean commitText(java.lang.CharSequence, int); @@ -66507,3 +66510,4 @@ package org.xmlpull.v1.sax2 { } } + diff --git a/core/java/android/view/inputmethod/BaseInputConnection.java b/core/java/android/view/inputmethod/BaseInputConnection.java index a3c49c5a8457..89dec2d22d7a 100644 --- a/core/java/android/view/inputmethod/BaseInputConnection.java +++ b/core/java/android/view/inputmethod/BaseInputConnection.java @@ -16,6 +16,7 @@ package android.view.inputmethod; +import android.annotation.CallSuper; import android.content.Context; import android.content.res.TypedArray; import android.os.Bundle; @@ -154,12 +155,11 @@ public class BaseInputConnection implements InputConnection { } /** - * Called when this InputConnection is no longer used by the InputMethodManager. - * - * @hide + * Default implementation calls {@link #finishComposingText()}. */ - public void reportFinish() { - // Intentionally empty + @CallSuper + public void closeConnection() { + finishComposingText(); } /** diff --git a/core/java/android/view/inputmethod/InputConnection.java b/core/java/android/view/inputmethod/InputConnection.java index 8002a8e9ba14..9f6642934768 100644 --- a/core/java/android/view/inputmethod/InputConnection.java +++ b/core/java/android/view/inputmethod/InputConnection.java @@ -45,6 +45,8 @@ import android.view.KeyEvent; * was introduced in {@link android.os.Build.VERSION_CODES#N}.</li> * <li>{@link #getHandler()}}, which was introduced in * {@link android.os.Build.VERSION_CODES#N}.</li> + * <li>{@link #closeConnection()}}, which was introduced in + * {@link android.os.Build.VERSION_CODES#N}.</li> * </ul> * * <h3>Implementing an IME or an editor</h3> @@ -820,4 +822,18 @@ public interface InputConnection { * @return {@code null} to use the default {@link Handler}. */ public Handler getHandler(); + + /** + * Called by the system up to only once to notify that the system is about to invalidate + * connection between the input method and the application. + * + * <p><strong>Editor authors</strong>: You can clear all the nested batch edit right now and + * you no longer need to handle subsequent callbacks on this connection, including + * {@link #beginBatchEdit()}}. Note that although the system tries to call this method whenever + * possible, there may be a chance that this method is not called in some exceptional + * situations.</p> + * + * <p>Note: This does nothing when called from input methods.</p> + */ + public void closeConnection(); } diff --git a/core/java/android/view/inputmethod/InputConnectionInspector.java b/core/java/android/view/inputmethod/InputConnectionInspector.java index 46b2c3e0fbea..118a61f8d571 100644 --- a/core/java/android/view/inputmethod/InputConnectionInspector.java +++ b/core/java/android/view/inputmethod/InputConnectionInspector.java @@ -73,6 +73,11 @@ public final class InputConnectionInspector { * {@link android.os.Build.VERSION_CODES#N} and later. */ int GET_HANDLER = 1 << 5; + /** + * {@link InputConnection#closeConnection()}} is available in + * {@link android.os.Build.VERSION_CODES#N} and later. + */ + int CLOSE_CONNECTION = 1 << 6; } private static final Map<Class, Integer> sMissingMethodsMap = Collections.synchronizedMap( @@ -119,6 +124,9 @@ public final class InputConnectionInspector { if (!hasGetHandler(clazz)) { flags |= MissingMethodFlags.GET_HANDLER; } + if (!hasCloseConnection(clazz)) { + flags |= MissingMethodFlags.CLOSE_CONNECTION; + } sMissingMethodsMap.put(clazz, flags); return flags; } @@ -178,6 +186,15 @@ public final class InputConnectionInspector { } } + private static boolean hasCloseConnection(@NonNull final Class clazz) { + try { + final Method method = clazz.getMethod("closeConnection"); + return !Modifier.isAbstract(method.getModifiers()); + } catch (NoSuchMethodException e) { + return false; + } + } + public static String getMissingMethodFlagsAsString(@MissingMethodFlags final int flags) { final StringBuilder sb = new StringBuilder(); boolean isEmpty = true; @@ -219,6 +236,12 @@ public final class InputConnectionInspector { } sb.append("getHandler()"); } + if ((flags & MissingMethodFlags.CLOSE_CONNECTION) != 0) { + if (!isEmpty) { + sb.append(","); + } + sb.append("closeConnection()"); + } return sb.toString(); } } diff --git a/core/java/android/view/inputmethod/InputConnectionWrapper.java b/core/java/android/view/inputmethod/InputConnectionWrapper.java index 381df49d8a69..e743f62a5a0a 100644 --- a/core/java/android/view/inputmethod/InputConnectionWrapper.java +++ b/core/java/android/view/inputmethod/InputConnectionWrapper.java @@ -261,4 +261,12 @@ public class InputConnectionWrapper implements InputConnection { public Handler getHandler() { return mTarget.getHandler(); } + + /** + * {@inheritDoc} + * @throws NullPointerException if the target is {@code null}. + */ + public void closeConnection() { + mTarget.closeConnection(); + } } diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index f5908a50ec64..687990124a55 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -544,7 +544,7 @@ public final class InputMethodManager { // reportFinish() will take effect. return; } - reportFinish(); + closeConnection(); } @Override diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index 7cbe8de67492..6e1dff9ea0de 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -5937,6 +5937,11 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te public Handler getHandler() { return getTarget().getHandler(); } + + @Override + public void closeConnection() { + getTarget().closeConnection(); + } } /** diff --git a/core/java/com/android/internal/view/IInputConnectionWrapper.java b/core/java/com/android/internal/view/IInputConnectionWrapper.java index 6c1ebb43287b..3a4afad3f367 100644 --- a/core/java/com/android/internal/view/IInputConnectionWrapper.java +++ b/core/java/com/android/internal/view/IInputConnectionWrapper.java @@ -27,13 +27,12 @@ import android.os.Message; import android.os.RemoteException; import android.util.Log; import android.view.KeyEvent; -import android.view.inputmethod.BaseInputConnection; import android.view.inputmethod.CompletionInfo; import android.view.inputmethod.CorrectionInfo; import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.InputConnection; - -import java.lang.ref.WeakReference; +import android.view.inputmethod.InputConnectionInspector; +import android.view.inputmethod.InputConnectionInspector.MissingMethodFlags; public abstract class IInputConnectionWrapper extends IInputContext.Stub { static final String TAG = "IInputConnectionWrapper"; @@ -61,7 +60,7 @@ public abstract class IInputConnectionWrapper extends IInputContext.Stub { private static final int DO_PERFORM_PRIVATE_COMMAND = 120; private static final int DO_CLEAR_META_KEY_STATES = 130; private static final int DO_REQUEST_UPDATE_CURSOR_ANCHOR_INFO = 140; - private static final int DO_REPORT_FINISH = 150; + private static final int DO_CLOSE_CONNECTION = 150; @GuardedBy("mLock") @Nullable @@ -222,8 +221,8 @@ public abstract class IInputConnectionWrapper extends IInputContext.Stub { seq, callback)); } - public void reportFinish() { - dispatchMessage(obtainMessage(DO_REPORT_FINISH)); + public void closeConnection() { + dispatchMessage(obtainMessage(DO_CLOSE_CONNECTION)); } void dispatchMessage(Message msg) { @@ -501,10 +500,10 @@ public abstract class IInputConnectionWrapper extends IInputContext.Stub { } return; } - case DO_REPORT_FINISH: { + case DO_CLOSE_CONNECTION: { // Note that we do not need to worry about race condition here, because 1) mFinished // is updated only inside this block, and 2) the code here is running on a Handler - // hence we assume multiple DO_REPORT_FINISH messages will not be handled at the + // hence we assume multiple DO_CLOSE_CONNECTION messages will not be handled at the // same time. if (isFinished()) { return; @@ -518,11 +517,10 @@ public abstract class IInputConnectionWrapper extends IInputContext.Stub { if (ic == null) { return; } - ic.finishComposingText(); - // TODO: Make reportFinish() public method of InputConnection to remove this - // check. - if (ic instanceof BaseInputConnection) { - ((BaseInputConnection) ic).reportFinish(); + @MissingMethodFlags + final int missingMethods = InputConnectionInspector.getMissingMethodFlags(ic); + if ((missingMethods & MissingMethodFlags.CLOSE_CONNECTION) == 0) { + ic.closeConnection(); } } finally { synchronized (mLock) { diff --git a/core/java/com/android/internal/view/InputConnectionWrapper.java b/core/java/com/android/internal/view/InputConnectionWrapper.java index 85b8606f7500..f9884d85d52c 100644 --- a/core/java/com/android/internal/view/InputConnectionWrapper.java +++ b/core/java/com/android/internal/view/InputConnectionWrapper.java @@ -487,6 +487,10 @@ public class InputConnectionWrapper implements InputConnection { return null; } + public void closeConnection() { + // Nothing should happen when called from input method. + } + private boolean isMethodMissing(@MissingMethodFlags final int methodFlag) { return (mMissingMethods & methodFlag) == methodFlag; } diff --git a/core/java/com/android/internal/widget/EditableInputConnection.java b/core/java/com/android/internal/widget/EditableInputConnection.java index a96b5a0d6ab1..7f7528dd3de0 100644 --- a/core/java/com/android/internal/widget/EditableInputConnection.java +++ b/core/java/com/android/internal/widget/EditableInputConnection.java @@ -83,13 +83,9 @@ public class EditableInputConnection extends BaseInputConnection { return false; } - /** - * @hide - */ @Override - public void reportFinish() { - super.reportFinish(); - + public void closeConnection() { + super.closeConnection(); synchronized(this) { while (mBatchEditNesting > 0) { endBatchEdit(); -- GitLab