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