• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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