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.internal.annotations.VisibleForTesting.Visibility.PACKAGE; 20 import static com.android.server.inputmethod.InputMethodManagerService.DEBUG; 21 import static com.android.server.inputmethod.InputMethodManagerService.MSG_HARD_KEYBOARD_SWITCH_CHANGED; 22 import static com.android.server.inputmethod.InputMethodUtils.NOT_A_SUBTYPE_ID; 23 24 import android.app.ActivityThread; 25 import android.app.AlertDialog; 26 import android.app.KeyguardManager; 27 import android.content.Context; 28 import android.content.DialogInterface; 29 import android.content.res.TypedArray; 30 import android.graphics.drawable.Drawable; 31 import android.os.Handler; 32 import android.os.IBinder; 33 import android.text.TextUtils; 34 import android.util.ArrayMap; 35 import android.util.Slog; 36 import android.view.ContextThemeWrapper; 37 import android.view.LayoutInflater; 38 import android.view.View; 39 import android.view.ViewGroup; 40 import android.view.Window; 41 import android.view.WindowManager; 42 import android.view.inputmethod.InputMethodInfo; 43 import android.view.inputmethod.InputMethodSubtype; 44 import android.widget.ArrayAdapter; 45 import android.widget.RadioButton; 46 import android.widget.Switch; 47 import android.widget.TextView; 48 49 import com.android.internal.annotations.VisibleForTesting; 50 import com.android.server.LocalServices; 51 import com.android.server.inputmethod.InputMethodSubtypeSwitchingController.ImeSubtypeListItem; 52 import com.android.server.wm.WindowManagerInternal; 53 54 import java.util.List; 55 56 /** A controller to show/hide the input method menu */ 57 @VisibleForTesting(visibility = PACKAGE) 58 public class InputMethodMenuController { 59 private static final String TAG = InputMethodMenuController.class.getSimpleName(); 60 61 private final InputMethodManagerService mService; 62 private final InputMethodUtils.InputMethodSettings mSettings; 63 private final InputMethodSubtypeSwitchingController mSwitchingController; 64 private final Handler mHandler; 65 private final ArrayMap<String, InputMethodInfo> mMethodMap; 66 private final KeyguardManager mKeyguardManager; 67 private final HardKeyboardListener mHardKeyboardListener; 68 private final WindowManagerInternal mWindowManagerInternal; 69 70 private Context mSettingsContext; 71 private AlertDialog.Builder mDialogBuilder; 72 private AlertDialog mSwitchingDialog; 73 private IBinder mSwitchingDialogToken; 74 private View mSwitchingDialogTitleView; 75 private InputMethodInfo[] mIms; 76 private int[] mSubtypeIds; 77 78 private boolean mShowImeWithHardKeyboard; 79 80 @VisibleForTesting(visibility = PACKAGE) InputMethodMenuController(InputMethodManagerService service)81 public InputMethodMenuController(InputMethodManagerService service) { 82 mService = service; 83 mSettings = mService.mSettings; 84 mSwitchingController = mService.mSwitchingController; 85 mHandler = mService.mHandler; 86 mMethodMap = mService.mMethodMap; 87 mKeyguardManager = mService.mKeyguardManager; 88 mHardKeyboardListener = new HardKeyboardListener(); 89 mWindowManagerInternal = LocalServices.getService(WindowManagerInternal.class); 90 } 91 showInputMethodMenu(boolean showAuxSubtypes, int displayId)92 void showInputMethodMenu(boolean showAuxSubtypes, int displayId) { 93 if (DEBUG) Slog.v(TAG, "Show switching menu. showAuxSubtypes=" + showAuxSubtypes); 94 95 final boolean isScreenLocked = isScreenLocked(); 96 97 final String lastInputMethodId = mSettings.getSelectedInputMethod(); 98 int lastInputMethodSubtypeId = mSettings.getSelectedInputMethodSubtypeId(lastInputMethodId); 99 if (DEBUG) Slog.v(TAG, "Current IME: " + lastInputMethodId); 100 101 synchronized (mMethodMap) { 102 final List<ImeSubtypeListItem> imList = mSwitchingController 103 .getSortedInputMethodAndSubtypeListForImeMenuLocked( 104 showAuxSubtypes, isScreenLocked); 105 if (imList.isEmpty()) { 106 return; 107 } 108 109 hideInputMethodMenuLocked(); 110 111 if (lastInputMethodSubtypeId == NOT_A_SUBTYPE_ID) { 112 final InputMethodSubtype currentSubtype = 113 mService.getCurrentInputMethodSubtypeLocked(); 114 if (currentSubtype != null) { 115 final String curMethodId = mService.getCurrentMethodId(); 116 final InputMethodInfo currentImi = mMethodMap.get(curMethodId); 117 lastInputMethodSubtypeId = InputMethodUtils.getSubtypeIdFromHashCode( 118 currentImi, currentSubtype.hashCode()); 119 } 120 } 121 122 final int size = imList.size(); 123 mIms = new InputMethodInfo[size]; 124 mSubtypeIds = new int[size]; 125 int checkedItem = 0; 126 for (int i = 0; i < size; ++i) { 127 final ImeSubtypeListItem item = imList.get(i); 128 mIms[i] = item.mImi; 129 mSubtypeIds[i] = item.mSubtypeId; 130 if (mIms[i].getId().equals(lastInputMethodId)) { 131 int subtypeId = mSubtypeIds[i]; 132 if ((subtypeId == NOT_A_SUBTYPE_ID) 133 || (lastInputMethodSubtypeId == NOT_A_SUBTYPE_ID && subtypeId == 0) 134 || (subtypeId == lastInputMethodSubtypeId)) { 135 checkedItem = i; 136 } 137 } 138 } 139 140 final Context settingsContext = getSettingsContext(displayId); 141 mDialogBuilder = new AlertDialog.Builder(settingsContext); 142 mDialogBuilder.setOnCancelListener(dialog -> hideInputMethodMenu()); 143 144 final Context dialogContext = mDialogBuilder.getContext(); 145 final TypedArray a = dialogContext.obtainStyledAttributes(null, 146 com.android.internal.R.styleable.DialogPreference, 147 com.android.internal.R.attr.alertDialogStyle, 0); 148 final Drawable dialogIcon = a.getDrawable( 149 com.android.internal.R.styleable.DialogPreference_dialogIcon); 150 a.recycle(); 151 152 mDialogBuilder.setIcon(dialogIcon); 153 154 final LayoutInflater inflater = dialogContext.getSystemService(LayoutInflater.class); 155 final View tv = inflater.inflate( 156 com.android.internal.R.layout.input_method_switch_dialog_title, null); 157 mDialogBuilder.setCustomTitle(tv); 158 159 // Setup layout for a toggle switch of the hardware keyboard 160 mSwitchingDialogTitleView = tv; 161 mSwitchingDialogTitleView 162 .findViewById(com.android.internal.R.id.hard_keyboard_section) 163 .setVisibility(mWindowManagerInternal.isHardKeyboardAvailable() 164 ? View.VISIBLE : View.GONE); 165 final Switch hardKeySwitch = mSwitchingDialogTitleView.findViewById( 166 com.android.internal.R.id.hard_keyboard_switch); 167 hardKeySwitch.setChecked(mShowImeWithHardKeyboard); 168 hardKeySwitch.setOnCheckedChangeListener((buttonView, isChecked) -> { 169 mSettings.setShowImeWithHardKeyboard(isChecked); 170 // Ensure that the input method dialog is dismissed when changing 171 // the hardware keyboard state. 172 hideInputMethodMenu(); 173 }); 174 175 final ImeSubtypeListAdapter adapter = new ImeSubtypeListAdapter(dialogContext, 176 com.android.internal.R.layout.input_method_switch_item, imList, checkedItem); 177 final DialogInterface.OnClickListener choiceListener = (dialog, which) -> { 178 synchronized (mMethodMap) { 179 if (mIms == null || mIms.length <= which || mSubtypeIds == null 180 || mSubtypeIds.length <= which) { 181 return; 182 } 183 final InputMethodInfo im = mIms[which]; 184 int subtypeId = mSubtypeIds[which]; 185 adapter.mCheckedItem = which; 186 adapter.notifyDataSetChanged(); 187 hideInputMethodMenu(); 188 if (im != null) { 189 if (subtypeId < 0 || subtypeId >= im.getSubtypeCount()) { 190 subtypeId = NOT_A_SUBTYPE_ID; 191 } 192 mService.setInputMethodLocked(im.getId(), subtypeId); 193 } 194 } 195 }; 196 mDialogBuilder.setSingleChoiceItems(adapter, checkedItem, choiceListener); 197 198 mSwitchingDialog = mDialogBuilder.create(); 199 mSwitchingDialog.setCanceledOnTouchOutside(true); 200 final Window w = mSwitchingDialog.getWindow(); 201 final WindowManager.LayoutParams attrs = w.getAttributes(); 202 w.setType(WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG); 203 // Use an alternate token for the dialog for that window manager can group the token 204 // with other IME windows based on type vs. grouping based on whichever token happens 205 // to get selected by the system later on. 206 attrs.token = mSwitchingDialogToken; 207 attrs.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; 208 attrs.setTitle("Select input method"); 209 w.setAttributes(attrs); 210 mService.updateSystemUiLocked(); 211 mSwitchingDialog.show(); 212 } 213 } 214 215 /** 216 * Returns the window context for IME switch dialogs to receive configuration changes. 217 * 218 * This method initializes the window context if it was not initialized. This method also moves 219 * the context to the targeted display if the current display of context is different than 220 * the display specified by {@code displayId}. 221 */ 222 @VisibleForTesting getSettingsContext(int displayId)223 public Context getSettingsContext(int displayId) { 224 if (mSettingsContext == null || mSettingsContext.getDisplayId() != displayId) { 225 final Context systemUiContext = ActivityThread.currentActivityThread() 226 .createSystemUiContext(displayId); 227 final Context windowContext = systemUiContext.createWindowContext( 228 WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG, null /* options */); 229 mSettingsContext = new ContextThemeWrapper( 230 windowContext, com.android.internal.R.style.Theme_DeviceDefault_Settings); 231 mSwitchingDialogToken = mSettingsContext.getWindowContextToken(); 232 } 233 return mSettingsContext; 234 } 235 isScreenLocked()236 private boolean isScreenLocked() { 237 return mKeyguardManager != null && mKeyguardManager.isKeyguardLocked() 238 && mKeyguardManager.isKeyguardSecure(); 239 } 240 updateKeyboardFromSettingsLocked()241 void updateKeyboardFromSettingsLocked() { 242 mShowImeWithHardKeyboard = mSettings.isShowImeWithHardKeyboardEnabled(); 243 if (mSwitchingDialog != null && mSwitchingDialogTitleView != null 244 && mSwitchingDialog.isShowing()) { 245 final Switch hardKeySwitch = mSwitchingDialogTitleView.findViewById( 246 com.android.internal.R.id.hard_keyboard_switch); 247 hardKeySwitch.setChecked(mShowImeWithHardKeyboard); 248 } 249 } 250 hideInputMethodMenu()251 void hideInputMethodMenu() { 252 synchronized (mMethodMap) { 253 hideInputMethodMenuLocked(); 254 } 255 } 256 hideInputMethodMenuLocked()257 void hideInputMethodMenuLocked() { 258 if (DEBUG) Slog.v(TAG, "Hide switching menu"); 259 260 if (mSwitchingDialog != null) { 261 mSwitchingDialog.dismiss(); 262 mSwitchingDialog = null; 263 mSwitchingDialogTitleView = null; 264 265 mService.updateSystemUiLocked(); 266 mDialogBuilder = null; 267 mIms = null; 268 } 269 } 270 getHardKeyboardListener()271 HardKeyboardListener getHardKeyboardListener() { 272 return mHardKeyboardListener; 273 } 274 getSwitchingDialogLocked()275 AlertDialog getSwitchingDialogLocked() { 276 return mSwitchingDialog; 277 } 278 getShowImeWithHardKeyboard()279 boolean getShowImeWithHardKeyboard() { 280 return mShowImeWithHardKeyboard; 281 } 282 isisInputMethodPickerShownForTestLocked()283 boolean isisInputMethodPickerShownForTestLocked() { 284 if (mSwitchingDialog == null) { 285 return false; 286 } 287 return mSwitchingDialog.isShowing(); 288 } 289 290 class HardKeyboardListener implements WindowManagerInternal.OnHardKeyboardStatusChangeListener { 291 @Override onHardKeyboardStatusChange(boolean available)292 public void onHardKeyboardStatusChange(boolean available) { 293 mHandler.sendMessage(mHandler.obtainMessage(MSG_HARD_KEYBOARD_SWITCH_CHANGED, 294 available ? 1 : 0)); 295 } 296 handleHardKeyboardStatusChange(boolean available)297 public void handleHardKeyboardStatusChange(boolean available) { 298 if (DEBUG) { 299 Slog.w(TAG, "HardKeyboardStatusChanged: available=" + available); 300 } 301 synchronized (mMethodMap) { 302 if (mSwitchingDialog != null && mSwitchingDialogTitleView != null 303 && mSwitchingDialog.isShowing()) { 304 mSwitchingDialogTitleView.findViewById( 305 com.android.internal.R.id.hard_keyboard_section).setVisibility( 306 available ? View.VISIBLE : View.GONE); 307 } 308 } 309 } 310 } 311 312 private static class ImeSubtypeListAdapter extends ArrayAdapter<ImeSubtypeListItem> { 313 private final LayoutInflater mInflater; 314 private final int mTextViewResourceId; 315 private final List<ImeSubtypeListItem> mItemsList; 316 public int mCheckedItem; ImeSubtypeListAdapter(Context context, int textViewResourceId, List<ImeSubtypeListItem> itemsList, int checkedItem)317 private ImeSubtypeListAdapter(Context context, int textViewResourceId, 318 List<ImeSubtypeListItem> itemsList, int checkedItem) { 319 super(context, textViewResourceId, itemsList); 320 321 mTextViewResourceId = textViewResourceId; 322 mItemsList = itemsList; 323 mCheckedItem = checkedItem; 324 mInflater = LayoutInflater.from(context); 325 } 326 327 @Override getView(int position, View convertView, ViewGroup parent)328 public View getView(int position, View convertView, ViewGroup parent) { 329 final View view = convertView != null ? convertView 330 : mInflater.inflate(mTextViewResourceId, null); 331 if (position < 0 || position >= mItemsList.size()) return view; 332 final ImeSubtypeListItem item = mItemsList.get(position); 333 final CharSequence imeName = item.mImeName; 334 final CharSequence subtypeName = item.mSubtypeName; 335 final TextView firstTextView = view.findViewById(android.R.id.text1); 336 final TextView secondTextView = view.findViewById(android.R.id.text2); 337 if (TextUtils.isEmpty(subtypeName)) { 338 firstTextView.setText(imeName); 339 secondTextView.setVisibility(View.GONE); 340 } else { 341 firstTextView.setText(subtypeName); 342 secondTextView.setText(imeName); 343 secondTextView.setVisibility(View.VISIBLE); 344 } 345 final RadioButton radioButton = view.findViewById(com.android.internal.R.id.radio); 346 radioButton.setChecked(position == mCheckedItem); 347 return view; 348 } 349 } 350 } 351