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.NonNull; 23 import android.annotation.Nullable; 24 import android.app.AlertDialog; 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.provider.Settings; 30 import android.text.TextUtils; 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 WindowManagerInternal mWindowManagerInternal; 57 58 private AlertDialog.Builder mDialogBuilder; 59 private AlertDialog mSwitchingDialog; 60 private View mSwitchingDialogTitleView; 61 private InputMethodInfo[] mIms; 62 private int[] mSubtypeIds; 63 64 private boolean mShowImeWithHardKeyboard; 65 66 @GuardedBy("ImfLock.class") 67 @Nullable 68 private InputMethodDialogWindowContext mDialogWindowContext; 69 InputMethodMenuController(InputMethodManagerService service)70 InputMethodMenuController(InputMethodManagerService service) { 71 mService = service; 72 mWindowManagerInternal = LocalServices.getService(WindowManagerInternal.class); 73 } 74 75 @GuardedBy("ImfLock.class") showInputMethodMenuLocked(boolean showAuxSubtypes, int displayId, String preferredInputMethodId, int preferredInputMethodSubtypeId, @NonNull List<ImeSubtypeListItem> imList)76 void showInputMethodMenuLocked(boolean showAuxSubtypes, int displayId, 77 String preferredInputMethodId, int preferredInputMethodSubtypeId, 78 @NonNull List<ImeSubtypeListItem> imList) { 79 if (DEBUG) Slog.v(TAG, "Show switching menu. showAuxSubtypes=" + showAuxSubtypes); 80 81 final int userId = mService.getCurrentImeUserIdLocked(); 82 83 hideInputMethodMenuLocked(); 84 85 if (preferredInputMethodSubtypeId == NOT_A_SUBTYPE_ID) { 86 final InputMethodSubtype currentSubtype = 87 mService.getCurrentInputMethodSubtypeLocked(); 88 if (currentSubtype != null) { 89 final String curMethodId = mService.getSelectedMethodIdLocked(); 90 final InputMethodInfo currentImi = 91 mService.queryInputMethodForCurrentUserLocked(curMethodId); 92 preferredInputMethodSubtypeId = SubtypeUtils.getSubtypeIdFromHashCode( 93 currentImi, currentSubtype.hashCode()); 94 } 95 } 96 97 // Find out which item should be checked by default. 98 final int size = imList.size(); 99 mIms = new InputMethodInfo[size]; 100 mSubtypeIds = new int[size]; 101 // No items are checked by default. When we have a list of explicitly enabled subtypes, 102 // the implicit subtype is no longer listed, but if it is still the selected one, 103 // no items will be shown as checked. 104 int checkedItem = -1; 105 for (int i = 0; i < size; ++i) { 106 final ImeSubtypeListItem item = imList.get(i); 107 mIms[i] = item.mImi; 108 mSubtypeIds[i] = item.mSubtypeId; 109 if (mIms[i].getId().equals(preferredInputMethodId)) { 110 int subtypeId = mSubtypeIds[i]; 111 if ((subtypeId == NOT_A_SUBTYPE_ID) 112 || (preferredInputMethodSubtypeId == NOT_A_SUBTYPE_ID && subtypeId == 0) 113 || (subtypeId == preferredInputMethodSubtypeId)) { 114 checkedItem = i; 115 } 116 } 117 } 118 119 if (checkedItem == -1) { 120 Slog.w(TAG, "Switching menu shown with no item selected" 121 + ", IME id: " + preferredInputMethodId 122 + ", subtype index: " + preferredInputMethodSubtypeId); 123 } 124 125 if (mDialogWindowContext == null) { 126 mDialogWindowContext = new InputMethodDialogWindowContext(); 127 } 128 final Context dialogWindowContext = mDialogWindowContext.get(displayId); 129 mDialogBuilder = new AlertDialog.Builder(dialogWindowContext); 130 mDialogBuilder.setOnCancelListener(dialog -> hideInputMethodMenu()); 131 132 final Context dialogContext = mDialogBuilder.getContext(); 133 final TypedArray a = dialogContext.obtainStyledAttributes(null, 134 com.android.internal.R.styleable.DialogPreference, 135 com.android.internal.R.attr.alertDialogStyle, 0); 136 final Drawable dialogIcon = a.getDrawable( 137 com.android.internal.R.styleable.DialogPreference_dialogIcon); 138 a.recycle(); 139 140 mDialogBuilder.setIcon(dialogIcon); 141 142 final LayoutInflater inflater = dialogContext.getSystemService(LayoutInflater.class); 143 final View tv = inflater.inflate( 144 com.android.internal.R.layout.input_method_switch_dialog_title, null); 145 mDialogBuilder.setCustomTitle(tv); 146 147 // Setup layout for a toggle switch of the hardware keyboard 148 mSwitchingDialogTitleView = tv; 149 mSwitchingDialogTitleView 150 .findViewById(com.android.internal.R.id.hard_keyboard_section) 151 .setVisibility(mWindowManagerInternal.isHardKeyboardAvailable() 152 ? View.VISIBLE : View.GONE); 153 final Switch hardKeySwitch = mSwitchingDialogTitleView.findViewById( 154 com.android.internal.R.id.hard_keyboard_switch); 155 hardKeySwitch.setChecked(mShowImeWithHardKeyboard); 156 hardKeySwitch.setOnCheckedChangeListener((buttonView, isChecked) -> { 157 SecureSettingsWrapper.putBoolean(Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD, 158 isChecked, userId); 159 // Ensure that the input method dialog is dismissed when changing 160 // the hardware keyboard state. 161 hideInputMethodMenu(); 162 }); 163 164 // Fill the list items with onClick listener, which takes care of IME (and subtype) 165 // switching when clicked. 166 final ImeSubtypeListAdapter adapter = new ImeSubtypeListAdapter(dialogContext, 167 com.android.internal.R.layout.input_method_switch_item, imList, checkedItem); 168 final DialogInterface.OnClickListener choiceListener = (dialog, which) -> { 169 synchronized (ImfLock.class) { 170 if (mIms == null || mIms.length <= which || mSubtypeIds == null 171 || mSubtypeIds.length <= which) { 172 return; 173 } 174 final InputMethodInfo im = mIms[which]; 175 int subtypeId = mSubtypeIds[which]; 176 adapter.mCheckedItem = which; 177 adapter.notifyDataSetChanged(); 178 if (im != null) { 179 if (subtypeId < 0 || subtypeId >= im.getSubtypeCount()) { 180 subtypeId = NOT_A_SUBTYPE_ID; 181 } 182 mService.setInputMethodLocked(im.getId(), subtypeId); 183 } 184 hideInputMethodMenuLocked(); 185 } 186 }; 187 mDialogBuilder.setSingleChoiceItems(adapter, checkedItem, choiceListener); 188 189 // Final steps to instantiate a dialog to show it up. 190 mSwitchingDialog = mDialogBuilder.create(); 191 mSwitchingDialog.setCanceledOnTouchOutside(true); 192 final Window w = mSwitchingDialog.getWindow(); 193 final WindowManager.LayoutParams attrs = w.getAttributes(); 194 w.setType(WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG); 195 w.setHideOverlayWindows(true); 196 // Use an alternate token for the dialog for that window manager can group the token 197 // with other IME windows based on type vs. grouping based on whichever token happens 198 // to get selected by the system later on. 199 attrs.token = dialogWindowContext.getWindowContextToken(); 200 attrs.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; 201 attrs.setTitle("Select input method"); 202 w.setAttributes(attrs); 203 mService.updateSystemUiLocked(); 204 mService.sendOnNavButtonFlagsChangedLocked(); 205 mSwitchingDialog.show(); 206 } 207 updateKeyboardFromSettingsLocked()208 void updateKeyboardFromSettingsLocked() { 209 mShowImeWithHardKeyboard = 210 SecureSettingsWrapper.getBoolean(Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD, 211 false, mService.getCurrentImeUserIdLocked()); 212 if (mSwitchingDialog != null && mSwitchingDialogTitleView != null 213 && mSwitchingDialog.isShowing()) { 214 final Switch hardKeySwitch = mSwitchingDialogTitleView.findViewById( 215 com.android.internal.R.id.hard_keyboard_switch); 216 hardKeySwitch.setChecked(mShowImeWithHardKeyboard); 217 } 218 } 219 220 /** 221 * Hides the input method switcher menu. 222 */ hideInputMethodMenu()223 void hideInputMethodMenu() { 224 synchronized (ImfLock.class) { 225 hideInputMethodMenuLocked(); 226 } 227 } 228 229 /** 230 * Hides the input method switcher menu, synchronised version of {@link #hideInputMethodMenu}. 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