From 71ab752fc59e3db45e12d6ed9c922eb1ffca84a7 Mon Sep 17 00:00:00 2001 From: Taran Singh Date: Fri, 19 Mar 2021 15:34:02 +0000 Subject: [PATCH] Avoid IME restart for configChanges Handle onConfigurationChanged() in order to prevent restarting InputMethodService everytime. We introduce a new API attribute "configChanges" in InputMethod(attrs.xml) which when declared by IME, will be responsible for handling mentioned configuration changes. This CL re-introduces [1] with fix: Use new Configuration instance for IMS#mLastKnownConfig and also handle followup comments. [1] Ib94fddadb0dae648cf73a4c1642e51edebd19f50 Note: this change has no impact for devices not using DisplayAreas. Bug: 167948419 Test: atest InputMethodServiceTest Manually: 1. Patch Ie91e7a8e06b80864ef9409031e8543858552d70d to use dual display area. 2. Open applications with editors on both display areas. 3. Attach a debug point for IMS#onConfigurationChanged(). 4. Make sure IMS#resetStateForNewConfiguration() is not called when IME moves between these two identical DisplayAreas Also verify that bug 182604598 don't happen. Change-Id: I43b6b80cdb35410554412ee1d3b0917ee3198272 --- core/api/current.txt | 1 + core/api/test-current.txt | 4 + .../IInputMethodWrapper.java | 7 +- .../ImsConfigurationTracker.java | 102 ++++++++++++++++++ .../InputMethodService.java | 12 ++- .../android/view/inputmethod/InputMethod.java | 3 +- .../view/inputmethod/InputMethodInfo.java | 51 ++++++++- .../android/internal/view/IInputMethod.aidl | 3 +- core/res/res/values/attrs.xml | 10 ++ .../ImsConfigurationTrackerTest.java | 77 +++++++++++++ .../InputMethodManagerService.java | 8 +- 11 files changed, 263 insertions(+), 15 deletions(-) create mode 100644 core/java/android/inputmethodservice/ImsConfigurationTracker.java create mode 100644 core/tests/coretests/src/android/inputmethodservice/ImsConfigurationTrackerTest.java diff --git a/core/api/current.txt b/core/api/current.txt index 5622f4b14d94d..13a01ae359d1f 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -51914,6 +51914,7 @@ package android.view.inputmethod { method public int describeContents(); method public void dump(android.util.Printer, String); method public android.content.ComponentName getComponent(); + method public int getConfigChanges(); method public String getId(); method public int getIsDefaultResourceId(); method public String getPackageName(); diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 56b0a9d5a4d04..6745a69e001e3 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -2899,6 +2899,10 @@ package android.view.inputmethod { method @NonNull public static android.view.inputmethod.InlineSuggestionsResponse newInlineSuggestionsResponse(@NonNull java.util.List); } + public final class InputMethodInfo implements android.os.Parcelable { + ctor public InputMethodInfo(@NonNull String, @NonNull String, @NonNull CharSequence, @NonNull String, int); + } + public final class InputMethodManager { method public int getDisplayId(); method @NonNull @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL) public java.util.List getInputMethodListAsUser(int); diff --git a/core/java/android/inputmethodservice/IInputMethodWrapper.java b/core/java/android/inputmethodservice/IInputMethodWrapper.java index 5cfcd667632b0..9198eb74d1f84 100644 --- a/core/java/android/inputmethodservice/IInputMethodWrapper.java +++ b/core/java/android/inputmethodservice/IInputMethodWrapper.java @@ -171,7 +171,7 @@ class IInputMethodWrapper extends IInputMethod.Stub SomeArgs args = (SomeArgs) msg.obj; try { inputMethod.initializeInternal((IBinder) args.arg1, msg.arg1, - (IInputMethodPrivilegedOperations) args.arg2); + (IInputMethodPrivilegedOperations) args.arg2, (int) args.arg3); } finally { args.recycle(); } @@ -280,9 +280,10 @@ class IInputMethodWrapper extends IInputMethod.Stub @BinderThread @Override public void initializeInternal(IBinder token, int displayId, - IInputMethodPrivilegedOperations privOps) { + IInputMethodPrivilegedOperations privOps, int configChanges) { mCaller.executeOrSendMessage( - mCaller.obtainMessageIOO(DO_INITIALIZE_INTERNAL, displayId, token, privOps)); + mCaller.obtainMessageIOOO(DO_INITIALIZE_INTERNAL, displayId, token, privOps, + configChanges)); } @BinderThread diff --git a/core/java/android/inputmethodservice/ImsConfigurationTracker.java b/core/java/android/inputmethodservice/ImsConfigurationTracker.java new file mode 100644 index 0000000000000..3c788884371b0 --- /dev/null +++ b/core/java/android/inputmethodservice/ImsConfigurationTracker.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2021 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.annotation.Nullable; +import android.content.res.Configuration; +import android.content.res.Resources; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.Preconditions; + +/** + * Helper class that takes care of Configuration change behavior of {@link InputMethodService}. + * Note: this class is public for testing only. Never call any of it's methods for development + * of IMEs. + * @hide + */ +@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) +public final class ImsConfigurationTracker { + + /** + * A constant value that represents {@link Configuration} has changed from the last time + * {@link InputMethodService#onConfigurationChanged(Configuration)} was called. + */ + private static final int CONFIG_CHANGED = -1; + + @Nullable + private Configuration mLastKnownConfig = null; + private int mHandledConfigChanges = 0; + private boolean mInitialized = false; + + /** + * Called from {@link InputMethodService.InputMethodImpl + * #initializeInternal(IBinder, int, IInputMethodPrivilegedOperations, int)} ()} + * @param handledConfigChanges Configuration changes declared handled by IME + * {@link android.R.styleable#InputMethod_configChanges}. + */ + @MainThread + public void onInitialize(int handledConfigChanges) { + Preconditions.checkState(!mInitialized, "onInitialize can be called only once."); + mInitialized = true; + mHandledConfigChanges = handledConfigChanges; + } + + /** + * Called from {@link InputMethodService.InputMethodImpl#onBindInput()} + */ + @MainThread + public void onBindInput(@Nullable Resources resources) { + Preconditions.checkState(mInitialized, + "onBindInput can be called only after onInitialize()."); + if (mLastKnownConfig == null && resources != null) { + mLastKnownConfig = new Configuration(resources.getConfiguration()); + } + } + + /** + * Dynamically set handled configChanges. + * Note: this method is public for testing only. + */ + public void setHandledConfigChanges(int configChanges) { + mHandledConfigChanges = configChanges; + } + + /** + * Called from {@link InputMethodService.InputMethodImpl#onConfigurationChanged(Configuration)}} + */ + @MainThread + public void onConfigurationChanged(@NonNull Configuration newConfig, + @NonNull Runnable resetStateForNewConfigurationRunner) { + if (!mInitialized) { + return; + } + final int diff = mLastKnownConfig != null + ? mLastKnownConfig.diffPublicOnly(newConfig) : CONFIG_CHANGED; + // If the new config is the same as the config this Service is already running with, + // then don't bother calling resetStateForNewConfiguration. + final int unhandledDiff = (diff & ~mHandledConfigChanges); + if (unhandledDiff != 0) { + resetStateForNewConfigurationRunner.run(); + } + if (diff != 0) { + mLastKnownConfig = new Configuration(newConfig); + } + } +} diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java index 7e2be01feb014..bf016124da312 100644 --- a/core/java/android/inputmethodservice/InputMethodService.java +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -513,6 +513,7 @@ public class InputMethodService extends AbstractInputMethodService { private boolean mIsAutomotive; private Handler mHandler; private boolean mImeSurfaceScheduledForRemoval; + private ImsConfigurationTracker mConfigTracker = new ImsConfigurationTracker(); /** * An opaque {@link Binder} token of window requesting {@link InputMethodImpl#showSoftInput} @@ -588,12 +589,13 @@ public class InputMethodService extends AbstractInputMethodService { @MainThread @Override public final void initializeInternal(@NonNull IBinder token, int displayId, - IInputMethodPrivilegedOperations privilegedOperations) { + IInputMethodPrivilegedOperations privilegedOperations, int configChanges) { if (InputMethodPrivilegedOperationsRegistry.isRegistered(token)) { Log.w(TAG, "The token has already registered, ignore this initialization."); return; } Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMS.initializeInternal"); + mConfigTracker.onInitialize(configChanges); mPrivOps.set(privilegedOperations); InputMethodPrivilegedOperationsRegistry.put(token, mPrivOps); updateInputMethodDisplay(displayId); @@ -663,6 +665,7 @@ public class InputMethodService extends AbstractInputMethodService { reportFullscreenMode(); initialize(); onBindInput(); + mConfigTracker.onBindInput(getResources()); Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); } @@ -1428,10 +1431,13 @@ public class InputMethodService extends AbstractInputMethodService { * state: {@link #onStartInput} if input is active, and * {@link #onCreateInputView} and {@link #onStartInputView} and related * appropriate functions if the UI is displayed. + *

