Introduce InputMethodMenuController

... for preparation to migrate the input method switch dialog to
window context and easier for testing.

Test: existing tests pass
Bug: 157888351
Change-Id: Ief3c691029b14d848f6ad4a5ec8a8743760c7cda
This commit is contained in:
Charles Chen
2020-09-28 11:13:42 +08:00
parent d9f98db1ac
commit c88452c3ee
2 changed files with 356 additions and 280 deletions

View File

@@ -37,8 +37,6 @@ import android.annotation.RequiresPermission;
import android.annotation.UserIdInt;
import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
import android.app.ActivityThread;
import android.app.AlertDialog;
import android.app.AppGlobals;
import android.app.AppOpsManager;
import android.app.KeyguardManager;
@@ -50,9 +48,6 @@ import android.content.ComponentName;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnCancelListener;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
@@ -64,10 +59,8 @@ import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.database.ContentObserver;
import android.graphics.Matrix;
import android.graphics.drawable.Drawable;
import android.hardware.display.DisplayManagerInternal;
import android.hardware.input.InputManagerInternal;
import android.inputmethodservice.InputMethodService;
@@ -108,14 +101,10 @@ import android.util.Slog;
import android.util.SparseArray;
import android.util.imetracing.ImeTracing;
import android.util.proto.ProtoOutputStream;
import android.view.ContextThemeWrapper;
import android.view.DisplayInfo;
import android.view.IWindowManager;
import android.view.InputChannel;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager.LayoutParams;
import android.view.WindowManager.LayoutParams.SoftInputModeFlags;
import android.view.autofill.AutofillId;
@@ -129,12 +118,6 @@ import android.view.inputmethod.InputMethod;
import android.view.inputmethod.InputMethodInfo;
import android.view.inputmethod.InputMethodManager;
import android.view.inputmethod.InputMethodSubtype;
import android.widget.ArrayAdapter;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.RadioButton;
import android.widget.Switch;
import android.widget.TextView;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.content.PackageMonitor;
@@ -165,7 +148,6 @@ import com.android.internal.view.InputBindResult;
import com.android.server.EventLogTags;
import com.android.server.LocalServices;
import com.android.server.SystemService;
import com.android.server.SystemService.TargetUser;
import com.android.server.inputmethod.InputMethodManagerInternal.InputMethodListListener;
import com.android.server.inputmethod.InputMethodSubtypeSwitchingController.ImeSubtypeListItem;
import com.android.server.inputmethod.InputMethodUtils.InputMethodSettings;
@@ -330,10 +312,10 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
private final ArrayMap<String, List<InputMethodSubtype>> mAdditionalSubtypeMap =
new ArrayMap<>();
private final boolean mIsLowRam;
private final HardKeyboardListener mHardKeyboardListener;
private final AppOpsManager mAppOpsManager;
private final UserManager mUserManager;
private final UserManagerInternal mUserManagerInternal;
private final InputMethodMenuController mMenuController;
/**
* Cache the result of {@code LocalServices.getService(AudioManagerInternal.class)}.
@@ -351,7 +333,7 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
final ArrayMap<String, InputMethodInfo> mMethodMap = new ArrayMap<>();
private final LruCache<SuggestionSpan, InputMethodInfo> mSecureSuggestionSpans =
new LruCache<>(SECURE_SUGGESTION_SPANS_MAX_SIZE);
private final InputMethodSubtypeSwitchingController mSwitchingController;
final InputMethodSubtypeSwitchingController mSwitchingController;
/**
* Tracks how many times {@link #mMethodMap} was updated.
@@ -380,7 +362,7 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
// Ongoing notification
private NotificationManager mNotificationManager;
private KeyguardManager mKeyguardManager;
KeyguardManager mKeyguardManager;
private @Nullable StatusBarManagerService mStatusBar;
private Notification.Builder mImeSwitcherNotification;
private PendingIntent mImeSwitchPendingIntent;
@@ -724,21 +706,14 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
*/
int mImeWindowVis;
private AlertDialog.Builder mDialogBuilder;
private AlertDialog mSwitchingDialog;
private IBinder mSwitchingDialogToken = new Binder();
private View mSwitchingDialogTitleView;
private InputMethodInfo[] mIms;
private int[] mSubtypeIds;
private LocaleList mLastSystemLocales;
private boolean mShowImeWithHardKeyboard;
private boolean mAccessibilityRequestingNoSoftKeyboard;
private final MyPackageMonitor mMyPackageMonitor = new MyPackageMonitor();
private final IPackageManager mIPackageManager;
private final String mSlotIme;
/**
* Registered {@link InputMethodListListeners}.
* Registered {@link InputMethodListListener}.
* This variable can be accessed from both of MainThread and BinderThread.
*/
private final CopyOnWriteArrayList<InputMethodListListener> mInputMethodListListeners =
@@ -1138,7 +1113,7 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
Settings.Secure.ACCESSIBILITY_SOFT_KEYBOARD_MODE);
synchronized (mMethodMap) {
if (showImeUri.equals(uri)) {
updateKeyboardFromSettingsLocked();
mMenuController.updateKeyboardFromSettingsLocked();
} else if (accessibilityRequestingNoImeUri.equals(uri)) {
final int accessibilitySoftKeyboardSetting = Settings.Secure.getIntForUser(
mContext.getContentResolver(),
@@ -1226,7 +1201,7 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
return;
}
}
hideInputMethodMenu();
mMenuController.hideInputMethodMenu();
} else {
Slog.w(TAG, "Unexpected intent " + intent);
}
@@ -1526,29 +1501,6 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
}
}
private class HardKeyboardListener
implements WindowManagerInternal.OnHardKeyboardStatusChangeListener {
@Override
public void onHardKeyboardStatusChange(boolean available) {
mHandler.sendMessage(mHandler.obtainMessage(MSG_HARD_KEYBOARD_SWITCH_CHANGED,
available ? 1 : 0));
}
public void handleHardKeyboardStatusChange(boolean available) {
if (DEBUG) {
Slog.w(TAG, "HardKeyboardStatusChanged: available=" + available);
}
synchronized(mMethodMap) {
if (mSwitchingDialog != null && mSwitchingDialogTitleView != null
&& mSwitchingDialog.isShowing()) {
mSwitchingDialogTitleView.findViewById(
com.android.internal.R.id.hard_keyboard_section).setVisibility(
available ? View.VISIBLE : View.GONE);
}
}
}
}
private static final class UserSwitchHandlerTask implements Runnable {
final InputMethodManagerService mService;
@@ -1687,7 +1639,6 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
mAppOpsManager = mContext.getSystemService(AppOpsManager.class);
mUserManager = mContext.getSystemService(UserManager.class);
mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
mHardKeyboardListener = new HardKeyboardListener();
mHasFeature = context.getPackageManager().hasSystemFeature(
PackageManager.FEATURE_INPUT_METHODS);
mSlotIme = mContext.getString(com.android.internal.R.string.status_bar_ime);
@@ -1731,6 +1682,7 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
AdditionalSubtypeUtils.load(mAdditionalSubtypeMap, userId);
mSwitchingController = InputMethodSubtypeSwitchingController.createInstanceLocked(
mSettings, context);
mMenuController = new InputMethodMenuController(this);
}
private void resetDefaultImeLocked(Context context) {
@@ -1861,8 +1813,10 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
mShowOngoingImeSwitcherForPhones = mRes.getBoolean(
com.android.internal.R.bool.show_ongoing_ime_switcher);
if (mShowOngoingImeSwitcherForPhones) {
final InputMethodMenuController.HardKeyboardListener hardKeyboardListener =
mMenuController.getHardKeyboardListener();
mWindowManagerInternal.setOnHardKeyboardStatusChangeListener(
mHardKeyboardListener);
hardKeyboardListener);
}
mMyPackageMonitor.register(mContext, null, UserHandle.ALL, true);
@@ -2331,7 +2285,7 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
mCurClient = null;
mCurActivityViewToScreenMatrix = null;
hideInputMethodMenuLocked();
mMenuController.hideInputMethodMenuLocked();
}
}
@@ -2813,7 +2767,7 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
private boolean shouldShowImeSwitcherLocked(int visibility) {
if (!mShowOngoingImeSwitcherForPhones) return false;
if (mSwitchingDialog != null) return false;
if (mMenuController.getSwitchingDialogLocked() != null) return false;
if (mWindowManagerInternal.isKeyguardShowingAndNotOccluded()
&& mKeyguardManager != null && mKeyguardManager.isKeyguardSecure()) return false;
if ((visibility & InputMethodService.IME_ACTIVE) == 0
@@ -2930,6 +2884,10 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
}
}
void updateSystemUiLocked() {
updateSystemUiLocked(mImeWindowVis, mBackDisposition);
}
// Caution! This method is called in this class. Handle multi-user carefully
private void updateSystemUiLocked(int vis, int backDisposition) {
if (mCurToken == null) {
@@ -3000,7 +2958,7 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
void updateFromSettingsLocked(boolean enabledMayChange) {
updateInputMethodsFromSettingsLocked(enabledMayChange);
updateKeyboardFromSettingsLocked();
mMenuController.updateKeyboardFromSettingsLocked();
}
void updateInputMethodsFromSettingsLocked(boolean enabledMayChange) {
@@ -3057,17 +3015,6 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
}
public void updateKeyboardFromSettingsLocked() {
mShowImeWithHardKeyboard = mSettings.isShowImeWithHardKeyboardEnabled();
if (mSwitchingDialog != null
&& mSwitchingDialogTitleView != null
&& mSwitchingDialog.isShowing()) {
final Switch hardKeySwitch = (Switch)mSwitchingDialogTitleView.findViewById(
com.android.internal.R.id.hard_keyboard_switch);
hardKeySwitch.setChecked(mShowImeWithHardKeyboard);
}
}
/* package */ void setInputMethodLocked(String id, int subtypeId) {
InputMethodInfo info = mMethodMap.get(id);
if (info == null) {
@@ -3697,10 +3644,7 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
public boolean isInputMethodPickerShownForTest() {
synchronized(mMethodMap) {
if (mSwitchingDialog == null) {
return false;
}
return mSwitchingDialog.isShowing();
return mMenuController.isisInputMethodPickerShownForTestLocked();
}
}
@@ -3935,7 +3879,7 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
* {@link InputMethodService#onCreate()}.
*
* <p>TODO(Bug 113914148): Check if we can remove this.</p>
* @return {@link WindowManagerInternal#getInputMethodWindowVisibleHeight()}
* @return {@link WindowManagerInternal#getInputMethodWindowVisibleHeight(int)}
*/
@Override
public int getInputMethodWindowVisibleHeight() {
@@ -4247,7 +4191,7 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
Slog.e(TAG, "Unknown subtype picker mode = " + msg.arg1);
return false;
}
showInputMethodMenu(showAuxSubtypes, displayId);
mMenuController.showInputMethodMenu(showAuxSubtypes, displayId);
return true;
case MSG_SHOW_IM_SUBTYPE_ENABLER:
@@ -4484,7 +4428,9 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
// --------------------------------------------------------------
case MSG_HARD_KEYBOARD_SWITCH_CHANGED:
mHardKeyboardListener.handleHardKeyboardStatusChange(msg.arg1 == 1);
final InputMethodMenuController.HardKeyboardListener hardKeyboardListener =
mMenuController.getHardKeyboardListener();
hardKeyboardListener.handleHardKeyboardStatusChange(msg.arg1 == 1);
return true;
case MSG_SYSTEM_UNLOCK_USER: {
final int userId = msg.arg1;
@@ -4735,208 +4681,6 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
mContext.startActivityAsUser(intent, null, UserHandle.CURRENT);
}
private boolean isScreenLocked() {
return mKeyguardManager != null
&& mKeyguardManager.isKeyguardLocked() && mKeyguardManager.isKeyguardSecure();
}
private void showInputMethodMenu(boolean showAuxSubtypes, int displayId) {
if (DEBUG) Slog.v(TAG, "Show switching menu. showAuxSubtypes=" + showAuxSubtypes);
final boolean isScreenLocked = isScreenLocked();
final String lastInputMethodId = mSettings.getSelectedInputMethod();
int lastInputMethodSubtypeId = mSettings.getSelectedInputMethodSubtypeId(lastInputMethodId);
if (DEBUG) Slog.v(TAG, "Current IME: " + lastInputMethodId);
synchronized (mMethodMap) {
final List<ImeSubtypeListItem> imList =
mSwitchingController.getSortedInputMethodAndSubtypeListLocked(
showAuxSubtypes, isScreenLocked);
if (imList.isEmpty()) {
return;
}
hideInputMethodMenuLocked();
if (lastInputMethodSubtypeId == NOT_A_SUBTYPE_ID) {
final InputMethodSubtype currentSubtype = getCurrentInputMethodSubtypeLocked();
if (currentSubtype != null) {
final InputMethodInfo currentImi = mMethodMap.get(mCurMethodId);
lastInputMethodSubtypeId = InputMethodUtils.getSubtypeIdFromHashCode(
currentImi, currentSubtype.hashCode());
}
}
final int N = imList.size();
mIms = new InputMethodInfo[N];
mSubtypeIds = new int[N];
int checkedItem = 0;
for (int i = 0; i < N; ++i) {
final ImeSubtypeListItem item = imList.get(i);
mIms[i] = item.mImi;
mSubtypeIds[i] = item.mSubtypeId;
if (mIms[i].getId().equals(lastInputMethodId)) {
int subtypeId = mSubtypeIds[i];
if ((subtypeId == NOT_A_SUBTYPE_ID)
|| (lastInputMethodSubtypeId == NOT_A_SUBTYPE_ID && subtypeId == 0)
|| (subtypeId == lastInputMethodSubtypeId)) {
checkedItem = i;
}
}
}
final ActivityThread currentThread = ActivityThread.currentActivityThread();
final Context settingsContext = new ContextThemeWrapper(
displayId == DEFAULT_DISPLAY ? currentThread.getSystemUiContext()
: currentThread.createSystemUiContext(displayId),
com.android.internal.R.style.Theme_DeviceDefault_Settings);
mDialogBuilder = new AlertDialog.Builder(settingsContext);
mDialogBuilder.setOnCancelListener(new OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
hideInputMethodMenu();
}
});
final Context dialogContext = mDialogBuilder.getContext();
final TypedArray a = dialogContext.obtainStyledAttributes(null,
com.android.internal.R.styleable.DialogPreference,
com.android.internal.R.attr.alertDialogStyle, 0);
final Drawable dialogIcon = a.getDrawable(
com.android.internal.R.styleable.DialogPreference_dialogIcon);
a.recycle();
mDialogBuilder.setIcon(dialogIcon);
final LayoutInflater inflater = dialogContext.getSystemService(LayoutInflater.class);
final View tv = inflater.inflate(
com.android.internal.R.layout.input_method_switch_dialog_title, null);
mDialogBuilder.setCustomTitle(tv);
// Setup layout for a toggle switch of the hardware keyboard
mSwitchingDialogTitleView = tv;
mSwitchingDialogTitleView
.findViewById(com.android.internal.R.id.hard_keyboard_section)
.setVisibility(mWindowManagerInternal.isHardKeyboardAvailable()
? View.VISIBLE : View.GONE);
final Switch hardKeySwitch = (Switch) mSwitchingDialogTitleView.findViewById(
com.android.internal.R.id.hard_keyboard_switch);
hardKeySwitch.setChecked(mShowImeWithHardKeyboard);
hardKeySwitch.setOnCheckedChangeListener(new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
mSettings.setShowImeWithHardKeyboard(isChecked);
// Ensure that the input method dialog is dismissed when changing
// the hardware keyboard state.
hideInputMethodMenu();
}
});
final ImeSubtypeListAdapter adapter = new ImeSubtypeListAdapter(dialogContext,
com.android.internal.R.layout.input_method_switch_item, imList, checkedItem);
final OnClickListener choiceListener = new OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int which) {
synchronized (mMethodMap) {
if (mIms == null || mIms.length <= which || mSubtypeIds == null
|| mSubtypeIds.length <= which) {
return;
}
final InputMethodInfo im = mIms[which];
int subtypeId = mSubtypeIds[which];
adapter.mCheckedItem = which;
adapter.notifyDataSetChanged();
hideInputMethodMenu();
if (im != null) {
if (subtypeId < 0 || subtypeId >= im.getSubtypeCount()) {
subtypeId = NOT_A_SUBTYPE_ID;
}
setInputMethodLocked(im.getId(), subtypeId);
}
}
}
};
mDialogBuilder.setSingleChoiceItems(adapter, checkedItem, choiceListener);
mSwitchingDialog = mDialogBuilder.create();
mSwitchingDialog.setCanceledOnTouchOutside(true);
final Window w = mSwitchingDialog.getWindow();
final LayoutParams attrs = w.getAttributes();
w.setType(LayoutParams.TYPE_INPUT_METHOD_DIALOG);
// Use an alternate token for the dialog for that window manager can group the token
// with other IME windows based on type vs. grouping based on whichever token happens
// to get selected by the system later on.
attrs.token = mSwitchingDialogToken;
attrs.privateFlags |= LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
attrs.setTitle("Select input method");
w.setAttributes(attrs);
updateSystemUiLocked(mImeWindowVis, mBackDisposition);
mSwitchingDialog.show();
}
}
private static class ImeSubtypeListAdapter extends ArrayAdapter<ImeSubtypeListItem> {
private final LayoutInflater mInflater;
private final int mTextViewResourceId;
private final List<ImeSubtypeListItem> mItemsList;
public int mCheckedItem;
public ImeSubtypeListAdapter(Context context, int textViewResourceId,
List<ImeSubtypeListItem> itemsList, int checkedItem) {
super(context, textViewResourceId, itemsList);
mTextViewResourceId = textViewResourceId;
mItemsList = itemsList;
mCheckedItem = checkedItem;
mInflater = context.getSystemService(LayoutInflater.class);
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
final View view = convertView != null ? convertView
: mInflater.inflate(mTextViewResourceId, null);
if (position < 0 || position >= mItemsList.size()) return view;
final ImeSubtypeListItem item = mItemsList.get(position);
final CharSequence imeName = item.mImeName;
final CharSequence subtypeName = item.mSubtypeName;
final TextView firstTextView = (TextView)view.findViewById(android.R.id.text1);
final TextView secondTextView = (TextView)view.findViewById(android.R.id.text2);
if (TextUtils.isEmpty(subtypeName)) {
firstTextView.setText(imeName);
secondTextView.setVisibility(View.GONE);
} else {
firstTextView.setText(subtypeName);
secondTextView.setText(imeName);
secondTextView.setVisibility(View.VISIBLE);
}
final RadioButton radioButton =
(RadioButton)view.findViewById(com.android.internal.R.id.radio);
radioButton.setChecked(position == mCheckedItem);
return view;
}
}
void hideInputMethodMenu() {
synchronized (mMethodMap) {
hideInputMethodMenuLocked();
}
}
void hideInputMethodMenuLocked() {
if (DEBUG) Slog.v(TAG, "Hide switching menu");
if (mSwitchingDialog != null) {
mSwitchingDialog.dismiss();
mSwitchingDialog = null;
mSwitchingDialogTitleView = null;
}
updateSystemUiLocked(mImeWindowVis, mBackDisposition);
mDialogBuilder = null;
mIms = null;
}
// ----------------------------------------------------------------------
/**
@@ -5040,7 +4784,7 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
}
}
private InputMethodSubtype getCurrentInputMethodSubtypeLocked() {
InputMethodSubtype getCurrentInputMethodSubtypeLocked() {
if (mCurMethodId == null) {
return null;
}
@@ -5079,6 +4823,11 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
return mCurrentSubtype;
}
@Nullable
String getCurrentMethodId() {
return mCurMethodId;
}
private List<InputMethodInfo> getInputMethodListAsUser(@UserIdInt int userId) {
synchronized (mMethodMap) {
return getInputMethodListLocked(userId);

View File

@@ -0,0 +1,327 @@
/*
* Copyright (C) 2020 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 com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
import static com.android.server.inputmethod.InputMethodManagerService.DEBUG;
import static com.android.server.inputmethod.InputMethodManagerService.MSG_HARD_KEYBOARD_SWITCH_CHANGED;
import static com.android.server.inputmethod.InputMethodUtils.NOT_A_SUBTYPE_ID;
import android.app.ActivityThread;
import android.app.AlertDialog;
import android.app.KeyguardManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.IBinder;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Slog;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodInfo;
import android.view.inputmethod.InputMethodSubtype;
import android.widget.ArrayAdapter;
import android.widget.RadioButton;
import android.widget.Switch;
import android.widget.TextView;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.LocalServices;
import com.android.server.inputmethod.InputMethodSubtypeSwitchingController.ImeSubtypeListItem;
import com.android.server.wm.WindowManagerInternal;
import java.util.List;
/** A controller to show/hide the input method menu */
@VisibleForTesting(visibility = PACKAGE)
public class InputMethodMenuController {
private static final String TAG = InputMethodMenuController.class.getSimpleName();
private final InputMethodManagerService mService;
private final InputMethodUtils.InputMethodSettings mSettings;
private final InputMethodSubtypeSwitchingController mSwitchingController;
private final Handler mHandler;
private final ArrayMap<String, InputMethodInfo> mMethodMap;
private final KeyguardManager mKeyguardManager;
private final HardKeyboardListener mHardKeyboardListener;
private final WindowManagerInternal mWindowManagerInternal;
private Context mSettingsContext;
private AlertDialog.Builder mDialogBuilder;
private AlertDialog mSwitchingDialog;
private IBinder mSwitchingDialogToken;
private View mSwitchingDialogTitleView;
private InputMethodInfo[] mIms;
private int[] mSubtypeIds;
private boolean mShowImeWithHardKeyboard;
@VisibleForTesting(visibility = PACKAGE)
public InputMethodMenuController(InputMethodManagerService service) {
mService = service;
mSettings = mService.mSettings;
mSwitchingController = mService.mSwitchingController;
mHandler = mService.mHandler;
mMethodMap = mService.mMethodMap;
mKeyguardManager = mService.mKeyguardManager;
mHardKeyboardListener = new HardKeyboardListener();
mWindowManagerInternal = LocalServices.getService(WindowManagerInternal.class);
}
void showInputMethodMenu(boolean showAuxSubtypes, int displayId) {
if (DEBUG) Slog.v(TAG, "Show switching menu. showAuxSubtypes=" + showAuxSubtypes);
final boolean isScreenLocked = isScreenLocked();
final String lastInputMethodId = mSettings.getSelectedInputMethod();
int lastInputMethodSubtypeId = mSettings.getSelectedInputMethodSubtypeId(lastInputMethodId);
if (DEBUG) Slog.v(TAG, "Current IME: " + lastInputMethodId);
synchronized (mMethodMap) {
final List<ImeSubtypeListItem> imList = mSwitchingController
.getSortedInputMethodAndSubtypeListLocked(showAuxSubtypes, isScreenLocked);
if (imList.isEmpty()) {
return;
}
hideInputMethodMenuLocked();
if (lastInputMethodSubtypeId == NOT_A_SUBTYPE_ID) {
final InputMethodSubtype currentSubtype =
mService.getCurrentInputMethodSubtypeLocked();
if (currentSubtype != null) {
final String curMethodId = mService.getCurrentMethodId();
final InputMethodInfo currentImi = mMethodMap.get(curMethodId);
lastInputMethodSubtypeId = InputMethodUtils.getSubtypeIdFromHashCode(
currentImi, currentSubtype.hashCode());
}
}
final int size = imList.size();
mIms = new InputMethodInfo[size];
mSubtypeIds = new int[size];
int checkedItem = 0;
for (int i = 0; i < size; ++i) {
final ImeSubtypeListItem item = imList.get(i);
mIms[i] = item.mImi;
mSubtypeIds[i] = item.mSubtypeId;
if (mIms[i].getId().equals(lastInputMethodId)) {
int subtypeId = mSubtypeIds[i];
if ((subtypeId == NOT_A_SUBTYPE_ID)
|| (lastInputMethodSubtypeId == NOT_A_SUBTYPE_ID && subtypeId == 0)
|| (subtypeId == lastInputMethodSubtypeId)) {
checkedItem = i;
}
}
}
final ActivityThread currentThread = ActivityThread.currentActivityThread();
final Context settingsContext = new ContextThemeWrapper(
currentThread.createSystemUiContext(displayId),
com.android.internal.R.style.Theme_DeviceDefault_Settings);
mDialogBuilder = new AlertDialog.Builder(settingsContext);
mDialogBuilder.setOnCancelListener(dialog -> hideInputMethodMenu());
final Context dialogContext = mDialogBuilder.getContext();
final TypedArray a = dialogContext.obtainStyledAttributes(null,
com.android.internal.R.styleable.DialogPreference,
com.android.internal.R.attr.alertDialogStyle, 0);
final Drawable dialogIcon = a.getDrawable(
com.android.internal.R.styleable.DialogPreference_dialogIcon);
a.recycle();
mDialogBuilder.setIcon(dialogIcon);
final LayoutInflater inflater = dialogContext.getSystemService(LayoutInflater.class);
final View tv = inflater.inflate(
com.android.internal.R.layout.input_method_switch_dialog_title, null);
mDialogBuilder.setCustomTitle(tv);
// Setup layout for a toggle switch of the hardware keyboard
mSwitchingDialogTitleView = tv;
mSwitchingDialogTitleView
.findViewById(com.android.internal.R.id.hard_keyboard_section)
.setVisibility(mWindowManagerInternal.isHardKeyboardAvailable()
? View.VISIBLE : View.GONE);
final Switch hardKeySwitch = mSwitchingDialogTitleView.findViewById(
com.android.internal.R.id.hard_keyboard_switch);
hardKeySwitch.setChecked(mShowImeWithHardKeyboard);
hardKeySwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
mSettings.setShowImeWithHardKeyboard(isChecked);
// Ensure that the input method dialog is dismissed when changing
// the hardware keyboard state.
hideInputMethodMenu();
});
final ImeSubtypeListAdapter adapter = new ImeSubtypeListAdapter(dialogContext,
com.android.internal.R.layout.input_method_switch_item, imList, checkedItem);
final DialogInterface.OnClickListener choiceListener = (dialog, which) -> {
synchronized (mMethodMap) {
if (mIms == null || mIms.length <= which || mSubtypeIds == null
|| mSubtypeIds.length <= which) {
return;
}
final InputMethodInfo im = mIms[which];
int subtypeId = mSubtypeIds[which];
adapter.mCheckedItem = which;
adapter.notifyDataSetChanged();
hideInputMethodMenu();
if (im != null) {
if (subtypeId < 0 || subtypeId >= im.getSubtypeCount()) {
subtypeId = NOT_A_SUBTYPE_ID;
}
mService.setInputMethodLocked(im.getId(), subtypeId);
}
}
};
mDialogBuilder.setSingleChoiceItems(adapter, checkedItem, choiceListener);
mSwitchingDialog = mDialogBuilder.create();
mSwitchingDialog.setCanceledOnTouchOutside(true);
final Window w = mSwitchingDialog.getWindow();
final WindowManager.LayoutParams attrs = w.getAttributes();
w.setType(WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG);
// Use an alternate token for the dialog for that window manager can group the token
// with other IME windows based on type vs. grouping based on whichever token happens
// to get selected by the system later on.
attrs.token = mSwitchingDialogToken;
attrs.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
attrs.setTitle("Select input method");
w.setAttributes(attrs);
mService.updateSystemUiLocked();
mSwitchingDialog.show();
}
}
private boolean isScreenLocked() {
return mKeyguardManager != null && mKeyguardManager.isKeyguardLocked()
&& mKeyguardManager.isKeyguardSecure();
}
void updateKeyboardFromSettingsLocked() {
mShowImeWithHardKeyboard = mSettings.isShowImeWithHardKeyboardEnabled();
if (mSwitchingDialog != null && mSwitchingDialogTitleView != null
&& mSwitchingDialog.isShowing()) {
final Switch hardKeySwitch = mSwitchingDialogTitleView.findViewById(
com.android.internal.R.id.hard_keyboard_switch);
hardKeySwitch.setChecked(mShowImeWithHardKeyboard);
}
}
void hideInputMethodMenu() {
synchronized (mMethodMap) {
hideInputMethodMenuLocked();
}
}
void hideInputMethodMenuLocked() {
if (DEBUG) Slog.v(TAG, "Hide switching menu");
if (mSwitchingDialog != null) {
mSwitchingDialog.dismiss();
mSwitchingDialog = null;
mSwitchingDialogTitleView = null;
}
mService.updateSystemUiLocked();
mDialogBuilder = null;
mIms = null;
}
HardKeyboardListener getHardKeyboardListener() {
return mHardKeyboardListener;
}
AlertDialog getSwitchingDialogLocked() {
return mSwitchingDialog;
}
boolean isisInputMethodPickerShownForTestLocked() {
if (mSwitchingDialog == null) {
return false;
}
return mSwitchingDialog.isShowing();
}
class HardKeyboardListener implements WindowManagerInternal.OnHardKeyboardStatusChangeListener {
@Override
public void onHardKeyboardStatusChange(boolean available) {
mHandler.sendMessage(mHandler.obtainMessage(MSG_HARD_KEYBOARD_SWITCH_CHANGED,
available ? 1 : 0));
}
public void handleHardKeyboardStatusChange(boolean available) {
if (DEBUG) {
Slog.w(TAG, "HardKeyboardStatusChanged: available=" + available);
}
synchronized (mMethodMap) {
if (mSwitchingDialog != null && mSwitchingDialogTitleView != null
&& mSwitchingDialog.isShowing()) {
mSwitchingDialogTitleView.findViewById(
com.android.internal.R.id.hard_keyboard_section).setVisibility(
available ? View.VISIBLE : View.GONE);
}
}
}
}
private static class ImeSubtypeListAdapter extends ArrayAdapter<ImeSubtypeListItem> {
private final LayoutInflater mInflater;
private final int mTextViewResourceId;
private final List<ImeSubtypeListItem> mItemsList;
public int mCheckedItem;
private ImeSubtypeListAdapter(Context context, int textViewResourceId,
List<ImeSubtypeListItem> itemsList, int checkedItem) {
super(context, textViewResourceId, itemsList);
mTextViewResourceId = textViewResourceId;
mItemsList = itemsList;
mCheckedItem = checkedItem;
mInflater = LayoutInflater.from(context);
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
final View view = convertView != null ? convertView
: mInflater.inflate(mTextViewResourceId, null);
if (position < 0 || position >= mItemsList.size()) return view;
final ImeSubtypeListItem item = mItemsList.get(position);
final CharSequence imeName = item.mImeName;
final CharSequence subtypeName = item.mSubtypeName;
final TextView firstTextView = view.findViewById(android.R.id.text1);
final TextView secondTextView = view.findViewById(android.R.id.text2);
if (TextUtils.isEmpty(subtypeName)) {
firstTextView.setText(imeName);
secondTextView.setVisibility(View.GONE);
} else {
firstTextView.setText(subtypeName);
secondTextView.setText(imeName);
secondTextView.setVisibility(View.VISIBLE);
}
final RadioButton radioButton = view.findViewById(com.android.internal.R.id.radio);
radioButton.setChecked(position == mCheckedItem);
return view;
}
}
}