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.

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

Change-Id: Iff88b768c6b06cf5cf1fe9e97ee97f8f78e6f0bd
This commit is contained in:
Taran Singh
2021-01-29 16:42:52 +00:00
parent a84fbe7f17
commit d6f8a1ada1
10 changed files with 203 additions and 15 deletions

View File

@@ -51530,6 +51530,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();

View File

@@ -2554,6 +2554,10 @@ package android.view.inputmethod {
method @NonNull public static android.view.inputmethod.InlineSuggestionsResponse newInlineSuggestionsResponse(@NonNull java.util.List<android.view.inputmethod.InlineSuggestion>);
}
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 public boolean hasActiveInputConnection(@Nullable android.view.View);

View File

@@ -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

View File

@@ -70,6 +70,7 @@ import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledSince;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.content.res.Resources;
@@ -131,6 +132,7 @@ import android.widget.TextView;
import android.window.WindowMetricsHelper;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.inputmethod.IInputContentUriToken;
import com.android.internal.inputmethod.IInputMethodPrivilegedOperations;
import com.android.internal.inputmethod.InputMethodPrivilegedOperations;
@@ -513,6 +515,8 @@ public class InputMethodService extends AbstractInputMethodService {
private boolean mIsAutomotive;
private Handler mHandler;
private boolean mImeSurfaceScheduledForRemoval;
private Configuration mLastKnownConfig;
private int mHandledConfigChanges;
/**
* An opaque {@link Binder} token of window requesting {@link InputMethodImpl#showSoftInput}
@@ -588,12 +592,14 @@ 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");
mHandledConfigChanges = configChanges;
mPrivOps.set(privilegedOperations);
InputMethodPrivilegedOperationsRegistry.put(token, mPrivOps);
updateInputMethodDisplay(displayId);
@@ -821,6 +827,9 @@ public class InputMethodService extends AbstractInputMethodService {
setImeWindowStatus(mapToImeWindowStatus(), mBackDisposition);
}
final boolean isVisible = isInputViewShown();
if (isVisible && getResources() != null) {
mLastKnownConfig = getResources().getConfiguration();
}
final boolean visibilityChanged = isVisible != wasVisible;
if (resultReceiver != null) {
resultReceiver.send(visibilityChanged
@@ -1428,10 +1437,37 @@ 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.
* <p>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();
if (shouldImeRestartForConfig(newConfig)) {
resetStateForNewConfiguration();
}
}
/**
* @return {@code true} if {@link InputMethodService} needs to restart to handle
* .{@link #onConfigurationChanged(Configuration)}
*/
@VisibleForTesting
boolean shouldImeRestartForConfig(@NonNull Configuration newConfig) {
if (mLastKnownConfig == null) {
return true;
}
// If the new config is the same as the config this Service is already running with,
// then don't bother calling resetStateForNewConfiguration.
int diff = mLastKnownConfig.diffPublicOnly(newConfig);
if (diff != 0) {
// remove attrs not-relevant to IME service.
diff &= ActivityInfo.CONFIG_KEYBOARD_HIDDEN;
diff &= ActivityInfo.CONFIG_KEYBOARD;
diff &= ActivityInfo.CONFIG_NAVIGATION;
}
int unhandledDiff = (diff & ~mHandledConfigChanges);
return unhandledDiff != 0;
}
private void resetStateForNewConfiguration() {
@@ -3181,7 +3217,17 @@ public class InputMethodService extends AbstractInputMethodService {
requestHideSelf(InputMethodManager.HIDE_NOT_ALWAYS);
}
}
@VisibleForTesting
void setLastKnownConfig(@NonNull Configuration config) {
mLastKnownConfig = config;
}
@VisibleForTesting
void setHandledConfigChanges(int configChanges) {
mHandledConfigChanges = configChanges;
}
void startExtractingText(boolean inputChanged) {
final ExtractEditText eet = mExtractEditText;
if (eet != null && getCurrentInputStarted()

View File

@@ -105,7 +105,7 @@ public interface InputMethod {
*/
@MainThread
default void initializeInternal(IBinder token, int displayId,
IInputMethodPrivilegedOperations privilegedOperations) {
IInputMethodPrivilegedOperations privilegedOperations, int configChanges) {
updateInputMethodDisplay(displayId);
attachToken(token);
}

View File

@@ -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;
@@ -60,6 +64,7 @@ import java.util.List;
* @attr ref android.R.styleable#InputMethod_isDefault
* @attr ref android.R.styleable#InputMethod_supportsSwitchingToNextInputMethod
* @attr ref android.R.styleable#InputMethod_supportsInlineSuggestions
* @attr ref android.R.styleable#InputMethod_configChanges
*/
public final class InputMethodInfo implements Parcelable {
static final String TAG = "InputMethodInfo";
@@ -117,6 +122,12 @@ public final class InputMethodInfo implements Parcelable {
*/
private final boolean mInlineSuggestionsEnabled;
/**
* 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
@@ -203,6 +214,8 @@ public final class InputMethodInfo implements Parcelable {
false);
inlineSuggestionsEnabled = sa.getBoolean(
com.android.internal.R.styleable.InputMethod_supportsInlineSuggestions, false);
mHandledConfigChanges = sa.getInt(
com.android.internal.R.styleable.InputMethod_configChanges, 0);
sa.recycle();
final int depth = parser.getDepth();
@@ -287,6 +300,7 @@ public final class InputMethodInfo implements Parcelable {
mIsVrOnly = source.readBoolean();
mService = ResolveInfo.CREATOR.createFromParcel(source);
mSubtypes = new InputMethodSubtypeArray(source);
mHandledConfigChanges = source.readInt();
mForceDefault = false;
}
@@ -298,7 +312,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);
}
/**
@@ -310,7 +339,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 */);
}
/**
@@ -321,7 +350,8 @@ public final class InputMethodInfo implements Parcelable {
List<InputMethodSubtype> 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 */);
}
/**
@@ -331,7 +361,7 @@ public final class InputMethodInfo implements Parcelable {
public InputMethodInfo(ResolveInfo ri, boolean isAuxIme, String settingsActivity,
List<InputMethodSubtype> 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();
@@ -343,6 +373,7 @@ public final class InputMethodInfo implements Parcelable {
mSupportsSwitchingToNextInputMethod = supportsSwitchingToNextInputMethod;
mInlineSuggestionsEnabled = inlineSuggestionsEnabled;
mIsVrOnly = isVrOnly;
mHandledConfigChanges = handledConfigChanges;
}
private static ResolveInfo buildFakeResolveInfo(String packageName, String className,
@@ -489,6 +520,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
@@ -579,6 +621,7 @@ public final class InputMethodInfo implements Parcelable {
dest.writeBoolean(mIsVrOnly);
mService.writeToParcel(dest, flags);
mSubtypes.writeToParcel(dest);
dest.writeInt(mHandledConfigChanges);
}
/**

View File

@@ -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);

View File

@@ -3564,6 +3564,16 @@
<attr name="__removed2" format="boolean" />
<!-- Specifies whether the IME supports showing inline suggestions. -->
<attr name="supportsInlineSuggestions" format="boolean" />
<!-- Specify one or more configuration changes that the IME will handle itself. If not
specified, the IME will be restarted if any of these configuration changes happen in
the system. Otherwise, the IME will remain running and its
{@link android.inputmethodservice.InputMethodService#onConfigurationChanged}
method is called with the new configuration.
<p>Note that all of these configuration changes can impact the
resource values seen by the application, so you will generally need
to re-retrieve all resources (including view layouts, drawables, etc)
to correctly handle any configuration change.-->
<attr name="configChanges" />
</declare-styleable>
<!-- This is the subtype of InputMethod. Subtype can describe locales (for example, en_US and

View File

@@ -0,0 +1,80 @@
/*
* 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 static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static junit.framework.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.os.Build;
import androidx.test.filters.SmallTest;
import androidx.test.rule.ServiceTestRule;
import androidx.test.runner.AndroidJUnit4;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.concurrent.TimeoutException;
@SmallTest
@RunWith(AndroidJUnit4.class)
public class InputMethodServiceTest {
private InputMethodService mService;
private Context mContext;
@Rule
public final ServiceTestRule serviceRule = new ServiceTestRule();
@Before
public void setUp() throws TimeoutException {
mContext = getInstrumentation().getContext();
mService = new InputMethodService();
}
@Test
public void testShouldImeRestartForConfig() throws Exception {
// Make sure we preserve Pre-S behavior i.e. Service restarts.
mContext.getApplicationInfo().targetSdkVersion = Build.VERSION_CODES.R;
Configuration config = mContext.getResources().getConfiguration();
mService.setLastKnownConfig(config);
assertTrue("IME should restart for Pre-S",
mService.shouldImeRestartForConfig(config));
// IME shouldn't restart on targetSdk S+ (with no config changes).
mContext.getApplicationInfo().targetSdkVersion = Build.VERSION_CODES.S;
assertFalse("IME shouldn't restart for S+",
mService.shouldImeRestartForConfig(config));
// Screen density changed but IME doesn't handle congfigChanges
config.densityDpi = 99;
assertTrue("IME should restart for unhandled configChanges",
mService.shouldImeRestartForConfig(config));
// opt-in IME to handle config changes.
mService.setHandledConfigChanges(ActivityInfo.CONFIG_DENSITY);
assertFalse("IME shouldn't restart for S+ since it handles configChanges",
mService.shouldImeRestartForConfig(config));
}
}

View File

@@ -2620,8 +2620,9 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
}
if (DEBUG) Slog.v(TAG, "Initiating attach with token: " + mCurToken);
// Dispatch display id for InputMethodService to update context display.
executeOrSendMessage(mCurMethod, mCaller.obtainMessageIOO(
MSG_INITIALIZE_IME, mCurTokenDisplayId, mCurMethod, mCurToken));
executeOrSendMessage(mCurMethod, mCaller.obtainMessageIOOO(
MSG_INITIALIZE_IME, mCurTokenDisplayId, mCurMethod, mCurToken,
mMethodMap.get(mCurMethodId).getConfigChanges()));
scheduleNotifyImeUidToAudioService(mCurMethodUid);
if (mCurClient != null) {
clearClientSessionLocked(mCurClient);
@@ -4466,7 +4467,8 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
}
final IBinder token = (IBinder) args.arg2;
((IInputMethod) args.arg1).initializeInternal(token, msg.arg1,
new InputMethodPrivilegedOperationsImpl(this, token));
new InputMethodPrivilegedOperationsImpl(this, token),
(int) args.arg3);
} catch (RemoteException e) {
}
args.recycle();