• 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.NonNull;
23 import android.annotation.Nullable;
24 import android.app.AlertDialog;
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.provider.Settings;
30 import android.text.TextUtils;
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 WindowManagerInternal mWindowManagerInternal;
57 
58     private AlertDialog.Builder mDialogBuilder;
59     private AlertDialog mSwitchingDialog;
60     private View mSwitchingDialogTitleView;
61     private InputMethodInfo[] mIms;
62     private int[] mSubtypeIds;
63 
64     private boolean mShowImeWithHardKeyboard;
65 
66     @GuardedBy("ImfLock.class")
67     @Nullable
68     private InputMethodDialogWindowContext mDialogWindowContext;
69 
InputMethodMenuController(InputMethodManagerService service)70     InputMethodMenuController(InputMethodManagerService service) {
71         mService = service;
72         mWindowManagerInternal = LocalServices.getService(WindowManagerInternal.class);
73     }
74 
75     @GuardedBy("ImfLock.class")
showInputMethodMenuLocked(boolean showAuxSubtypes, int displayId, String preferredInputMethodId, int preferredInputMethodSubtypeId, @NonNull List<ImeSubtypeListItem> imList)76     void showInputMethodMenuLocked(boolean showAuxSubtypes, int displayId,
77             String preferredInputMethodId, int preferredInputMethodSubtypeId,
78             @NonNull List<ImeSubtypeListItem> imList) {
79         if (DEBUG) Slog.v(TAG, "Show switching menu. showAuxSubtypes=" + showAuxSubtypes);
80 
81         final int userId = mService.getCurrentImeUserIdLocked();
82 
83         hideInputMethodMenuLocked();
84 
85         if (preferredInputMethodSubtypeId == NOT_A_SUBTYPE_ID) {
86             final InputMethodSubtype currentSubtype =
87                     mService.getCurrentInputMethodSubtypeLocked();
88             if (currentSubtype != null) {
89                 final String curMethodId = mService.getSelectedMethodIdLocked();
90                 final InputMethodInfo currentImi =
91                         mService.queryInputMethodForCurrentUserLocked(curMethodId);
92                 preferredInputMethodSubtypeId = SubtypeUtils.getSubtypeIdFromHashCode(
93                         currentImi, currentSubtype.hashCode());
94             }
95         }
96 
97         // Find out which item should be checked by default.
98         final int size = imList.size();
99         mIms = new InputMethodInfo[size];
100         mSubtypeIds = new int[size];
101         // No items are checked by default. When we have a list of explicitly enabled subtypes,
102         // the implicit subtype is no longer listed, but if it is still the selected one,
103         // no items will be shown as checked.
104         int checkedItem = -1;
105         for (int i = 0; i < size; ++i) {
106             final ImeSubtypeListItem item = imList.get(i);
107             mIms[i] = item.mImi;
108             mSubtypeIds[i] = item.mSubtypeId;
109             if (mIms[i].getId().equals(preferredInputMethodId)) {
110                 int subtypeId = mSubtypeIds[i];
111                 if ((subtypeId == NOT_A_SUBTYPE_ID)
112                         || (preferredInputMethodSubtypeId == NOT_A_SUBTYPE_ID && subtypeId == 0)
113                         || (subtypeId == preferredInputMethodSubtypeId)) {
114                     checkedItem = i;
115                 }
116             }
117         }
118 
119         if (checkedItem == -1) {
120             Slog.w(TAG, "Switching menu shown with no item selected"
121                     + ", IME id: " + preferredInputMethodId
122                     + ", subtype index: " + preferredInputMethodSubtypeId);
123         }
124 
125         if (mDialogWindowContext == null) {
126             mDialogWindowContext = new InputMethodDialogWindowContext();
127         }
128         final Context dialogWindowContext = mDialogWindowContext.get(displayId);
129         mDialogBuilder = new AlertDialog.Builder(dialogWindowContext);
130         mDialogBuilder.setOnCancelListener(dialog -> hideInputMethodMenu());
131 
132         final Context dialogContext = mDialogBuilder.getContext();
133         final TypedArray a = dialogContext.obtainStyledAttributes(null,
134                 com.android.internal.R.styleable.DialogPreference,
135                 com.android.internal.R.attr.alertDialogStyle, 0);
136         final Drawable dialogIcon = a.getDrawable(
137                 com.android.internal.R.styleable.DialogPreference_dialogIcon);
138         a.recycle();
139 
140         mDialogBuilder.setIcon(dialogIcon);
141 
142         final LayoutInflater inflater = dialogContext.getSystemService(LayoutInflater.class);
143         final View tv = inflater.inflate(
144                 com.android.internal.R.layout.input_method_switch_dialog_title, null);
145         mDialogBuilder.setCustomTitle(tv);
146 
147         // Setup layout for a toggle switch of the hardware keyboard
148         mSwitchingDialogTitleView = tv;
149         mSwitchingDialogTitleView
150                 .findViewById(com.android.internal.R.id.hard_keyboard_section)
151                 .setVisibility(mWindowManagerInternal.isHardKeyboardAvailable()
152                         ? View.VISIBLE : View.GONE);
153         final Switch hardKeySwitch = mSwitchingDialogTitleView.findViewById(
154                 com.android.internal.R.id.hard_keyboard_switch);
155         hardKeySwitch.setChecked(mShowImeWithHardKeyboard);
156         hardKeySwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
157             SecureSettingsWrapper.putBoolean(Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD,
158                     isChecked, userId);
159             // Ensure that the input method dialog is dismissed when changing
160             // the hardware keyboard state.
161             hideInputMethodMenu();
162         });
163 
164         // Fill the list items with onClick listener, which takes care of IME (and subtype)
165         // switching when clicked.
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         // Final steps to instantiate a dialog to show it up.
190         mSwitchingDialog = mDialogBuilder.create();
191         mSwitchingDialog.setCanceledOnTouchOutside(true);
192         final Window w = mSwitchingDialog.getWindow();
193         final WindowManager.LayoutParams attrs = w.getAttributes();
194         w.setType(WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG);
195         w.setHideOverlayWindows(true);
196         // Use an alternate token for the dialog for that window manager can group the token
197         // with other IME windows based on type vs. grouping based on whichever token happens
198         // to get selected by the system later on.
199         attrs.token = dialogWindowContext.getWindowContextToken();
200         attrs.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
201         attrs.setTitle("Select input method");
202         w.setAttributes(attrs);
203         mService.updateSystemUiLocked();
204         mService.sendOnNavButtonFlagsChangedLocked();
205         mSwitchingDialog.show();
206     }
207 
updateKeyboardFromSettingsLocked()208     void updateKeyboardFromSettingsLocked() {
209         mShowImeWithHardKeyboard =
210                 SecureSettingsWrapper.getBoolean(Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD,
211                         false, mService.getCurrentImeUserIdLocked());
212         if (mSwitchingDialog != null && mSwitchingDialogTitleView != null
213                 && mSwitchingDialog.isShowing()) {
214             final Switch hardKeySwitch = mSwitchingDialogTitleView.findViewById(
215                     com.android.internal.R.id.hard_keyboard_switch);
216             hardKeySwitch.setChecked(mShowImeWithHardKeyboard);
217         }
218     }
219 
220     /**
221      * Hides the input method switcher menu.
222      */
hideInputMethodMenu()223     void hideInputMethodMenu() {
224         synchronized (ImfLock.class) {
225             hideInputMethodMenuLocked();
226         }
227     }
228 
229     /**
230      * Hides the input method switcher menu, synchronised version of {@link #hideInputMethodMenu}.
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