blob: c8c0482f5a9dd29c402e777c88ce34f5c38e9297 [file] [log] [blame]
/*
* 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 com.android.server.inputmethod;
import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManagerInternal;
import android.inputmethodservice.InputMethodService;
import android.os.Binder;
import android.os.IBinder;
import android.os.Process;
import android.os.SystemClock;
import android.os.Trace;
import android.os.UserHandle;
import android.provider.Settings;
import android.util.EventLog;
import android.util.Slog;
import android.view.WindowManager;
import android.view.inputmethod.InputMethod;
import android.view.inputmethod.InputMethodInfo;
import android.view.inputmethod.InputMethodManager;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.inputmethod.IInputMethod;
import com.android.internal.inputmethod.InputBindResult;
import com.android.internal.inputmethod.UnbindReason;
import com.android.server.EventLogTags;
import com.android.server.wm.WindowManagerInternal;
import java.util.concurrent.CountDownLatch;
/**
* A controller managing the state of the input method binding.
*/
final class InputMethodBindingController {
static final boolean DEBUG = false;
private static final String TAG = InputMethodBindingController.class.getSimpleName();
/** Time in milliseconds that the IME service has to bind before it is reconnected. */
static final long TIME_TO_RECONNECT = 3 * 1000;
@NonNull private final InputMethodManagerService mService;
@NonNull private final Context mContext;
@NonNull private final PackageManagerInternal mPackageManagerInternal;
@NonNull private final WindowManagerInternal mWindowManagerInternal;
@GuardedBy("ImfLock.class") private long mLastBindTime;
@GuardedBy("ImfLock.class") private boolean mHasMainConnection;
@GuardedBy("ImfLock.class") @Nullable private String mCurId;
@GuardedBy("ImfLock.class") @Nullable private String mSelectedMethodId;
@GuardedBy("ImfLock.class") @Nullable private Intent mCurIntent;
@GuardedBy("ImfLock.class") @Nullable private IInputMethodInvoker mCurMethod;
@GuardedBy("ImfLock.class") private int mCurMethodUid = Process.INVALID_UID;
@GuardedBy("ImfLock.class") @Nullable private IBinder mCurToken;
@GuardedBy("ImfLock.class") private int mCurSeq;
@GuardedBy("ImfLock.class") private boolean mVisibleBound;
@GuardedBy("ImfLock.class") private boolean mSupportsStylusHw;
@Nullable private CountDownLatch mLatchForTesting;
/**
* Binding flags for establishing connection to the {@link InputMethodService}.
*/
@VisibleForTesting
static final int IME_CONNECTION_BIND_FLAGS =
Context.BIND_AUTO_CREATE
| Context.BIND_NOT_VISIBLE
| Context.BIND_NOT_FOREGROUND
| Context.BIND_IMPORTANT_BACKGROUND
| Context.BIND_SCHEDULE_LIKE_TOP_APP;
private final int mImeConnectionBindFlags;
/**
* Binding flags used only while the {@link InputMethodService} is showing window.
*/
@VisibleForTesting
static final int IME_VISIBLE_BIND_FLAGS =
Context.BIND_AUTO_CREATE
| Context.BIND_TREAT_LIKE_ACTIVITY
| Context.BIND_FOREGROUND_SERVICE
| Context.BIND_INCLUDE_CAPABILITIES
| Context.BIND_SHOWING_UI;
InputMethodBindingController(@NonNull InputMethodManagerService service) {
this(service, IME_CONNECTION_BIND_FLAGS, null /* latchForTesting */);
}
InputMethodBindingController(@NonNull InputMethodManagerService service,
int imeConnectionBindFlags, CountDownLatch latchForTesting) {
mService = service;
mContext = mService.mContext;
mPackageManagerInternal = mService.mPackageManagerInternal;
mWindowManagerInternal = mService.mWindowManagerInternal;
mImeConnectionBindFlags = imeConnectionBindFlags;
mLatchForTesting = latchForTesting;
}
/**
* Time that we last initiated a bind to the input method, to determine
* if we should try to disconnect and reconnect to it.
*/
@GuardedBy("ImfLock.class")
long getLastBindTime() {
return mLastBindTime;
}
/**
* Set to true if our ServiceConnection is currently actively bound to
* a service (whether or not we have gotten its IBinder back yet).
*/
@GuardedBy("ImfLock.class")
boolean hasMainConnection() {
return mHasMainConnection;
}
/**
* Id obtained with {@link InputMethodInfo#getId()} for the input method that we are currently
* connected to or in the process of connecting to.
*
* <p>This can be {@code null} when no input method is connected.</p>
*
* @see #getSelectedMethodId()
*/
@GuardedBy("ImfLock.class")
@Nullable
String getCurId() {
return mCurId;
}
/**
* Id obtained with {@link InputMethodInfo#getId()} for the currently selected input method.
* This is to be synchronized with the secure settings keyed with
* {@link android.provider.Settings.Secure#DEFAULT_INPUT_METHOD}.
*
* <p>This can be transiently {@code null} when the system is re-initializing input method
* settings, e.g., the system locale is just changed.</p>
*
* <p>Note that {@link #getCurId()} is used to track which IME is being connected to
* {@link com.android.server.inputmethod.InputMethodManagerService}.</p>
*
* @see #getCurId()
*/
@GuardedBy("ImfLock.class")
@Nullable
String getSelectedMethodId() {
return mSelectedMethodId;
}
@GuardedBy("ImfLock.class")
void setSelectedMethodId(@Nullable String selectedMethodId) {
mSelectedMethodId = selectedMethodId;
}
/**
* The token we have made for the currently active input method, to
* identify it in the future.
*/
@GuardedBy("ImfLock.class")
@Nullable
IBinder getCurToken() {
return mCurToken;
}
/**
* The Intent used to connect to the current input method.
*/
@GuardedBy("ImfLock.class")
@Nullable
Intent getCurIntent() {
return mCurIntent;
}
/**
* The current binding sequence number, incremented every time there is
* a new bind performed.
*/
@GuardedBy("ImfLock.class")
int getSequenceNumber() {
return mCurSeq;
}
/**
* Increase the current binding sequence number by one.
* Reset to 1 on overflow.
*/
@GuardedBy("ImfLock.class")
void advanceSequenceNumber() {
mCurSeq += 1;
if (mCurSeq <= 0) {
mCurSeq = 1;
}
}
/**
* If non-null, this is the input method service we are currently connected
* to.
*/
@GuardedBy("ImfLock.class")
@Nullable
IInputMethodInvoker getCurMethod() {
return mCurMethod;
}
/**
* If not {@link Process#INVALID_UID}, then the UID of {@link #getCurIntent()}.
*/
@GuardedBy("ImfLock.class")
int getCurMethodUid() {
return mCurMethodUid;
}
/**
* Indicates whether {@link #mVisibleConnection} is currently in use.
*/
@GuardedBy("ImfLock.class")
boolean isVisibleBound() {
return mVisibleBound;
}
/**
* Returns {@code true} if current IME supports Stylus Handwriting.
*/
boolean supportsStylusHandwriting() {
return mSupportsStylusHw;
}
/**
* Used to bring IME service up to visible adjustment while it is being shown.
*/
@GuardedBy("ImfLock.class")
private final ServiceConnection mVisibleConnection = new ServiceConnection() {
@Override public void onBindingDied(ComponentName name) {
synchronized (ImfLock.class) {
mService.invalidateAutofillSessionLocked();
if (isVisibleBound()) {
unbindVisibleConnection();
}
}
}
@Override public void onServiceConnected(ComponentName name, IBinder service) {
}
@Override public void onServiceDisconnected(ComponentName name) {
synchronized (ImfLock.class) {
mService.invalidateAutofillSessionLocked();
}
}
};
/**
* Used to bind the IME while it is not currently being shown.
*/
@GuardedBy("ImfLock.class")
private final ServiceConnection mMainConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.onServiceConnected");
synchronized (ImfLock.class) {
if (mCurIntent != null && name.equals(mCurIntent.getComponent())) {
mCurMethod = IInputMethodInvoker.create(IInputMethod.Stub.asInterface(service));
updateCurrentMethodUid();
if (mCurToken == null) {
Slog.w(TAG, "Service connected without a token!");
unbindCurrentMethod();
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
return;
}
if (DEBUG) Slog.v(TAG, "Initiating attach with token: " + mCurToken);
final InputMethodInfo info =
mService.queryInputMethodForCurrentUserLocked(mSelectedMethodId);
boolean supportsStylusHwChanged =
mSupportsStylusHw != info.supportsStylusHandwriting();
mSupportsStylusHw = info.supportsStylusHandwriting();
if (supportsStylusHwChanged) {
InputMethodManager.invalidateLocalStylusHandwritingAvailabilityCaches();
}
mService.initializeImeLocked(mCurMethod, mCurToken);
mService.scheduleNotifyImeUidToAudioService(mCurMethodUid);
mService.reRequestCurrentClientSessionLocked();
mService.performOnCreateInlineSuggestionsRequestLocked();
}
// reset Handwriting event receiver.
// always call this as it handles changes in mSupportsStylusHw. It is a noop
// if unchanged.
mService.scheduleResetStylusHandwriting();
}
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
if (mLatchForTesting != null) {
mLatchForTesting.countDown(); // Notify the finish to tests
}
}
@GuardedBy("ImfLock.class")
private void updateCurrentMethodUid() {
final String curMethodPackage = mCurIntent.getComponent().getPackageName();
final int curMethodUid = mPackageManagerInternal.getPackageUid(
curMethodPackage, 0 /* flags */, mService.getCurrentImeUserIdLocked());
if (curMethodUid < 0) {
Slog.e(TAG, "Failed to get UID for package=" + curMethodPackage);
mCurMethodUid = Process.INVALID_UID;
} else {
mCurMethodUid = curMethodUid;
}
}
@Override
public void onServiceDisconnected(@NonNull ComponentName name) {
// Note that mContext.unbindService(this) does not trigger this. Hence if we are
// here the
// disconnection is not intended by IMMS (e.g. triggered because the current IMS
// crashed),
// which is irregular but can eventually happen for everyone just by continuing
// using the
// device. Thus it is important to make sure that all the internal states are
// properly
// refreshed when this method is called back. Running
// adb install -r <APK that implements the current IME>
// would be a good way to trigger such a situation.
synchronized (ImfLock.class) {
if (DEBUG) {
Slog.v(TAG, "Service disconnected: " + name + " mCurIntent=" + mCurIntent);
}
if (mCurMethod != null && mCurIntent != null
&& name.equals(mCurIntent.getComponent())) {
// We consider this to be a new bind attempt, since the system
// should now try to restart the service for us.
mLastBindTime = SystemClock.uptimeMillis();
clearCurMethodAndSessions();
mService.clearInputShownLocked();
mService.unbindCurrentClientLocked(UnbindReason.DISCONNECT_IME);
}
}
}
};
@GuardedBy("ImfLock.class")
void unbindCurrentMethod() {
if (isVisibleBound()) {
unbindVisibleConnection();
}
if (hasMainConnection()) {
unbindMainConnection();
}
if (getCurToken() != null) {
removeCurrentToken();
mService.resetSystemUiLocked();
}
mCurId = null;
clearCurMethodAndSessions();
}
@GuardedBy("ImfLock.class")
private void clearCurMethodAndSessions() {
mService.clearClientSessionsLocked();
mCurMethod = null;
mCurMethodUid = Process.INVALID_UID;
}
@GuardedBy("ImfLock.class")
private void removeCurrentToken() {
int curTokenDisplayId = mService.getCurTokenDisplayIdLocked();
if (DEBUG) {
Slog.v(TAG,
"Removing window token: " + mCurToken + " for display: " + curTokenDisplayId);
}
mWindowManagerInternal.removeWindowToken(mCurToken, false /* removeWindows */,
false /* animateExit */, curTokenDisplayId);
mCurToken = null;
}
@GuardedBy("ImfLock.class")
@NonNull
InputBindResult bindCurrentMethod() {
if (mSelectedMethodId == null) {
Slog.e(TAG, "mSelectedMethodId is null!");
return InputBindResult.NO_IME;
}
InputMethodInfo info = mService.queryInputMethodForCurrentUserLocked(mSelectedMethodId);
if (info == null) {
throw new IllegalArgumentException("Unknown id: " + mSelectedMethodId);
}
mCurIntent = createImeBindingIntent(info.getComponent());
if (bindCurrentInputMethodServiceMainConnection()) {
mCurId = info.getId();
mLastBindTime = SystemClock.uptimeMillis();
addFreshWindowToken();
return new InputBindResult(
InputBindResult.ResultCode.SUCCESS_WAITING_IME_BINDING,
null, null, null, mCurId, mCurSeq, false);
}
Slog.w(InputMethodManagerService.TAG,
"Failure connecting to input method service: " + mCurIntent);
mCurIntent = null;
return InputBindResult.IME_NOT_CONNECTED;
}
@NonNull
private Intent createImeBindingIntent(ComponentName component) {
Intent intent = new Intent(InputMethod.SERVICE_INTERFACE);
intent.setComponent(component);
intent.putExtra(Intent.EXTRA_CLIENT_LABEL,
com.android.internal.R.string.input_method_binding_label);
intent.putExtra(Intent.EXTRA_CLIENT_INTENT, PendingIntent.getActivity(
mContext, 0, new Intent(Settings.ACTION_INPUT_METHOD_SETTINGS),
PendingIntent.FLAG_IMMUTABLE));
return intent;
}
@GuardedBy("ImfLock.class")
private void addFreshWindowToken() {
int displayIdToShowIme = mService.getDisplayIdToShowImeLocked();
mCurToken = new Binder();
mService.setCurTokenDisplayIdLocked(displayIdToShowIme);
if (DEBUG) {
Slog.v(TAG, "Adding window token: " + mCurToken + " for display: "
+ displayIdToShowIme);
}
mWindowManagerInternal.addWindowToken(mCurToken,
WindowManager.LayoutParams.TYPE_INPUT_METHOD,
displayIdToShowIme, null /* options */);
}
@GuardedBy("ImfLock.class")
private void unbindMainConnection() {
mContext.unbindService(mMainConnection);
mHasMainConnection = false;
}
@GuardedBy("ImfLock.class")
void unbindVisibleConnection() {
mContext.unbindService(mVisibleConnection);
mVisibleBound = false;
}
@GuardedBy("ImfLock.class")
private boolean bindCurrentInputMethodService(ServiceConnection conn, int flags) {
if (mCurIntent == null || conn == null) {
Slog.e(TAG, "--- bind failed: service = " + mCurIntent + ", conn = " + conn);
return false;
}
return mContext.bindServiceAsUser(mCurIntent, conn, flags,
new UserHandle(mService.getCurrentImeUserIdLocked()));
}
@GuardedBy("ImfLock.class")
private boolean bindCurrentInputMethodServiceMainConnection() {
mHasMainConnection = bindCurrentInputMethodService(mMainConnection,
mImeConnectionBindFlags);
return mHasMainConnection;
}
/**
* Bind the IME so that it can be shown.
*
* <p>
* Performs a rebind if no binding is achieved in {@link #TIME_TO_RECONNECT} milliseconds.
*/
@GuardedBy("ImfLock.class")
void setCurrentMethodVisible() {
if (mCurMethod != null) {
if (DEBUG) Slog.d(TAG, "setCurrentMethodVisible: mCurToken=" + mCurToken);
if (hasMainConnection() && !isVisibleBound()) {
mVisibleBound = bindCurrentInputMethodService(mVisibleConnection,
IME_VISIBLE_BIND_FLAGS);
}
return;
}
// No IME is currently connected. Reestablish the main connection.
if (!hasMainConnection()) {
if (DEBUG) {
Slog.d(TAG, "Cannot show input: no IME bound. Rebinding.");
}
bindCurrentMethod();
return;
}
long bindingDuration = SystemClock.uptimeMillis() - mLastBindTime;
if (bindingDuration >= TIME_TO_RECONNECT) {
// The client has asked to have the input method shown, but
// we have been sitting here too long with a connection to the
// service and no interface received, so let's disconnect/connect
// to try to prod things along.
EventLog.writeEvent(EventLogTags.IMF_FORCE_RECONNECT_IME, getSelectedMethodId(),
bindingDuration, 1);
Slog.w(TAG, "Force disconnect/connect to the IME in setCurrentMethodVisible()");
unbindMainConnection();
bindCurrentInputMethodServiceMainConnection();
} else {
if (DEBUG) {
Slog.d(TAG, "Can't show input: connection = " + mHasMainConnection + ", time = "
+ (TIME_TO_RECONNECT - bindingDuration));
}
}
}
/**
* Remove the binding needed for the IME to be shown.
*/
@GuardedBy("ImfLock.class")
void setCurrentMethodNotVisible() {
if (isVisibleBound()) {
unbindVisibleConnection();
}
}
}