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