Merge "Avoid IME restart for configChanges" into sc-dev

This commit is contained in:
Yohei Yukawa
2021-03-31 02:28:22 +00:00
committed by Android (Google) Code Review
11 changed files with 263 additions and 15 deletions

View File

@@ -51921,6 +51921,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

@@ -2899,6 +2899,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 @NonNull @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL) public java.util.List<android.view.inputmethod.InputMethodInfo> getInputMethodListAsUser(int);

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

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

View File

@@ -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.
* <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();
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()

View File

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

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;
@@ -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<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 */);
}
/**
@@ -353,7 +383,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();
@@ -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);
}
/**

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

@@ -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.-->
<attr name="showInInputMethodPicker" 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,77 @@
/*
* 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 org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
@SmallTest
@RunWith(AndroidJUnit4.class)
public class ImsConfigurationTrackerTest {
private ImsConfigurationTracker mImsConfigTracker;
private Context mContext;
@Before
public void setUp() throws TimeoutException {
mContext = getInstrumentation().getContext();
mImsConfigTracker = new ImsConfigurationTracker();
}
@Test
public void testShouldImeRestart() throws Exception {
Configuration config = mContext.getResources().getConfiguration();
mImsConfigTracker.onInitialize(0 /* handledConfigChanges */);
mImsConfigTracker.onBindInput(mContext.getResources());
Configuration newConfig = new Configuration(config);
final AtomicBoolean didReset = new AtomicBoolean();
Runnable resetStateRunner = () -> didReset.set(true);
mImsConfigTracker.onConfigurationChanged(newConfig, resetStateRunner);
assertFalse("IME shouldn't restart if config hasn't changed",
didReset.get());
// Screen density changed but IME doesn't handle configChanges
newConfig.densityDpi = 99;
mImsConfigTracker.onConfigurationChanged(newConfig, resetStateRunner);
assertTrue("IME should restart for unhandled configChanges",
didReset.get());
didReset.set(false);
// opt-in IME to handle density config changes.
mImsConfigTracker.setHandledConfigChanges(ActivityInfo.CONFIG_DENSITY);
mImsConfigTracker.onConfigurationChanged(newConfig, resetStateRunner);
assertFalse("IME shouldn't restart since it handles configChanges",
didReset.get());
}
}

View File

@@ -2623,8 +2623,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);
@@ -4477,7 +4478,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();