1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.inputmethod; 18 19 import static com.android.server.inputmethod.InputMethodManagerService.DEBUG; 20 import static com.android.server.inputmethod.InputMethodUtils.NOT_A_SUBTYPE_ID; 21 22 import android.annotation.Nullable; 23 import android.app.AlertDialog; 24 import android.app.KeyguardManager; 25 import android.content.Context; 26 import android.content.DialogInterface; 27 import android.content.res.TypedArray; 28 import android.graphics.drawable.Drawable; 29 import android.text.TextUtils; 30 import android.util.ArrayMap; 31 import android.util.Slog; 32 import android.view.LayoutInflater; 33 import android.view.View; 34 import android.view.ViewGroup; 35 import android.view.Window; 36 import android.view.WindowManager; 37 import android.view.inputmethod.InputMethodInfo; 38 import android.view.inputmethod.InputMethodSubtype; 39 import android.widget.ArrayAdapter; 40 import android.widget.RadioButton; 41 import android.widget.Switch; 42 import android.widget.TextView; 43 44 import com.android.internal.annotations.GuardedBy; 45 import com.android.server.LocalServices; 46 import com.android.server.inputmethod.InputMethodSubtypeSwitchingController.ImeSubtypeListItem; 47 import com.android.server.wm.WindowManagerInternal; 48 49 import java.util.List; 50 51 /** A controller to show/hide the input method menu */ 52 final class InputMethodMenuController { 53 private static final String TAG = InputMethodMenuController.class.getSimpleName(); 54 55 private final InputMethodManagerService mService; 56 private final InputMethodUtils.InputMethodSettings mSettings; 57 private final InputMethodSubtypeSwitchingController mSwitchingController; 58 private final ArrayMap<String, InputMethodInfo> mMethodMap; 59 private final KeyguardManager mKeyguardManager; 60 private final WindowManagerInternal mWindowManagerInternal; 61 62 private AlertDialog.Builder mDialogBuilder; 63 private AlertDialog mSwitchingDialog; 64 private View mSwitchingDialogTitleView; 65 private InputMethodInfo[] mIms; 66 private int[] mSubtypeIds; 67 68 private boolean mShowImeWithHardKeyboard; 69 70 @GuardedBy("ImfLock.class") 71 @Nullable 72 private InputMethodDialogWindowContext mDialogWindowContext; 73 InputMethodMenuController(InputMethodManagerService service)74 public InputMethodMenuController(InputMethodManagerService service) { 75 mService = service; 76 mSettings = mService.mSettings; 77 mSwitchingController = mService.mSwitchingController; 78 mMethodMap = mService.mMethodMap; 79 mKeyguardManager = mService.mKeyguardManager; 80 mWindowManagerInternal = LocalServices.getService(WindowManagerInternal.class); 81 } 82 showInputMethodMenu(boolean showAuxSubtypes, int displayId)83 void showInputMethodMenu(boolean showAuxSubtypes, int displayId) { 84 if (DEBUG) Slog.v(TAG, "Show switching menu. showAuxSubtypes=" + showAuxSubtypes); 85 86 final boolean isScreenLocked = isScreenLocked(); 87 88 final String lastInputMethodId = mSettings.getSelectedInputMethod(); 89 int lastInputMethodSubtypeId = mSettings.getSelectedInputMethodSubtypeId(lastInputMethodId); 90 if (DEBUG) Slog.v(TAG, "Current IME: " + lastInputMethodId); 91 92 synchronized (ImfLock.class) { 93 final List<ImeSubtypeListItem> imList = mSwitchingController 94 .getSortedInputMethodAndSubtypeListForImeMenuLocked( 95 showAuxSubtypes, isScreenLocked); 96 if (imList.isEmpty()) { 97 return; 98 } 99 100 hideInputMethodMenuLocked(); 101 102 if (lastInputMethodSubtypeId == NOT_A_SUBTYPE_ID) { 103 final InputMethodSubtype currentSubtype = 104 mService.getCurrentInputMethodSubtypeLocked(); 105 if (currentSubtype != null) { 106 final String curMethodId = mService.getSelectedMethodIdLocked(); 107 final InputMethodInfo currentImi = mMethodMap.get(curMethodId); 108 lastInputMethodSubtypeId = InputMethodUtils.getSubtypeIdFromHashCode( 109 currentImi, currentSubtype.hashCode()); 110 } 111 } 112 113 final int size = imList.size(); 114 mIms = new InputMethodInfo[size]; 115 mSubtypeIds = new int[size]; 116 int checkedItem = 0; 117 for (int i = 0; i < size; ++i) { 118 final ImeSubtypeListItem item = imList.get(i); 119 mIms[i] = item.mImi; 120 mSubtypeIds[i] = item.mSubtypeId; 121 if (mIms[i].getId().equals(lastInputMethodId)) { 122 int subtypeId = mSubtypeIds[i]; 123 if ((subtypeId == NOT_A_SUBTYPE_ID) 124 || (lastInputMethodSubtypeId == NOT_A_SUBTYPE_ID && subtypeId == 0) 125 || (subtypeId == lastInputMethodSubtypeId)) { 126 checkedItem = i; 127 } 128 } 129 } 130 131 if (mDialogWindowContext == null) { 132 mDialogWindowContext = new InputMethodDialogWindowContext(); 133 } 134 final Context dialogWindowContext = mDialogWindowContext.get(displayId); 135 mDialogBuilder = new AlertDialog.Builder(dialogWindowContext); 136 mDialogBuilder.setOnCancelListener(dialog -> hideInputMethodMenu()); 137 138 final Context dialogContext = mDialogBuilder.getContext(); 139 final TypedArray a = dialogContext.obtainStyledAttributes(null, 140 com.android.internal.R.styleable.DialogPreference, 141 com.android.internal.R.attr.alertDialogStyle, 0); 142 final Drawable dialogIcon = a.getDrawable( 143 com.android.internal.R.styleable.DialogPreference_dialogIcon); 144 a.recycle(); 145 146 mDialogBuilder.setIcon(dialogIcon); 147 148 final LayoutInflater inflater = dialogContext.getSystemService(LayoutInflater.class); 149 final View tv = inflater.inflate( 150 com.android.internal.R.layout.input_method_switch_dialog_title, null); 151 mDialogBuilder.setCustomTitle(tv); 152 153 // Setup layout for a toggle switch of the hardware keyboard 154 mSwitchingDialogTitleView = tv; 155 mSwitchingDialogTitleView 156 .findViewById(com.android.internal.R.id.hard_keyboard_section) 157 .setVisibility(mWindowManagerInternal.isHardKeyboardAvailable() 158 ? View.VISIBLE : View.GONE); 159 final Switch hardKeySwitch = mSwitchingDialogTitleView.findViewById( 160 com.android.internal.R.id.hard_keyboard_switch); 161 hardKeySwitch.setChecked(mShowImeWithHardKeyboard); 162 hardKeySwitch.setOnCheckedChangeListener((buttonView, isChecked) -> { 163 mSettings.setShowImeWithHardKeyboard(isChecked); 164 // Ensure that the input method dialog is dismissed when changing 165 // the hardware keyboard state. 166 hideInputMethodMenu(); 167 }); 168 169 final ImeSubtypeListAdapter adapter = new ImeSubtypeListAdapter(dialogContext, 170 com.android.internal.R.layout.input_method_switch_item, imList, checkedItem); 171 final DialogInterface.OnClickListener choiceListener = (dialog, which) -> { 172 synchronized (ImfLock.class) { 173 if (mIms == null || mIms.length <= which || mSubtypeIds == null 174 || mSubtypeIds.length <= which) { 175 return; 176 } 177 final InputMethodInfo im = mIms[which]; 178 int subtypeId = mSubtypeIds[which]; 179 adapter.mCheckedItem = which; 180 adapter.notifyDataSetChanged(); 181 hideInputMethodMenu(); 182 if (im != null) { 183 if (subtypeId < 0 || subtypeId >= im.getSubtypeCount()) { 184 subtypeId = NOT_A_SUBTYPE_ID; 185 } 186 mService.setInputMethodLocked(im.getId(), subtypeId); 187 } 188 } 189 }; 190 mDialogBuilder.setSingleChoiceItems(adapter, checkedItem, choiceListener); 191 192 mSwitchingDialog = mDialogBuilder.create(); 193 mSwitchingDialog.setCanceledOnTouchOutside(true); 194 final Window w = mSwitchingDialog.getWindow(); 195 final WindowManager.LayoutParams attrs = w.getAttributes(); 196 w.setType(WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG); 197 w.setHideOverlayWindows(true); 198 // Use an alternate token for the dialog for that window manager can group the token 199 // with other IME windows based on type vs. grouping based on whichever token happens 200 // to get selected by the system later on. 201 attrs.token = dialogWindowContext.getWindowContextToken(); 202 attrs.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; 203 attrs.setTitle("Select input method"); 204 w.setAttributes(attrs); 205 mService.updateSystemUiLocked(); 206 mService.sendOnNavButtonFlagsChangedLocked(); 207 mSwitchingDialog.show(); 208 } 209 } 210 isScreenLocked()211 private boolean isScreenLocked() { 212 return mKeyguardManager != null && mKeyguardManager.isKeyguardLocked() 213 && mKeyguardManager.isKeyguardSecure(); 214 } 215 updateKeyboardFromSettingsLocked()216 void updateKeyboardFromSettingsLocked() { 217 mShowImeWithHardKeyboard = mSettings.isShowImeWithHardKeyboardEnabled(); 218 if (mSwitchingDialog != null && mSwitchingDialogTitleView != null 219 && mSwitchingDialog.isShowing()) { 220 final Switch hardKeySwitch = mSwitchingDialogTitleView.findViewById( 221 com.android.internal.R.id.hard_keyboard_switch); 222 hardKeySwitch.setChecked(mShowImeWithHardKeyboard); 223 } 224 } 225 hideInputMethodMenu()226 void hideInputMethodMenu() { 227 synchronized (ImfLock.class) { 228 hideInputMethodMenuLocked(); 229 } 230 } 231 232 @GuardedBy("ImfLock.class") hideInputMethodMenuLocked()233 void hideInputMethodMenuLocked() { 234 if (DEBUG) Slog.v(TAG, "Hide switching menu"); 235 236 if (mSwitchingDialog != null) { 237 mSwitchingDialog.dismiss(); 238 mSwitchingDialog = null; 239 mSwitchingDialogTitleView = null; 240 241 mService.updateSystemUiLocked(); 242 mService.sendOnNavButtonFlagsChangedLocked(); 243 mDialogBuilder = null; 244 mIms = null; 245 } 246 } 247 getSwitchingDialogLocked()248 AlertDialog getSwitchingDialogLocked() { 249 return mSwitchingDialog; 250 } 251 getShowImeWithHardKeyboard()252 boolean getShowImeWithHardKeyboard() { 253 return mShowImeWithHardKeyboard; 254 } 255 isisInputMethodPickerShownForTestLocked()256 boolean isisInputMethodPickerShownForTestLocked() { 257 if (mSwitchingDialog == null) { 258 return false; 259 } 260 return mSwitchingDialog.isShowing(); 261 } 262 handleHardKeyboardStatusChange(boolean available)263 void handleHardKeyboardStatusChange(boolean available) { 264 if (DEBUG) { 265 Slog.w(TAG, "HardKeyboardStatusChanged: available=" + available); 266 } 267 synchronized (ImfLock.class) { 268 if (mSwitchingDialog != null && mSwitchingDialogTitleView != null 269 && mSwitchingDialog.isShowing()) { 270 mSwitchingDialogTitleView.findViewById( 271 com.android.internal.R.id.hard_keyboard_section).setVisibility( 272 available ? View.VISIBLE : View.GONE); 273 } 274 } 275 } 276 277 private static class ImeSubtypeListAdapter extends ArrayAdapter<ImeSubtypeListItem> { 278 private final LayoutInflater mInflater; 279 private final int mTextViewResourceId; 280 private final List<ImeSubtypeListItem> mItemsList; 281 public int mCheckedItem; ImeSubtypeListAdapter(Context context, int textViewResourceId, List<ImeSubtypeListItem> itemsList, int checkedItem)282 private ImeSubtypeListAdapter(Context context, int textViewResourceId, 283 List<ImeSubtypeListItem> itemsList, int checkedItem) { 284 super(context, textViewResourceId, itemsList); 285 286 mTextViewResourceId = textViewResourceId; 287 mItemsList = itemsList; 288 mCheckedItem = checkedItem; 289 mInflater = LayoutInflater.from(context); 290 } 291 292 @Override getView(int position, View convertView, ViewGroup parent)293 public View getView(int position, View convertView, ViewGroup parent) { 294 final View view = convertView != null ? convertView 295 : mInflater.inflate(mTextViewResourceId, null); 296 if (position < 0 || position >= mItemsList.size()) return view; 297 final ImeSubtypeListItem item = mItemsList.get(position); 298 final CharSequence imeName = item.mImeName; 299 final CharSequence subtypeName = item.mSubtypeName; 300 final TextView firstTextView = view.findViewById(android.R.id.text1); 301 final TextView secondTextView = view.findViewById(android.R.id.text2); 302 if (TextUtils.isEmpty(subtypeName)) { 303 firstTextView.setText(imeName); 304 secondTextView.setVisibility(View.GONE); 305 } else { 306 firstTextView.setText(subtypeName); 307 secondTextView.setText(imeName); 308 secondTextView.setVisibility(View.VISIBLE); 309 } 310 final RadioButton radioButton = view.findViewById(com.android.internal.R.id.radio); 311 radioButton.setChecked(position == mCheckedItem); 312 return view; 313 } 314 } 315 } 316