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.content.Context; 25 import android.content.DialogInterface; 26 import android.content.res.TypedArray; 27 import android.graphics.drawable.Drawable; 28 import android.text.TextUtils; 29 import android.util.ArrayMap; 30 import android.util.Slog; 31 import android.view.LayoutInflater; 32 import android.view.View; 33 import android.view.ViewGroup; 34 import android.view.Window; 35 import android.view.WindowManager; 36 import android.view.inputmethod.InputMethodInfo; 37 import android.view.inputmethod.InputMethodSubtype; 38 import android.widget.ArrayAdapter; 39 import android.widget.RadioButton; 40 import android.widget.Switch; 41 import android.widget.TextView; 42 43 import com.android.internal.annotations.GuardedBy; 44 import com.android.server.LocalServices; 45 import com.android.server.inputmethod.InputMethodSubtypeSwitchingController.ImeSubtypeListItem; 46 import com.android.server.wm.WindowManagerInternal; 47 48 import java.util.List; 49 50 /** A controller to show/hide the input method menu */ 51 final class InputMethodMenuController { 52 private static final String TAG = InputMethodMenuController.class.getSimpleName(); 53 54 private final InputMethodManagerService mService; 55 private final InputMethodUtils.InputMethodSettings mSettings; 56 private final InputMethodSubtypeSwitchingController mSwitchingController; 57 private final ArrayMap<String, InputMethodInfo> mMethodMap; 58 private final WindowManagerInternal mWindowManagerInternal; 59 60 private AlertDialog.Builder mDialogBuilder; 61 private AlertDialog mSwitchingDialog; 62 private View mSwitchingDialogTitleView; 63 private InputMethodInfo[] mIms; 64 private int[] mSubtypeIds; 65 66 private boolean mShowImeWithHardKeyboard; 67 68 @GuardedBy("ImfLock.class") 69 @Nullable 70 private InputMethodDialogWindowContext mDialogWindowContext; 71 InputMethodMenuController(InputMethodManagerService service)72 InputMethodMenuController(InputMethodManagerService service) { 73 mService = service; 74 mSettings = mService.mSettings; 75 mSwitchingController = mService.mSwitchingController; 76 mMethodMap = mService.mMethodMap; 77 mWindowManagerInternal = LocalServices.getService(WindowManagerInternal.class); 78 } 79 showInputMethodMenu(boolean showAuxSubtypes, int displayId)80 void showInputMethodMenu(boolean showAuxSubtypes, int displayId) { 81 if (DEBUG) Slog.v(TAG, "Show switching menu. showAuxSubtypes=" + showAuxSubtypes); 82 83 final boolean isScreenLocked = isScreenLocked(); 84 85 final String lastInputMethodId = mSettings.getSelectedInputMethod(); 86 int lastInputMethodSubtypeId = mSettings.getSelectedInputMethodSubtypeId(lastInputMethodId); 87 if (DEBUG) Slog.v(TAG, "Current IME: " + lastInputMethodId); 88 89 synchronized (ImfLock.class) { 90 final List<ImeSubtypeListItem> imList = mSwitchingController 91 .getSortedInputMethodAndSubtypeListForImeMenuLocked( 92 showAuxSubtypes, isScreenLocked); 93 if (imList.isEmpty()) { 94 return; 95 } 96 97 hideInputMethodMenuLocked(); 98 99 if (lastInputMethodSubtypeId == NOT_A_SUBTYPE_ID) { 100 final InputMethodSubtype currentSubtype = 101 mService.getCurrentInputMethodSubtypeLocked(); 102 if (currentSubtype != null) { 103 final String curMethodId = mService.getSelectedMethodIdLocked(); 104 final InputMethodInfo currentImi = mMethodMap.get(curMethodId); 105 lastInputMethodSubtypeId = SubtypeUtils.getSubtypeIdFromHashCode( 106 currentImi, currentSubtype.hashCode()); 107 } 108 } 109 110 final int size = imList.size(); 111 mIms = new InputMethodInfo[size]; 112 mSubtypeIds = new int[size]; 113 int checkedItem = 0; 114 for (int i = 0; i < size; ++i) { 115 final ImeSubtypeListItem item = imList.get(i); 116 mIms[i] = item.mImi; 117 mSubtypeIds[i] = item.mSubtypeId; 118 if (mIms[i].getId().equals(lastInputMethodId)) { 119 int subtypeId = mSubtypeIds[i]; 120 if ((subtypeId == NOT_A_SUBTYPE_ID) 121 || (lastInputMethodSubtypeId == NOT_A_SUBTYPE_ID && subtypeId == 0) 122 || (subtypeId == lastInputMethodSubtypeId)) { 123 checkedItem = i; 124 } 125 } 126 } 127 128 if (mDialogWindowContext == null) { 129 mDialogWindowContext = new InputMethodDialogWindowContext(); 130 } 131 final Context dialogWindowContext = mDialogWindowContext.get(displayId); 132 mDialogBuilder = new AlertDialog.Builder(dialogWindowContext); 133 mDialogBuilder.setOnCancelListener(dialog -> hideInputMethodMenu()); 134 135 final Context dialogContext = mDialogBuilder.getContext(); 136 final TypedArray a = dialogContext.obtainStyledAttributes(null, 137 com.android.internal.R.styleable.DialogPreference, 138 com.android.internal.R.attr.alertDialogStyle, 0); 139 final Drawable dialogIcon = a.getDrawable( 140 com.android.internal.R.styleable.DialogPreference_dialogIcon); 141 a.recycle(); 142 143 mDialogBuilder.setIcon(dialogIcon); 144 145 final LayoutInflater inflater = dialogContext.getSystemService(LayoutInflater.class); 146 final View tv = inflater.inflate( 147 com.android.internal.R.layout.input_method_switch_dialog_title, null); 148 mDialogBuilder.setCustomTitle(tv); 149 150 // Setup layout for a toggle switch of the hardware keyboard 151 mSwitchingDialogTitleView = tv; 152 mSwitchingDialogTitleView 153 .findViewById(com.android.internal.R.id.hard_keyboard_section) 154 .setVisibility(mWindowManagerInternal.isHardKeyboardAvailable() 155 ? View.VISIBLE : View.GONE); 156 final Switch hardKeySwitch = mSwitchingDialogTitleView.findViewById( 157 com.android.internal.R.id.hard_keyboard_switch); 158 hardKeySwitch.setChecked(mShowImeWithHardKeyboard); 159 hardKeySwitch.setOnCheckedChangeListener((buttonView, isChecked) -> { 160 mSettings.setShowImeWithHardKeyboard(isChecked); 161 // Ensure that the input method dialog is dismissed when changing 162 // the hardware keyboard state. 163 hideInputMethodMenu(); 164 }); 165 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 mSwitchingDialog = mDialogBuilder.create(); 190 mSwitchingDialog.setCanceledOnTouchOutside(true); 191 final Window w = mSwitchingDialog.getWindow(); 192 final WindowManager.LayoutParams attrs = w.getAttributes(); 193 w.setType(WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG); 194 w.setHideOverlayWindows(true); 195 // Use an alternate token for the dialog for that window manager can group the token 196 // with other IME windows based on type vs. grouping based on whichever token happens 197 // to get selected by the system later on. 198 attrs.token = dialogWindowContext.getWindowContextToken(); 199 attrs.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; 200 attrs.setTitle("Select input method"); 201 w.setAttributes(attrs); 202 mService.updateSystemUiLocked(); 203 mService.sendOnNavButtonFlagsChangedLocked(); 204 mSwitchingDialog.show(); 205 } 206 } 207 isScreenLocked()208 private boolean isScreenLocked() { 209 return mWindowManagerInternal.isKeyguardLocked() 210 && mWindowManagerInternal.isKeyguardSecure(mSettings.getCurrentUserId()); 211 } 212 updateKeyboardFromSettingsLocked()213 void updateKeyboardFromSettingsLocked() { 214 mShowImeWithHardKeyboard = mSettings.isShowImeWithHardKeyboardEnabled(); 215 if (mSwitchingDialog != null && mSwitchingDialogTitleView != null 216 && mSwitchingDialog.isShowing()) { 217 final Switch hardKeySwitch = mSwitchingDialogTitleView.findViewById( 218 com.android.internal.R.id.hard_keyboard_switch); 219 hardKeySwitch.setChecked(mShowImeWithHardKeyboard); 220 } 221 } 222 223 /** 224 * Hides the input method switcher menu. 225 */ hideInputMethodMenu()226 void hideInputMethodMenu() { 227 synchronized (ImfLock.class) { 228 hideInputMethodMenuLocked(); 229 } 230 } 231 232 /** 233 * Hides the input method switcher menu, synchronised version of {@link #hideInputMethodMenu}. 234 */ 235 @GuardedBy("ImfLock.class") hideInputMethodMenuLocked()236 void hideInputMethodMenuLocked() { 237 if (DEBUG) Slog.v(TAG, "Hide switching menu"); 238 239 if (mSwitchingDialog != null) { 240 mSwitchingDialog.dismiss(); 241 mSwitchingDialog = null; 242 mSwitchingDialogTitleView = null; 243 244 mService.updateSystemUiLocked(); 245 mService.sendOnNavButtonFlagsChangedLocked(); 246 mDialogBuilder = null; 247 mIms = null; 248 } 249 } 250 getSwitchingDialogLocked()251 AlertDialog getSwitchingDialogLocked() { 252 return mSwitchingDialog; 253 } 254 getShowImeWithHardKeyboard()255 boolean getShowImeWithHardKeyboard() { 256 return mShowImeWithHardKeyboard; 257 } 258 isisInputMethodPickerShownForTestLocked()259 boolean isisInputMethodPickerShownForTestLocked() { 260 if (mSwitchingDialog == null) { 261 return false; 262 } 263 return mSwitchingDialog.isShowing(); 264 } 265 handleHardKeyboardStatusChange(boolean available)266 void handleHardKeyboardStatusChange(boolean available) { 267 if (DEBUG) { 268 Slog.w(TAG, "HardKeyboardStatusChanged: available=" + available); 269 } 270 synchronized (ImfLock.class) { 271 if (mSwitchingDialog != null && mSwitchingDialogTitleView != null 272 && mSwitchingDialog.isShowing()) { 273 mSwitchingDialogTitleView.findViewById( 274 com.android.internal.R.id.hard_keyboard_section).setVisibility( 275 available ? View.VISIBLE : View.GONE); 276 } 277 } 278 } 279 280 private static class ImeSubtypeListAdapter extends ArrayAdapter<ImeSubtypeListItem> { 281 private final LayoutInflater mInflater; 282 private final int mTextViewResourceId; 283 private final List<ImeSubtypeListItem> mItemsList; 284 public int mCheckedItem; ImeSubtypeListAdapter(Context context, int textViewResourceId, List<ImeSubtypeListItem> itemsList, int checkedItem)285 private ImeSubtypeListAdapter(Context context, int textViewResourceId, 286 List<ImeSubtypeListItem> itemsList, int checkedItem) { 287 super(context, textViewResourceId, itemsList); 288 289 mTextViewResourceId = textViewResourceId; 290 mItemsList = itemsList; 291 mCheckedItem = checkedItem; 292 mInflater = LayoutInflater.from(context); 293 } 294 295 @Override getView(int position, View convertView, ViewGroup parent)296 public View getView(int position, View convertView, ViewGroup parent) { 297 final View view = convertView != null ? convertView 298 : mInflater.inflate(mTextViewResourceId, null); 299 if (position < 0 || position >= mItemsList.size()) return view; 300 final ImeSubtypeListItem item = mItemsList.get(position); 301 final CharSequence imeName = item.mImeName; 302 final CharSequence subtypeName = item.mSubtypeName; 303 final TextView firstTextView = view.findViewById(android.R.id.text1); 304 final TextView secondTextView = view.findViewById(android.R.id.text2); 305 if (TextUtils.isEmpty(subtypeName)) { 306 firstTextView.setText(imeName); 307 secondTextView.setVisibility(View.GONE); 308 } else { 309 firstTextView.setText(subtypeName); 310 secondTextView.setText(imeName); 311 secondTextView.setVisibility(View.VISIBLE); 312 } 313 final RadioButton radioButton = view.findViewById(com.android.internal.R.id.radio); 314 radioButton.setChecked(position == mCheckedItem); 315 return view; 316 } 317 } 318 } 319