• 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_INDEX;
21 
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.annotation.UserIdInt;
25 import android.app.AlertDialog;
26 import android.content.Context;
27 import android.content.DialogInterface;
28 import android.content.res.TypedArray;
29 import android.graphics.drawable.Drawable;
30 import android.provider.Settings;
31 import android.text.TextUtils;
32 import android.util.Printer;
33 import android.util.Slog;
34 import android.view.LayoutInflater;
35 import android.view.View;
36 import android.view.ViewGroup;
37 import android.view.Window;
38 import android.view.WindowManager;
39 import android.view.inputmethod.InputMethodInfo;
40 import android.view.inputmethod.InputMethodSubtype;
41 import android.widget.ArrayAdapter;
42 import android.widget.RadioButton;
43 import android.widget.Switch;
44 import android.widget.TextView;
45 
46 import com.android.internal.annotations.GuardedBy;
47 import com.android.server.LocalServices;
48 import com.android.server.inputmethod.InputMethodSubtypeSwitchingController.ImeSubtypeListItem;
49 import com.android.server.wm.WindowManagerInternal;
50 
51 import java.util.List;
52 
53 /** A controller to show/hide the input method menu */
54 final class InputMethodMenuController {
55     private static final String TAG = InputMethodMenuController.class.getSimpleName();
56 
57     private final InputMethodManagerService mService;
58     private final WindowManagerInternal mWindowManagerInternal;
59 
60     private AlertDialog.Builder mDialogBuilder;
61     private AlertDialog mSwitchingDialog;
62     private View mSwitchingDialogTitleView;
63     private List<ImeSubtypeListItem> mImList;
64     private InputMethodInfo[] mIms;
65     private int[] mSubtypeIndices;
66 
67     private boolean mShowImeWithHardKeyboard;
68 
69     @GuardedBy("ImfLock.class")
70     @Nullable
71     private InputMethodDialogWindowContext mDialogWindowContext;
72 
InputMethodMenuController(InputMethodManagerService service)73     InputMethodMenuController(InputMethodManagerService service) {
74         mService = service;
75         mWindowManagerInternal = LocalServices.getService(WindowManagerInternal.class);
76     }
77 
78     @GuardedBy("ImfLock.class")
showInputMethodMenuLocked(boolean showAuxSubtypes, int displayId, String preferredInputMethodId, int preferredInputMethodSubtypeIndex, @NonNull List<ImeSubtypeListItem> imList, @UserIdInt int userId)79     void showInputMethodMenuLocked(boolean showAuxSubtypes, int displayId,
80             String preferredInputMethodId, int preferredInputMethodSubtypeIndex,
81             @NonNull List<ImeSubtypeListItem> imList, @UserIdInt int userId) {
82         if (DEBUG) Slog.v(TAG, "Show switching menu. showAuxSubtypes=" + showAuxSubtypes);
83 
84         final var bindingController = mService.getInputMethodBindingController(userId);
85 
86         hideInputMethodMenuLocked(userId);
87 
88         if (preferredInputMethodSubtypeIndex == NOT_A_SUBTYPE_INDEX) {
89             final InputMethodSubtype currentSubtype =
90                     bindingController.getCurrentInputMethodSubtype();
91             if (currentSubtype != null) {
92                 final String curMethodId = bindingController.getSelectedMethodId();
93                 final InputMethodInfo currentImi =
94                         InputMethodSettingsRepository.get(userId).getMethodMap().get(curMethodId);
95                 preferredInputMethodSubtypeIndex = SubtypeUtils.getSubtypeIndexFromHashCode(
96                         currentImi, currentSubtype.hashCode());
97             }
98         }
99 
100         // Find out which item should be checked by default.
101         final int size = imList.size();
102         mImList = imList;
103         mIms = new InputMethodInfo[size];
104         mSubtypeIndices = new int[size];
105         // No items are checked by default. When we have a list of explicitly enabled subtypes,
106         // the implicit subtype is no longer listed, but if it is still the selected one,
107         // no items will be shown as checked.
108         int checkedItem = -1;
109         for (int i = 0; i < size; ++i) {
110             final ImeSubtypeListItem item = imList.get(i);
111             mIms[i] = item.mImi;
112             mSubtypeIndices[i] = item.mSubtypeIndex;
113             if (mIms[i].getId().equals(preferredInputMethodId)) {
114                 int subtypeIndex = mSubtypeIndices[i];
115                 if ((subtypeIndex == NOT_A_SUBTYPE_INDEX)
116                         || (preferredInputMethodSubtypeIndex == NOT_A_SUBTYPE_INDEX
117                         && subtypeIndex == 0)
118                         || (subtypeIndex == preferredInputMethodSubtypeIndex)) {
119                     checkedItem = i;
120                 }
121             }
122         }
123 
124         if (checkedItem == -1) {
125             Slog.w(TAG, "Switching menu shown with no item selected"
126                     + ", IME id: " + preferredInputMethodId
127                     + ", subtype index: " + preferredInputMethodSubtypeIndex);
128         }
129 
130         if (mDialogWindowContext == null) {
131             mDialogWindowContext = new InputMethodDialogWindowContext();
132         }
133         final Context dialogWindowContext = mDialogWindowContext.get(displayId);
134         mDialogBuilder = new AlertDialog.Builder(dialogWindowContext);
135         mDialogBuilder.setOnCancelListener(dialog -> hideInputMethodMenu(userId));
136 
137         final Context dialogContext = mDialogBuilder.getContext();
138         final TypedArray a = dialogContext.obtainStyledAttributes(null,
139                 com.android.internal.R.styleable.DialogPreference,
140                 com.android.internal.R.attr.alertDialogStyle, 0);
141         final Drawable dialogIcon = a.getDrawable(
142                 com.android.internal.R.styleable.DialogPreference_dialogIcon);
143         a.recycle();
144 
145         mDialogBuilder.setIcon(dialogIcon);
146 
147         final LayoutInflater inflater = dialogContext.getSystemService(LayoutInflater.class);
148         final View tv = inflater.inflate(
149                 com.android.internal.R.layout.input_method_switch_dialog_title, null);
150         mDialogBuilder.setCustomTitle(tv);
151 
152         // Setup layout for a toggle switch of the hardware keyboard
153         mSwitchingDialogTitleView = tv;
154         mSwitchingDialogTitleView
155                 .findViewById(com.android.internal.R.id.hard_keyboard_section)
156                 .setVisibility(mWindowManagerInternal.isHardKeyboardAvailable()
157                         ? View.VISIBLE : View.GONE);
158         final Switch hardKeySwitch = mSwitchingDialogTitleView.findViewById(
159                 com.android.internal.R.id.hard_keyboard_switch);
160         hardKeySwitch.setChecked(mShowImeWithHardKeyboard);
161         hardKeySwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
162             SecureSettingsWrapper.putBoolean(Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD,
163                     isChecked, userId);
164             // Ensure that the input method dialog is dismissed when changing
165             // the hardware keyboard state.
166             hideInputMethodMenu(userId);
167         });
168 
169         // Fill the list items with onClick listener, which takes care of IME (and subtype)
170         // switching when clicked.
171         final ImeSubtypeListAdapter adapter = new ImeSubtypeListAdapter(dialogContext,
172                 com.android.internal.R.layout.input_method_switch_item, imList, checkedItem);
173         final DialogInterface.OnClickListener choiceListener = (dialog, which) -> {
174             synchronized (ImfLock.class) {
175                 if (mIms == null || mIms.length <= which || mSubtypeIndices == null
176                         || mSubtypeIndices.length <= which) {
177                     return;
178                 }
179                 final InputMethodInfo im = mIms[which];
180                 int subtypeIndex = mSubtypeIndices[which];
181                 adapter.mCheckedItem = which;
182                 adapter.notifyDataSetChanged();
183                 if (im != null) {
184                     if (subtypeIndex < 0 || subtypeIndex >= im.getSubtypeCount()) {
185                         subtypeIndex = NOT_A_SUBTYPE_INDEX;
186                     }
187                     mService.setInputMethodLocked(im.getId(), subtypeIndex, userId);
188                 }
189                 hideInputMethodMenuLocked(userId);
190             }
191         };
192         mDialogBuilder.setSingleChoiceItems(adapter, checkedItem, choiceListener);
193 
194         // Final steps to instantiate a dialog to show it up.
195         mSwitchingDialog = mDialogBuilder.create();
196         mSwitchingDialog.setCanceledOnTouchOutside(true);
197         final Window w = mSwitchingDialog.getWindow();
198         final WindowManager.LayoutParams attrs = w.getAttributes();
199         w.setType(WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG);
200         w.setHideOverlayWindows(true);
201         // Use an alternate token for the dialog for that window manager can group the token
202         // with other IME windows based on type vs. grouping based on whichever token happens
203         // to get selected by the system later on.
204         attrs.token = dialogWindowContext.getWindowContextToken();
205         attrs.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
206         attrs.setTitle("Select input method");
207         w.setAttributes(attrs);
208         mService.updateSystemUiLocked(userId);
209         mService.sendOnNavButtonFlagsChangedLocked(mService.getUserData(userId));
210         mSwitchingDialog.show();
211     }
212 
updateKeyboardFromSettingsLocked(@serIdInt int userId)213     void updateKeyboardFromSettingsLocked(@UserIdInt int userId) {
214         mShowImeWithHardKeyboard =
215                 SecureSettingsWrapper.getBoolean(Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD,
216                         false, userId);
217         if (mSwitchingDialog != null && mSwitchingDialogTitleView != null
218                 && mSwitchingDialog.isShowing()) {
219             final Switch hardKeySwitch = mSwitchingDialogTitleView.findViewById(
220                     com.android.internal.R.id.hard_keyboard_switch);
221             hardKeySwitch.setChecked(mShowImeWithHardKeyboard);
222         }
223     }
224 
225     /**
226      * Hides the input method switcher menu.
227      *
228      * @param userId user ID for this operation
229      */
hideInputMethodMenu(@serIdInt int userId)230     void hideInputMethodMenu(@UserIdInt int userId) {
231         synchronized (ImfLock.class) {
232             hideInputMethodMenuLocked(userId);
233         }
234     }
235 
236     /**
237      * Hides the input method switcher menu, synchronised version of {@link #hideInputMethodMenu}.
238      *
239      * @param userId user ID for this operation
240      */
241     @GuardedBy("ImfLock.class")
hideInputMethodMenuLocked(@serIdInt int userId)242     void hideInputMethodMenuLocked(@UserIdInt int userId) {
243         if (DEBUG) Slog.v(TAG, "Hide switching menu");
244 
245         if (mSwitchingDialog != null) {
246             mSwitchingDialog.dismiss();
247             mSwitchingDialog = null;
248             mSwitchingDialogTitleView = null;
249 
250             mService.updateSystemUiLocked(userId);
251             mService.sendOnNavButtonFlagsChangedToAllImesLocked();
252             mDialogBuilder = null;
253             mImList = null;
254             mIms = null;
255             mSubtypeIndices = null;
256         }
257     }
258 
getSwitchingDialogLocked()259     AlertDialog getSwitchingDialogLocked() {
260         return mSwitchingDialog;
261     }
262 
getShowImeWithHardKeyboard()263     boolean getShowImeWithHardKeyboard() {
264         return mShowImeWithHardKeyboard;
265     }
266 
isisInputMethodPickerShownForTestLocked()267     boolean isisInputMethodPickerShownForTestLocked() {
268         if (mSwitchingDialog == null) {
269             return false;
270         }
271         return mSwitchingDialog.isShowing();
272     }
273 
handleHardKeyboardStatusChange(boolean available)274     void handleHardKeyboardStatusChange(boolean available) {
275         if (DEBUG) {
276             Slog.w(TAG, "HardKeyboardStatusChanged: available=" + available);
277         }
278         synchronized (ImfLock.class) {
279             if (mSwitchingDialog != null && mSwitchingDialogTitleView != null
280                     && mSwitchingDialog.isShowing()) {
281                 mSwitchingDialogTitleView.findViewById(
282                         com.android.internal.R.id.hard_keyboard_section).setVisibility(
283                         available ? View.VISIBLE : View.GONE);
284             }
285         }
286     }
287 
dump(@onNull Printer pw, @NonNull String prefix)288     void dump(@NonNull Printer pw, @NonNull String prefix) {
289         final boolean showing = isisInputMethodPickerShownForTestLocked();
290         pw.println(prefix + "isShowing: " + showing);
291 
292         if (showing) {
293             pw.println(prefix + "imList: " + mImList);
294         }
295     }
296 
297     private static class ImeSubtypeListAdapter extends ArrayAdapter<ImeSubtypeListItem> {
298         private final LayoutInflater mInflater;
299         private final int mTextViewResourceId;
300         private final List<ImeSubtypeListItem> mItemsList;
301         public int mCheckedItem;
ImeSubtypeListAdapter(Context context, int textViewResourceId, List<ImeSubtypeListItem> itemsList, int checkedItem)302         private ImeSubtypeListAdapter(Context context, int textViewResourceId,
303                 List<ImeSubtypeListItem> itemsList, int checkedItem) {
304             super(context, textViewResourceId, itemsList);
305 
306             mTextViewResourceId = textViewResourceId;
307             mItemsList = itemsList;
308             mCheckedItem = checkedItem;
309             mInflater = LayoutInflater.from(context);
310         }
311 
312         @Override
getView(int position, View convertView, ViewGroup parent)313         public View getView(int position, View convertView, ViewGroup parent) {
314             final View view = convertView != null ? convertView
315                     : mInflater.inflate(mTextViewResourceId, null);
316             if (position < 0 || position >= mItemsList.size()) return view;
317             final ImeSubtypeListItem item = mItemsList.get(position);
318             final CharSequence imeName = item.mImeName;
319             final CharSequence subtypeName = item.mSubtypeName;
320             final TextView firstTextView = view.findViewById(android.R.id.text1);
321             final TextView secondTextView = view.findViewById(android.R.id.text2);
322             if (TextUtils.isEmpty(subtypeName)) {
323                 firstTextView.setText(imeName);
324                 secondTextView.setVisibility(View.GONE);
325             } else {
326                 firstTextView.setText(subtypeName);
327                 secondTextView.setText(imeName);
328                 secondTextView.setVisibility(View.VISIBLE);
329             }
330             final RadioButton radioButton = view.findViewById(com.android.internal.R.id.radio);
331             radioButton.setChecked(position == mCheckedItem);
332             return view;
333         }
334     }
335 }
336