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:
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user