1 /*
2  * Copyright (C) 2016 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.settings.inputmethod;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.app.Activity;
22 import android.app.LoaderManager;
23 import android.content.AsyncTaskLoader;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.Loader;
27 import android.database.ContentObserver;
28 import android.hardware.input.InputDeviceIdentifier;
29 import android.hardware.input.InputManager;
30 import android.hardware.input.KeyboardLayout;
31 import android.os.Bundle;
32 import android.os.Handler;
33 import android.os.UserHandle;
34 import android.provider.Settings.Secure;
35 import android.support.v14.preference.SwitchPreference;
36 import android.support.v7.preference.Preference;
37 import android.support.v7.preference.Preference.OnPreferenceChangeListener;
38 import android.support.v7.preference.PreferenceCategory;
39 import android.support.v7.preference.PreferenceScreen;
40 import android.text.TextUtils;
41 import android.view.InputDevice;
42 import android.view.inputmethod.InputMethodInfo;
43 import android.view.inputmethod.InputMethodManager;
44 import android.view.inputmethod.InputMethodSubtype;
45 
46 import com.android.internal.inputmethod.InputMethodUtils;
47 import com.android.internal.logging.MetricsProto.MetricsEvent;
48 import com.android.internal.util.Preconditions;
49 import com.android.settings.R;
50 import com.android.settings.Settings;
51 import com.android.settings.SettingsPreferenceFragment;
52 
53 import java.text.Collator;
54 import java.util.ArrayList;
55 import java.util.Collections;
56 import java.util.HashMap;
57 import java.util.HashSet;
58 import java.util.List;
59 import java.util.Objects;
60 
61 public final class PhysicalKeyboardFragment extends SettingsPreferenceFragment
62         implements InputManager.InputDeviceListener {
63 
64     private static final String KEYBOARD_ASSISTANCE_CATEGORY = "keyboard_assistance_category";
65     private static final String SHOW_VIRTUAL_KEYBOARD_SWITCH = "show_virtual_keyboard_switch";
66     private static final String KEYBOARD_SHORTCUTS_HELPER = "keyboard_shortcuts_helper";
67     private static final String IM_SUBTYPE_MODE_KEYBOARD = "keyboard";
68 
69     @NonNull
70     private final List<HardKeyboardDeviceInfo> mLastHardKeyboards = new ArrayList<>();
71     @NonNull
72     private final List<KeyboardInfoPreference> mTempKeyboardInfoList = new ArrayList<>();
73 
74     @NonNull
75     private final HashSet<Integer> mLoaderIDs = new HashSet<>();
76     private int mNextLoaderId = 0;
77 
78     private InputManager mIm;
79     @NonNull
80     private PreferenceCategory mKeyboardAssistanceCategory;
81     @NonNull
82     private SwitchPreference mShowVirtualKeyboardSwitch;
83     @NonNull
84     private InputMethodUtils.InputMethodSettings mSettings;
85 
86     @Override
onCreatePreferences(Bundle bundle, String s)87     public void onCreatePreferences(Bundle bundle, String s) {
88         Activity activity = Preconditions.checkNotNull(getActivity());
89         addPreferencesFromResource(R.xml.physical_keyboard_settings);
90         mIm = Preconditions.checkNotNull(activity.getSystemService(InputManager.class));
91         mSettings = new InputMethodUtils.InputMethodSettings(
92                 activity.getResources(),
93                 getContentResolver(),
94                 new HashMap<>(),
95                 new ArrayList<>(),
96                 UserHandle.myUserId(),
97                 false /* copyOnWrite */);
98         mKeyboardAssistanceCategory = Preconditions.checkNotNull(
99                 (PreferenceCategory) findPreference(KEYBOARD_ASSISTANCE_CATEGORY));
100         mShowVirtualKeyboardSwitch = Preconditions.checkNotNull(
101                 (SwitchPreference) mKeyboardAssistanceCategory.findPreference(
102                         SHOW_VIRTUAL_KEYBOARD_SWITCH));
103         findPreference(KEYBOARD_SHORTCUTS_HELPER).setOnPreferenceClickListener(
104                 new Preference.OnPreferenceClickListener() {
105                     @Override
106                     public boolean onPreferenceClick(Preference preference) {
107                         toggleKeyboardShortcutsMenu();
108                         return true;
109                     }
110                 });
111     }
112 
113     @Override
onResume()114     public void onResume() {
115         super.onResume();
116         clearLoader();
117         mLastHardKeyboards.clear();
118         updateHardKeyboards();
119         mIm.registerInputDeviceListener(this, null);
120         mShowVirtualKeyboardSwitch.setOnPreferenceChangeListener(
121                 mShowVirtualKeyboardSwitchPreferenceChangeListener);
122         registerShowVirtualKeyboardSettingsObserver();
123     }
124 
125     @Override
onPause()126     public void onPause() {
127         super.onPause();
128         clearLoader();
129         mLastHardKeyboards.clear();
130         mIm.unregisterInputDeviceListener(this);
131         mShowVirtualKeyboardSwitch.setOnPreferenceChangeListener(null);
132         unregisterShowVirtualKeyboardSettingsObserver();
133     }
134 
onLoadFinishedInternal( final int loaderId, @NonNull final List<Keyboards> keyboardsList)135     public void onLoadFinishedInternal(
136             final int loaderId, @NonNull final List<Keyboards> keyboardsList) {
137         if (!mLoaderIDs.remove(loaderId)) {
138             // Already destroyed loader.  Ignore.
139             return;
140         }
141 
142         Collections.sort(keyboardsList);
143         final PreferenceScreen preferenceScreen = getPreferenceScreen();
144         preferenceScreen.removeAll();
145         for (Keyboards keyboards : keyboardsList) {
146             final PreferenceCategory category = new PreferenceCategory(getPrefContext(), null);
147             category.setTitle(keyboards.mDeviceInfo.mDeviceName);
148             category.setOrder(0);
149             preferenceScreen.addPreference(category);
150             for (Keyboards.KeyboardInfo info : keyboards.mKeyboardInfoList) {
151                 mTempKeyboardInfoList.clear();
152                 final InputMethodInfo imi = info.mImi;
153                 final InputMethodSubtype imSubtype = info.mImSubtype;
154                 if (imi != null) {
155                     KeyboardInfoPreference pref =
156                             new KeyboardInfoPreference(getPrefContext(), info);
157                     pref.setOnPreferenceClickListener(preference -> {
158                         showKeyboardLayoutScreen(
159                                 keyboards.mDeviceInfo.mDeviceIdentifier, imi, imSubtype);
160                         return true;
161                     });
162                     mTempKeyboardInfoList.add(pref);
163                     Collections.sort(mTempKeyboardInfoList);
164                 }
165                 for (KeyboardInfoPreference pref : mTempKeyboardInfoList) {
166                     category.addPreference(pref);
167                 }
168             }
169         }
170         mTempKeyboardInfoList.clear();
171         mKeyboardAssistanceCategory.setOrder(1);
172         preferenceScreen.addPreference(mKeyboardAssistanceCategory);
173         updateShowVirtualKeyboardSwitch();
174     }
175 
176     @Override
onInputDeviceAdded(int deviceId)177     public void onInputDeviceAdded(int deviceId) {
178         updateHardKeyboards();
179     }
180 
181     @Override
onInputDeviceRemoved(int deviceId)182     public void onInputDeviceRemoved(int deviceId) {
183         updateHardKeyboards();
184     }
185 
186     @Override
onInputDeviceChanged(int deviceId)187     public void onInputDeviceChanged(int deviceId) {
188         updateHardKeyboards();
189     }
190 
191     @Override
getMetricsCategory()192     protected int getMetricsCategory() {
193         return MetricsEvent.PHYSICAL_KEYBOARDS;
194     }
195 
196     @NonNull
getHardKeyboards()197     private static ArrayList<HardKeyboardDeviceInfo> getHardKeyboards() {
198         final ArrayList<HardKeyboardDeviceInfo> keyboards = new ArrayList<>();
199         final int[] devicesIds = InputDevice.getDeviceIds();
200         for (int deviceId : devicesIds) {
201             final InputDevice device = InputDevice.getDevice(deviceId);
202             if (device != null && !device.isVirtual() && device.isFullKeyboard()) {
203                 keyboards.add(new HardKeyboardDeviceInfo(device.getName(), device.getIdentifier()));
204             }
205         }
206         return keyboards;
207     }
208 
updateHardKeyboards()209     private void updateHardKeyboards() {
210         final ArrayList<HardKeyboardDeviceInfo> newHardKeyboards = getHardKeyboards();
211         if (!Objects.equals(newHardKeyboards, mLastHardKeyboards)) {
212             clearLoader();
213             mLastHardKeyboards.clear();
214             mLastHardKeyboards.addAll(newHardKeyboards);
215             getLoaderManager().initLoader(mNextLoaderId, null,
216                     new Callbacks(getContext(), this, mLastHardKeyboards));
217             mLoaderIDs.add(mNextLoaderId);
218             ++mNextLoaderId;
219         }
220     }
221 
showKeyboardLayoutScreen( @onNull InputDeviceIdentifier inputDeviceIdentifier, @NonNull InputMethodInfo imi, @Nullable InputMethodSubtype imSubtype)222     private void showKeyboardLayoutScreen(
223             @NonNull InputDeviceIdentifier inputDeviceIdentifier,
224             @NonNull InputMethodInfo imi,
225             @Nullable InputMethodSubtype imSubtype) {
226         final Intent intent = new Intent(Intent.ACTION_MAIN);
227         intent.setClass(getActivity(), Settings.KeyboardLayoutPickerActivity.class);
228         intent.putExtra(KeyboardLayoutPickerFragment2.EXTRA_INPUT_DEVICE_IDENTIFIER,
229                 inputDeviceIdentifier);
230         intent.putExtra(KeyboardLayoutPickerFragment2.EXTRA_INPUT_METHOD_INFO, imi);
231         intent.putExtra(KeyboardLayoutPickerFragment2.EXTRA_INPUT_METHOD_SUBTYPE, imSubtype);
232         startActivity(intent);
233     }
234 
clearLoader()235     private void clearLoader() {
236         for (final int loaderId : mLoaderIDs) {
237             getLoaderManager().destroyLoader(loaderId);
238         }
239         mLoaderIDs.clear();
240     }
241 
registerShowVirtualKeyboardSettingsObserver()242     private void registerShowVirtualKeyboardSettingsObserver() {
243         unregisterShowVirtualKeyboardSettingsObserver();
244         getActivity().getContentResolver().registerContentObserver(
245                 Secure.getUriFor(Secure.SHOW_IME_WITH_HARD_KEYBOARD),
246                 false,
247                 mContentObserver,
248                 UserHandle.myUserId());
249         updateShowVirtualKeyboardSwitch();
250     }
251 
unregisterShowVirtualKeyboardSettingsObserver()252     private void unregisterShowVirtualKeyboardSettingsObserver() {
253         getActivity().getContentResolver().unregisterContentObserver(mContentObserver);
254     }
255 
updateShowVirtualKeyboardSwitch()256     private void updateShowVirtualKeyboardSwitch() {
257         mShowVirtualKeyboardSwitch.setChecked(mSettings.isShowImeWithHardKeyboardEnabled());
258     }
259 
toggleKeyboardShortcutsMenu()260     private void toggleKeyboardShortcutsMenu() {
261         getActivity().requestShowKeyboardShortcuts();
262     }
263 
264     private final OnPreferenceChangeListener mShowVirtualKeyboardSwitchPreferenceChangeListener =
265             new OnPreferenceChangeListener() {
266                 @Override
267                 public boolean onPreferenceChange(Preference preference, Object newValue) {
268                     mSettings.setShowImeWithHardKeyboard((Boolean) newValue);
269                     return false;
270                 }
271             };
272 
273     private final ContentObserver mContentObserver = new ContentObserver(new Handler(true)) {
274         @Override
275         public void onChange(boolean selfChange) {
276             updateShowVirtualKeyboardSwitch();
277         }
278     };
279 
280     private static final class Callbacks implements LoaderManager.LoaderCallbacks<List<Keyboards>> {
281         @NonNull
282         final Context mContext;
283         @NonNull
284         final PhysicalKeyboardFragment mPhysicalKeyboardFragment;
285         @NonNull
286         final List<HardKeyboardDeviceInfo> mHardKeyboards;
Callbacks( @onNull Context context, @NonNull PhysicalKeyboardFragment physicalKeyboardFragment, @NonNull List<HardKeyboardDeviceInfo> hardKeyboards)287         public Callbacks(
288                 @NonNull Context context,
289                 @NonNull PhysicalKeyboardFragment physicalKeyboardFragment,
290                 @NonNull List<HardKeyboardDeviceInfo> hardKeyboards) {
291             mContext = context;
292             mPhysicalKeyboardFragment = physicalKeyboardFragment;
293             mHardKeyboards = hardKeyboards;
294         }
295 
296         @Override
onCreateLoader(int id, Bundle args)297         public Loader<List<Keyboards>> onCreateLoader(int id, Bundle args) {
298             return new KeyboardLayoutLoader(mContext, mHardKeyboards);
299         }
300 
301         @Override
onLoadFinished(Loader<List<Keyboards>> loader, List<Keyboards> data)302         public void onLoadFinished(Loader<List<Keyboards>> loader, List<Keyboards> data) {
303             mPhysicalKeyboardFragment.onLoadFinishedInternal(loader.getId(), data);
304         }
305 
306         @Override
onLoaderReset(Loader<List<Keyboards>> loader)307         public void onLoaderReset(Loader<List<Keyboards>> loader) {
308         }
309     }
310 
311     private static final class KeyboardLayoutLoader extends AsyncTaskLoader<List<Keyboards>> {
312         @NonNull
313         private final List<HardKeyboardDeviceInfo> mHardKeyboards;
314 
KeyboardLayoutLoader( @onNull Context context, @NonNull List<HardKeyboardDeviceInfo> hardKeyboards)315         public KeyboardLayoutLoader(
316                 @NonNull Context context,
317                 @NonNull List<HardKeyboardDeviceInfo> hardKeyboards) {
318             super(context);
319             mHardKeyboards = Preconditions.checkNotNull(hardKeyboards);
320         }
321 
loadInBackground(HardKeyboardDeviceInfo deviceInfo)322         private Keyboards loadInBackground(HardKeyboardDeviceInfo deviceInfo) {
323             final ArrayList<Keyboards.KeyboardInfo> keyboardInfoList = new ArrayList<>();
324             final InputMethodManager imm = getContext().getSystemService(InputMethodManager.class);
325             final InputManager im = getContext().getSystemService(InputManager.class);
326             if (imm != null && im != null) {
327                 for (InputMethodInfo imi : imm.getEnabledInputMethodList()) {
328                     final List<InputMethodSubtype> subtypes = imm.getEnabledInputMethodSubtypeList(
329                             imi, true /* allowsImplicitlySelectedSubtypes */);
330                     if (subtypes.isEmpty()) {
331                         // Here we use null to indicate that this IME has no subtype.
332                         final InputMethodSubtype nullSubtype = null;
333                         final KeyboardLayout layout = im.getKeyboardLayoutForInputDevice(
334                                 deviceInfo.mDeviceIdentifier, imi, nullSubtype);
335                         keyboardInfoList.add(new Keyboards.KeyboardInfo(imi, nullSubtype, layout));
336                         continue;
337                     }
338 
339                     // If the IME supports subtypes, we pick up "keyboard" subtypes only.
340                     final int N = subtypes.size();
341                     for (int i = 0; i < N; ++i) {
342                         final InputMethodSubtype subtype = subtypes.get(i);
343                         if (!IM_SUBTYPE_MODE_KEYBOARD.equalsIgnoreCase(subtype.getMode())) {
344                             continue;
345                         }
346                         final KeyboardLayout layout = im.getKeyboardLayoutForInputDevice(
347                                 deviceInfo.mDeviceIdentifier, imi, subtype);
348                         keyboardInfoList.add(new Keyboards.KeyboardInfo(imi, subtype, layout));
349                     }
350                 }
351             }
352             return new Keyboards(deviceInfo, keyboardInfoList);
353         }
354 
355         @Override
loadInBackground()356         public List<Keyboards> loadInBackground() {
357             List<Keyboards> keyboardsList = new ArrayList<>(mHardKeyboards.size());
358             for (HardKeyboardDeviceInfo deviceInfo : mHardKeyboards) {
359                 keyboardsList.add(loadInBackground(deviceInfo));
360             }
361             return keyboardsList;
362         }
363 
364         @Override
onStartLoading()365         protected void onStartLoading() {
366             super.onStartLoading();
367             forceLoad();
368         }
369 
370         @Override
onStopLoading()371         protected void onStopLoading() {
372             super.onStopLoading();
373             cancelLoad();
374         }
375     }
376 
377     public static final class HardKeyboardDeviceInfo {
378         @NonNull
379         public final String mDeviceName;
380         @NonNull
381         public final InputDeviceIdentifier mDeviceIdentifier;
382 
HardKeyboardDeviceInfo( @ullable final String deviceName, @NonNull final InputDeviceIdentifier deviceIdentifier)383         public HardKeyboardDeviceInfo(
384                 @Nullable final String deviceName,
385                 @NonNull final InputDeviceIdentifier deviceIdentifier) {
386             mDeviceName = deviceName != null ? deviceName : "";
387             mDeviceIdentifier = deviceIdentifier;
388         }
389 
390         @Override
equals(Object o)391         public boolean equals(Object o) {
392             if (o == this) return true;
393             if (o == null) return false;
394 
395             if (!(o instanceof HardKeyboardDeviceInfo)) return false;
396 
397             final HardKeyboardDeviceInfo that = (HardKeyboardDeviceInfo) o;
398             if (!TextUtils.equals(mDeviceName, that.mDeviceName)) {
399                 return false;
400             }
401             if (mDeviceIdentifier.getVendorId() != that.mDeviceIdentifier.getVendorId()) {
402                 return false;
403             }
404             if (mDeviceIdentifier.getProductId() != that.mDeviceIdentifier.getProductId()) {
405                 return false;
406             }
407             if (!TextUtils.equals(mDeviceIdentifier.getDescriptor(),
408                     that.mDeviceIdentifier.getDescriptor())) {
409                 return false;
410             }
411 
412             return true;
413         }
414     }
415 
416     public static final class Keyboards implements Comparable<Keyboards> {
417         @NonNull
418         public final HardKeyboardDeviceInfo mDeviceInfo;
419         @NonNull
420         public final ArrayList<KeyboardInfo> mKeyboardInfoList;
421         @NonNull
422         public final Collator mCollator = Collator.getInstance();
423 
Keyboards( @onNull final HardKeyboardDeviceInfo deviceInfo, @NonNull final ArrayList<KeyboardInfo> keyboardInfoList)424         public Keyboards(
425                 @NonNull final HardKeyboardDeviceInfo deviceInfo,
426                 @NonNull final ArrayList<KeyboardInfo> keyboardInfoList) {
427             mDeviceInfo = deviceInfo;
428             mKeyboardInfoList = keyboardInfoList;
429         }
430 
431         @Override
compareTo(@onNull Keyboards another)432         public int compareTo(@NonNull Keyboards another) {
433             return mCollator.compare(mDeviceInfo.mDeviceName, another.mDeviceInfo.mDeviceName);
434         }
435 
436         public static final class KeyboardInfo {
437             @NonNull
438             public final InputMethodInfo mImi;
439             @Nullable
440             public final InputMethodSubtype mImSubtype;
441             @NonNull
442             public final KeyboardLayout mLayout;
443 
KeyboardInfo( @onNull final InputMethodInfo imi, @Nullable final InputMethodSubtype imSubtype, @NonNull final KeyboardLayout layout)444             public KeyboardInfo(
445                     @NonNull final InputMethodInfo imi,
446                     @Nullable final InputMethodSubtype imSubtype,
447                     @NonNull final KeyboardLayout layout) {
448                 mImi = imi;
449                 mImSubtype = imSubtype;
450                 mLayout = layout;
451             }
452         }
453     }
454 
455     static final class KeyboardInfoPreference extends Preference {
456 
457         @NonNull
458         private final CharSequence mImeName;
459         @Nullable
460         private final CharSequence mImSubtypeName;
461         @NonNull
462         private final Collator collator = Collator.getInstance();
463 
KeyboardInfoPreference( @onNull Context context, @NonNull Keyboards.KeyboardInfo info)464         private KeyboardInfoPreference(
465                 @NonNull Context context, @NonNull Keyboards.KeyboardInfo info) {
466             super(context);
467             mImeName = info.mImi.loadLabel(context.getPackageManager());
468             mImSubtypeName = getImSubtypeName(context, info.mImi, info.mImSubtype);
469             setTitle(formatDisplayName(context, mImeName, mImSubtypeName));
470             if (info.mLayout != null) {
471                 setSummary(info.mLayout.getLabel());
472             }
473         }
474 
475         @NonNull
getDisplayName( @onNull Context context, @NonNull InputMethodInfo imi, @Nullable InputMethodSubtype imSubtype)476         static CharSequence getDisplayName(
477                 @NonNull Context context, @NonNull InputMethodInfo imi,
478                 @Nullable InputMethodSubtype imSubtype) {
479             final CharSequence imeName = imi.loadLabel(context.getPackageManager());
480             final CharSequence imSubtypeName = getImSubtypeName(context, imi, imSubtype);
481             return formatDisplayName(context, imeName, imSubtypeName);
482         }
483 
formatDisplayName( @onNull Context context, @NonNull CharSequence imeName, @Nullable CharSequence imSubtypeName)484         private static CharSequence formatDisplayName(
485                 @NonNull Context context,
486                 @NonNull CharSequence imeName, @Nullable CharSequence imSubtypeName) {
487             if (imSubtypeName == null) {
488                 return imeName;
489             }
490             return String.format(
491                     context.getString(R.string.physical_device_title), imeName, imSubtypeName);
492         }
493 
494         @Nullable
getImSubtypeName( @onNull Context context, @NonNull InputMethodInfo imi, @Nullable InputMethodSubtype imSubtype)495         private static CharSequence getImSubtypeName(
496                 @NonNull Context context, @NonNull InputMethodInfo imi,
497                 @Nullable InputMethodSubtype imSubtype) {
498             if (imSubtype != null) {
499                 return InputMethodAndSubtypeUtil.getSubtypeLocaleNameAsSentence(
500                         imSubtype, context, imi);
501             }
502             return null;
503         }
504 
505         @Override
compareTo(@onNull Preference object)506         public int compareTo(@NonNull Preference object) {
507             if (!(object instanceof KeyboardInfoPreference)) {
508                 return super.compareTo(object);
509             }
510             KeyboardInfoPreference another = (KeyboardInfoPreference) object;
511             int result = compare(mImeName, another.mImeName);
512             if (result == 0) {
513                 result = compare(mImSubtypeName, another.mImSubtypeName);
514             }
515             return result;
516         }
517 
compare(@ullable CharSequence lhs, @Nullable CharSequence rhs)518         private int compare(@Nullable CharSequence lhs, @Nullable CharSequence rhs) {
519             if (!TextUtils.isEmpty(lhs) && !TextUtils.isEmpty(rhs)) {
520                 return collator.compare(lhs.toString(), rhs.toString());
521             } else if (TextUtils.isEmpty(lhs) && TextUtils.isEmpty(rhs)) {
522                 return 0;
523             } else if (!TextUtils.isEmpty(lhs)) {
524                 return -1;
525             } else {
526                 return 1;
527             }
528         }
529     }
530 
531 }
532