Starting with {@link Build.VERSION_CODES#S}, IMEs can opt into handling configuration + * changes themselves instead of being restarted with + * {@link android.R.styleable#InputMethod_configChanges}. */ @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); - resetStateForNewConfiguration(); + mConfigTracker.onConfigurationChanged(newConfig, this::resetStateForNewConfiguration); } private void resetStateForNewConfiguration() { @@ -3181,7 +3187,7 @@ public class InputMethodService extends AbstractInputMethodService { requestHideSelf(InputMethodManager.HIDE_NOT_ALWAYS); } } - + void startExtractingText(boolean inputChanged) { final ExtractEditText eet = mExtractEditText; if (eet != null && getCurrentInputStarted() diff --git a/core/java/android/view/inputmethod/InputMethod.java b/core/java/android/view/inputmethod/InputMethod.java index de4554b9e6249..d2db0df6c597c 100644 --- a/core/java/android/view/inputmethod/InputMethod.java +++ b/core/java/android/view/inputmethod/InputMethod.java @@ -101,11 +101,12 @@ public interface InputMethod { * @param privilegedOperations IPC endpoint to do some privileged * operations that are allowed only to the * current IME. + * @param configChanges {@link InputMethodInfo#getConfigChanges()} declared by IME. * @hide */ @MainThread default void initializeInternal(IBinder token, int displayId, - IInputMethodPrivilegedOperations privilegedOperations) { + IInputMethodPrivilegedOperations privilegedOperations, int configChanges) { updateInputMethodDisplay(displayId); attachToken(token); } diff --git a/core/java/android/view/inputmethod/InputMethodInfo.java b/core/java/android/view/inputmethod/InputMethodInfo.java index 6ba3b37ce2140..c26b302db9834 100644 --- a/core/java/android/view/inputmethod/InputMethodInfo.java +++ b/core/java/android/view/inputmethod/InputMethodInfo.java @@ -18,19 +18,23 @@ package android.view.inputmethod; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.TestApi; import android.compat.annotation.UnsupportedAppUsage; import android.content.ComponentName; import android.content.Context; +import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; +import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.Resources.NotFoundException; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; import android.graphics.drawable.Drawable; +import android.inputmethodservice.InputMethodService; import android.os.Parcel; import android.os.Parcelable; import android.util.AttributeSet; @@ -62,6 +66,7 @@ import java.util.List; * @attr ref android.R.styleable#InputMethod_supportsInlineSuggestions * @attr ref android.R.styleable#InputMethod_suppressesSpellChecker * @attr ref android.R.styleable#InputMethod_showInInputMethodPicker + * @attr ref android.R.styleable#InputMethod_configChanges */ public final class InputMethodInfo implements Parcelable { static final String TAG = "InputMethodInfo"; @@ -129,6 +134,12 @@ public final class InputMethodInfo implements Parcelable { */ private final boolean mShowInInputMethodPicker; + /** + * The flag for configurations IME assumes the responsibility for handling in + * {@link InputMethodService#onConfigurationChanged(Configuration)}}. + */ + private final int mHandledConfigChanges; + /** * @param service the {@link ResolveInfo} corresponds in which the IME is implemented. * @return a unique ID to be returned by {@link #getId()}. We have used @@ -221,6 +232,8 @@ public final class InputMethodInfo implements Parcelable { com.android.internal.R.styleable.InputMethod_suppressesSpellChecker, false); showInInputMethodPicker = sa.getBoolean( com.android.internal.R.styleable.InputMethod_showInInputMethodPicker, true); + mHandledConfigChanges = sa.getInt( + com.android.internal.R.styleable.InputMethod_configChanges, 0); sa.recycle(); final int depth = parser.getDepth(); @@ -309,6 +322,7 @@ public final class InputMethodInfo implements Parcelable { mIsVrOnly = source.readBoolean(); mService = ResolveInfo.CREATOR.createFromParcel(source); mSubtypes = new InputMethodSubtypeArray(source); + mHandledConfigChanges = source.readInt(); mForceDefault = false; } @@ -320,7 +334,22 @@ public final class InputMethodInfo implements Parcelable { this(buildFakeResolveInfo(packageName, className, label), false /* isAuxIme */, settingsActivity, null /* subtypes */, 0 /* isDefaultResId */, false /* forceDefault */, true /* supportsSwitchingToNextInputMethod */, - false /* inlineSuggestionsEnabled */, false /* isVrOnly */); + false /* inlineSuggestionsEnabled */, false /* isVrOnly */, + 0 /* handledConfigChanges */); + } + + /** + * Temporary API for creating a built-in input method for test. + * @hide + */ + @TestApi + public InputMethodInfo(@NonNull String packageName, @NonNull String className, + @NonNull CharSequence label, @NonNull String settingsActivity, + int handledConfigChanges) { + this(buildFakeResolveInfo(packageName, className, label), false /* isAuxIme */, + settingsActivity, null /* subtypes */, 0 /* isDefaultResId */, + false /* forceDefault */, true /* supportsSwitchingToNextInputMethod */, + false /* inlineSuggestionsEnabled */, false /* isVrOnly */, handledConfigChanges); } /** @@ -332,7 +361,7 @@ public final class InputMethodInfo implements Parcelable { boolean forceDefault) { this(ri, isAuxIme, settingsActivity, subtypes, isDefaultResId, forceDefault, true /* supportsSwitchingToNextInputMethod */, false /* inlineSuggestionsEnabled */, - false /* isVrOnly */); + false /* isVrOnly */, 0 /* handledconfigChanges */); } /** @@ -343,7 +372,8 @@ public final class InputMethodInfo implements Parcelable { List subtypes, int isDefaultResId, boolean forceDefault, boolean supportsSwitchingToNextInputMethod, boolean isVrOnly) { this(ri, isAuxIme, settingsActivity, subtypes, isDefaultResId, forceDefault, - supportsSwitchingToNextInputMethod, false /* inlineSuggestionsEnabled */, isVrOnly); + supportsSwitchingToNextInputMethod, false /* inlineSuggestionsEnabled */, isVrOnly, + 0 /* handledConfigChanges */); } /** @@ -353,7 +383,7 @@ public final class InputMethodInfo implements Parcelable { public InputMethodInfo(ResolveInfo ri, boolean isAuxIme, String settingsActivity, List subtypes, int isDefaultResId, boolean forceDefault, boolean supportsSwitchingToNextInputMethod, boolean inlineSuggestionsEnabled, - boolean isVrOnly) { + boolean isVrOnly, int handledConfigChanges) { final ServiceInfo si = ri.serviceInfo; mService = ri; mId = new ComponentName(si.packageName, si.name).flattenToShortString(); @@ -367,6 +397,7 @@ public final class InputMethodInfo implements Parcelable { mSuppressesSpellChecker = false; mShowInInputMethodPicker = true; mIsVrOnly = isVrOnly; + mHandledConfigChanges = handledConfigChanges; } private static ResolveInfo buildFakeResolveInfo(String packageName, String className, @@ -513,6 +544,17 @@ public final class InputMethodInfo implements Parcelable { } } + /** + * Returns the bit mask of kinds of configuration changes that this IME + * can handle itself (without being restarted by the system). + * + * @attr ref android.R.styleable#InputMethod_configChanges + */ + @ActivityInfo.Config + public int getConfigChanges() { + return mHandledConfigChanges; + } + public void dump(Printer pw, String prefix) { pw.println(prefix + "mId=" + mId + " mSettingsActivityName=" + mSettingsActivityName @@ -622,6 +664,7 @@ public final class InputMethodInfo implements Parcelable { dest.writeBoolean(mIsVrOnly); mService.writeToParcel(dest, flags); mSubtypes.writeToParcel(dest); + dest.writeInt(mHandledConfigChanges); } /** diff --git a/core/java/com/android/internal/view/IInputMethod.aidl b/core/java/com/android/internal/view/IInputMethod.aidl index c336373539844..8d82e33dc29ff 100644 --- a/core/java/com/android/internal/view/IInputMethod.aidl +++ b/core/java/com/android/internal/view/IInputMethod.aidl @@ -35,7 +35,8 @@ import com.android.internal.view.InlineSuggestionsRequestInfo; * {@hide} */ oneway interface IInputMethod { - void initializeInternal(IBinder token, int displayId, IInputMethodPrivilegedOperations privOps); + void initializeInternal(IBinder token, int displayId, IInputMethodPrivilegedOperations privOps, + int configChanges); void onCreateInlineSuggestionsRequest(in InlineSuggestionsRequestInfo requestInfo, in IInlineSuggestionsRequestCallback cb); diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index 3940dc556d41c..f508b3ed5d3ce 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -3585,6 +3585,16 @@ Note: This functions as a hint to the system, which may choose to ignore this preference in certain situations or in future releases.--> + +