diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 2eccaf1434b4c..4e735e228c12b 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -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> 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 mMethodMap = new ArrayMap<>(); private final LruCache 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 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()}. * *

TODO(Bug 113914148): Check if we can remove this.

- * @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 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 { - private final LayoutInflater mInflater; - private final int mTextViewResourceId; - private final List mItemsList; - public int mCheckedItem; - public ImeSubtypeListAdapter(Context context, int textViewResourceId, - List 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 getInputMethodListAsUser(@UserIdInt int userId) { synchronized (mMethodMap) { return getInputMethodListLocked(userId); diff --git a/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java b/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java new file mode 100644 index 0000000000000..6fb887409ac54 --- /dev/null +++ b/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java @@ -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 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 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 { + private final LayoutInflater mInflater; + private final int mTextViewResourceId; + private final List mItemsList; + public int mCheckedItem; + private ImeSubtypeListAdapter(Context context, int textViewResourceId, + List 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; + } + } +}