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_INDEX; 21 22 import android.annotation.NonNull; 23 import android.annotation.Nullable; 24 import android.annotation.UserIdInt; 25 import android.app.AlertDialog; 26 import android.content.Context; 27 import android.content.DialogInterface; 28 import android.content.res.TypedArray; 29 import android.graphics.drawable.Drawable; 30 import android.provider.Settings; 31 import android.text.TextUtils; 32 import android.util.Printer; 33 import android.util.Slog; 34 import android.view.LayoutInflater; 35 import android.view.View; 36 import android.view.ViewGroup; 37 import android.view.Window; 38 import android.view.WindowManager; 39 import android.view.inputmethod.InputMethodInfo; 40 import android.view.inputmethod.InputMethodSubtype; 41 import android.widget.ArrayAdapter; 42 import android.widget.RadioButton; 43 import android.widget.Switch; 44 import android.widget.TextView; 45 46 import com.android.internal.annotations.GuardedBy; 47 import com.android.server.LocalServices; 48 import com.android.server.inputmethod.InputMethodSubtypeSwitchingController.ImeSubtypeListItem; 49 import com.android.server.wm.WindowManagerInternal; 50 51 import java.util.List; 52 53 /** A controller to show/hide the input method menu */ 54 final class InputMethodMenuController { 55 private static final String TAG = InputMethodMenuController.class.getSimpleName(); 56 57 private final InputMethodManagerService mService; 58 private final WindowManagerInternal mWindowManagerInternal; 59 60 private AlertDialog.Builder mDialogBuilder; 61 private AlertDialog mSwitchingDialog; 62 private View mSwitchingDialogTitleView; 63 private List<ImeSubtypeListItem> mImList; 64 private InputMethodInfo[] mIms; 65 private int[] mSubtypeIndices; 66 67 private boolean mShowImeWithHardKeyboard; 68 69 @GuardedBy("ImfLock.class") 70 @Nullable 71 private InputMethodDialogWindowContext mDialogWindowContext; 72 InputMethodMenuController(InputMethodManagerService service)73 InputMethodMenuController(InputMethodManagerService service) { 74 mService = service; 75 mWindowManagerInternal = LocalServices.getService(WindowManagerInternal.class); 76 } 77 78 @GuardedBy("ImfLock.class") showInputMethodMenuLocked(boolean showAuxSubtypes, int displayId, String preferredInputMethodId, int preferredInputMethodSubtypeIndex, @NonNull List<ImeSubtypeListItem> imList, @UserIdInt int userId)79 void showInputMethodMenuLocked(boolean showAuxSubtypes, int displayId, 80 String preferredInputMethodId, int preferredInputMethodSubtypeIndex, 81 @NonNull List<ImeSubtypeListItem> imList, @UserIdInt int userId) { 82 if (DEBUG) Slog.v(TAG, "Show switching menu. showAuxSubtypes=" + showAuxSubtypes); 83 84 final var bindingController = mService.getInputMethodBindingController(userId); 85 86 hideInputMethodMenuLocked(userId); 87 88 if (preferredInputMethodSubtypeIndex == NOT_A_SUBTYPE_INDEX) { 89 final InputMethodSubtype currentSubtype = 90 bindingController.getCurrentInputMethodSubtype(); 91 if (currentSubtype != null) { 92 final String curMethodId = bindingController.getSelectedMethodId(); 93 final InputMethodInfo currentImi = 94 InputMethodSettingsRepository.get(userId).getMethodMap().get(curMethodId); 95 preferredInputMethodSubtypeIndex = SubtypeUtils.getSubtypeIndexFromHashCode( 96 currentImi, currentSubtype.hashCode()); 97 } 98 } 99 100 // Find out which item should be checked by default. 101 final int size = imList.size(); 102 mImList = imList; 103 mIms = new InputMethodInfo[size]; 104 mSubtypeIndices = new int[size]; 105 // No items are checked by default. When we have a list of explicitly enabled subtypes, 106 // the implicit subtype is no longer listed, but if it is still the selected one, 107 // no items will be shown as checked. 108 int checkedItem = -1; 109 for (int i = 0; i < size; ++i) { 110 final ImeSubtypeListItem item = imList.get(i); 111 mIms[i] = item.mImi; 112 mSubtypeIndices[i] = item.mSubtypeIndex; 113 if (mIms[i].getId().equals(preferredInputMethodId)) { 114 int subtypeIndex = mSubtypeIndices[i]; 115 if ((subtypeIndex == NOT_A_SUBTYPE_INDEX) 116 || (preferredInputMethodSubtypeIndex == NOT_A_SUBTYPE_INDEX 117 && subtypeIndex == 0) 118 || (subtypeIndex == preferredInputMethodSubtypeIndex)) { 119 checkedItem = i; 120 } 121 } 122 } 123 124 if (checkedItem == -1) { 125 Slog.w(TAG, "Switching menu shown with no item selected" 126 + ", IME id: " + preferredInputMethodId 127 + ", subtype index: " + preferredInputMethodSubtypeIndex); 128 } 129 130 if (mDialogWindowContext == null) { 131 mDialogWindowContext = new InputMethodDialogWindowContext(); 132 } 133 final Context dialogWindowContext = mDialogWindowContext.get(displayId); 134 mDialogBuilder = new AlertDialog.Builder(dialogWindowContext); 135 mDialogBuilder.setOnCancelListener(dialog -> hideInputMethodMenu(userId)); 136 137 final Context dialogContext = mDialogBuilder.getContext(); 138 final TypedArray a = dialogContext.obtainStyledAttributes(null, 139 com.android.internal.R.styleable.DialogPreference, 140 com.android.internal.R.attr.alertDialogStyle, 0); 141 final Drawable dialogIcon = a.getDrawable( 142 com.android.internal.R.styleable.DialogPreference_dialogIcon); 143 a.recycle(); 144 145 mDialogBuilder.setIcon(dialogIcon); 146 147 final LayoutInflater inflater = dialogContext.getSystemService(LayoutInflater.class); 148 final View tv = inflater.inflate( 149 com.android.internal.R.layout.input_method_switch_dialog_title, null); 150 mDialogBuilder.setCustomTitle(tv); 151 152 // Setup layout for a toggle switch of the hardware keyboard 153 mSwitchingDialogTitleView = tv; 154 mSwitchingDialogTitleView 155 .findViewById(com.android.internal.R.id.hard_keyboard_section) 156 .setVisibility(mWindowManagerInternal.isHardKeyboardAvailable() 157 ? View.VISIBLE : View.GONE); 158 final Switch hardKeySwitch = mSwitchingDialogTitleView.findViewById( 159 com.android.internal.R.id.hard_keyboard_switch); 160 hardKeySwitch.setChecked(mShowImeWithHardKeyboard); 161 hardKeySwitch.setOnCheckedChangeListener((buttonView, isChecked) -> { 162 SecureSettingsWrapper.putBoolean(Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD, 163 isChecked, userId); 164 // Ensure that the input method dialog is dismissed when changing 165 // the hardware keyboard state. 166 hideInputMethodMenu(userId); 167 }); 168 169 // Fill the list items with onClick listener, which takes care of IME (and subtype) 170 // switching when clicked. 171 final ImeSubtypeListAdapter adapter = new ImeSubtypeListAdapter(dialogContext, 172 com.android.internal.R.layout.input_method_switch_item, imList, checkedItem); 173 final DialogInterface.OnClickListener choiceListener = (dialog, which) -> { 174 synchronized (ImfLock.class) { 175 if (mIms == null || mIms.length <= which || mSubtypeIndices == null 176 || mSubtypeIndices.length <= which) { 177 return; 178 } 179 final InputMethodInfo im = mIms[which]; 180 int subtypeIndex = mSubtypeIndices[which]; 181 adapter.mCheckedItem = which; 182 adapter.notifyDataSetChanged(); 183 if (im != null) { 184 if (subtypeIndex < 0 || subtypeIndex >= im.getSubtypeCount()) { 185 subtypeIndex = NOT_A_SUBTYPE_INDEX; 186 } 187 mService.setInputMethodLocked(im.getId(), subtypeIndex, userId); 188 } 189 hideInputMethodMenuLocked(userId); 190 } 191 }; 192 mDialogBuilder.setSingleChoiceItems(adapter, checkedItem, choiceListener); 193 194 // Final steps to instantiate a dialog to show it up. 195 mSwitchingDialog = mDialogBuilder.create(); 196 mSwitchingDialog.setCanceledOnTouchOutside(true); 197 final Window w = mSwitchingDialog.getWindow(); 198 final WindowManager.LayoutParams attrs = w.getAttributes(); 199 w.setType(WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG); 200 w.setHideOverlayWindows(true); 201 // Use an alternate token for the dialog for that window manager can group the token 202 // with other IME windows based on type vs. grouping based on whichever token happens 203 // to get selected by the system later on. 204 attrs.token = dialogWindowContext.getWindowContextToken(); 205 attrs.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; 206 attrs.setTitle("Select input method"); 207 w.setAttributes(attrs); 208 mService.updateSystemUiLocked(userId); 209 mService.sendOnNavButtonFlagsChangedLocked(mService.getUserData(userId)); 210 mSwitchingDialog.show(); 211 } 212 updateKeyboardFromSettingsLocked(@serIdInt int userId)213 void updateKeyboardFromSettingsLocked(@UserIdInt int userId) { 214 mShowImeWithHardKeyboard = 215 SecureSettingsWrapper.getBoolean(Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD, 216 false, userId); 217 if (mSwitchingDialog != null && mSwitchingDialogTitleView != null 218 && mSwitchingDialog.isShowing()) { 219 final Switch hardKeySwitch = mSwitchingDialogTitleView.findViewById( 220 com.android.internal.R.id.hard_keyboard_switch); 221 hardKeySwitch.setChecked(mShowImeWithHardKeyboard); 222 } 223 } 224 225 /** 226 * Hides the input method switcher menu. 227 * 228 * @param userId user ID for this operation 229 */ hideInputMethodMenu(@serIdInt int userId)230 void hideInputMethodMenu(@UserIdInt int userId) { 231 synchronized (ImfLock.class) { 232 hideInputMethodMenuLocked(userId); 233 } 234 } 235 236 /** 237 * Hides the input method switcher menu, synchronised version of {@link #hideInputMethodMenu}. 238 * 239 * @param userId user ID for this operation 240 */ 241 @GuardedBy("ImfLock.class") hideInputMethodMenuLocked(@serIdInt int userId)242 void hideInputMethodMenuLocked(@UserIdInt int userId) { 243 if (DEBUG) Slog.v(TAG, "Hide switching menu"); 244 245 if (mSwitchingDialog != null) { 246 mSwitchingDialog.dismiss(); 247 mSwitchingDialog = null; 248 mSwitchingDialogTitleView = null; 249 250 mService.updateSystemUiLocked(userId); 251 mService.sendOnNavButtonFlagsChangedToAllImesLocked(); 252 mDialogBuilder = null; 253 mImList = null; 254 mIms = null; 255 mSubtypeIndices = null; 256 } 257 } 258 getSwitchingDialogLocked()259 AlertDialog getSwitchingDialogLocked() { 260 return mSwitchingDialog; 261 } 262 getShowImeWithHardKeyboard()263 boolean getShowImeWithHardKeyboard() { 264 return mShowImeWithHardKeyboard; 265 } 266 isisInputMethodPickerShownForTestLocked()267 boolean isisInputMethodPickerShownForTestLocked() { 268 if (mSwitchingDialog == null) { 269 return false; 270 } 271 return mSwitchingDialog.isShowing(); 272 } 273 handleHardKeyboardStatusChange(boolean available)274 void handleHardKeyboardStatusChange(boolean available) { 275 if (DEBUG) { 276 Slog.w(TAG, "HardKeyboardStatusChanged: available=" + available); 277 } 278 synchronized (ImfLock.class) { 279 if (mSwitchingDialog != null && mSwitchingDialogTitleView != null 280 && mSwitchingDialog.isShowing()) { 281 mSwitchingDialogTitleView.findViewById( 282 com.android.internal.R.id.hard_keyboard_section).setVisibility( 283 available ? View.VISIBLE : View.GONE); 284 } 285 } 286 } 287 dump(@onNull Printer pw, @NonNull String prefix)288 void dump(@NonNull Printer pw, @NonNull String prefix) { 289 final boolean showing = isisInputMethodPickerShownForTestLocked(); 290 pw.println(prefix + "isShowing: " + showing); 291 292 if (showing) { 293 pw.println(prefix + "imList: " + mImList); 294 } 295 } 296 297 private static class ImeSubtypeListAdapter extends ArrayAdapter<ImeSubtypeListItem> { 298 private final LayoutInflater mInflater; 299 private final int mTextViewResourceId; 300 private final List<ImeSubtypeListItem> mItemsList; 301 public int mCheckedItem; ImeSubtypeListAdapter(Context context, int textViewResourceId, List<ImeSubtypeListItem> itemsList, int checkedItem)302 private ImeSubtypeListAdapter(Context context, int textViewResourceId, 303 List<ImeSubtypeListItem> itemsList, int checkedItem) { 304 super(context, textViewResourceId, itemsList); 305 306 mTextViewResourceId = textViewResourceId; 307 mItemsList = itemsList; 308 mCheckedItem = checkedItem; 309 mInflater = LayoutInflater.from(context); 310 } 311 312 @Override getView(int position, View convertView, ViewGroup parent)313 public View getView(int position, View convertView, ViewGroup parent) { 314 final View view = convertView != null ? convertView 315 : mInflater.inflate(mTextViewResourceId, null); 316 if (position < 0 || position >= mItemsList.size()) return view; 317 final ImeSubtypeListItem item = mItemsList.get(position); 318 final CharSequence imeName = item.mImeName; 319 final CharSequence subtypeName = item.mSubtypeName; 320 final TextView firstTextView = view.findViewById(android.R.id.text1); 321 final TextView secondTextView = view.findViewById(android.R.id.text2); 322 if (TextUtils.isEmpty(subtypeName)) { 323 firstTextView.setText(imeName); 324 secondTextView.setVisibility(View.GONE); 325 } else { 326 firstTextView.setText(subtypeName); 327 secondTextView.setText(imeName); 328 secondTextView.setVisibility(View.VISIBLE); 329 } 330 final RadioButton radioButton = view.findViewById(com.android.internal.R.id.radio); 331 radioButton.setChecked(position == mCheckedItem); 332 return view; 333 } 334 } 335 } 336