• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.settings.SettingsEnums;
23 import android.content.ContentResolver;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.database.ContentObserver;
27 import android.hardware.input.InputDeviceIdentifier;
28 import android.hardware.input.InputManager;
29 import android.hardware.input.KeyboardLayout;
30 import android.os.Bundle;
31 import android.os.Handler;
32 import android.os.UserHandle;
33 import android.provider.SearchIndexableResource;
34 import android.provider.Settings.Secure;
35 import android.text.TextUtils;
36 import android.util.FeatureFlagUtils;
37 import android.view.InputDevice;
38 import android.view.inputmethod.InputMethodManager;
39 
40 import androidx.preference.Preference;
41 import androidx.preference.Preference.OnPreferenceChangeListener;
42 import androidx.preference.PreferenceCategory;
43 import androidx.preference.PreferenceScreen;
44 import androidx.preference.SwitchPreference;
45 
46 import com.android.internal.util.Preconditions;
47 import com.android.settings.R;
48 import com.android.settings.Settings;
49 import com.android.settings.SettingsPreferenceFragment;
50 import com.android.settings.core.SubSettingLauncher;
51 import com.android.settings.overlay.FeatureFactory;
52 import com.android.settings.search.BaseSearchIndexProvider;
53 import com.android.settingslib.search.SearchIndexable;
54 import com.android.settingslib.utils.ThreadUtils;
55 
56 import java.text.Collator;
57 import java.util.ArrayList;
58 import java.util.Arrays;
59 import java.util.List;
60 import java.util.Objects;
61 
62 @SearchIndexable
63 public final class PhysicalKeyboardFragment extends SettingsPreferenceFragment
64         implements InputManager.InputDeviceListener,
65         KeyboardLayoutDialogFragment.OnSetupKeyboardLayoutsListener {
66 
67     private static final String KEYBOARD_OPTIONS_CATEGORY = "keyboard_options_category";
68     private static final String SHOW_VIRTUAL_KEYBOARD_SWITCH = "show_virtual_keyboard_switch";
69     private static final String KEYBOARD_SHORTCUTS_HELPER = "keyboard_shortcuts_helper";
70     private static final String MODIFIER_KEYS_SETTINGS = "modifier_keys_settings";
71     private static final String EXTRA_AUTO_SELECTION = "auto_selection";
72 
73     @NonNull
74     private final ArrayList<HardKeyboardDeviceInfo> mLastHardKeyboards = new ArrayList<>();
75 
76     private InputManager mIm;
77     private InputMethodManager mImm;
78     private InputDeviceIdentifier mAutoInputDeviceIdentifier;
79     private KeyboardSettingsFeatureProvider mFeatureProvider;
80     @NonNull
81     private PreferenceCategory mKeyboardAssistanceCategory;
82     @NonNull
83     private SwitchPreference mShowVirtualKeyboardSwitch;
84 
85     private Intent mIntentWaitingForResult;
86     private boolean mIsNewKeyboardSettings;
87     private boolean mSupportsFirmwareUpdate;
88 
89     static final String EXTRA_BT_ADDRESS = "extra_bt_address";
90     private String mBluetoothAddress;
91 
92     @Override
onSaveInstanceState(Bundle outState)93     public void onSaveInstanceState(Bundle outState) {
94         outState.putParcelable(EXTRA_AUTO_SELECTION, mAutoInputDeviceIdentifier);
95         super.onSaveInstanceState(outState);
96     }
97 
98     @Override
onCreatePreferences(Bundle bundle, String s)99     public void onCreatePreferences(Bundle bundle, String s) {
100         Activity activity = Preconditions.checkNotNull(getActivity());
101         addPreferencesFromResource(R.xml.physical_keyboard_settings);
102         mIm = Preconditions.checkNotNull(activity.getSystemService(InputManager.class));
103         mImm = Preconditions.checkNotNull(activity.getSystemService(InputMethodManager.class));
104         mKeyboardAssistanceCategory = Preconditions.checkNotNull(
105                 (PreferenceCategory) findPreference(KEYBOARD_OPTIONS_CATEGORY));
106         mShowVirtualKeyboardSwitch = Preconditions.checkNotNull(
107                 (SwitchPreference) mKeyboardAssistanceCategory.findPreference(
108                         SHOW_VIRTUAL_KEYBOARD_SWITCH));
109 
110         FeatureFactory featureFactory = FeatureFactory.getFactory(getContext());
111         mFeatureProvider = featureFactory.getKeyboardSettingsFeatureProvider();
112         mSupportsFirmwareUpdate = mFeatureProvider.supportsFirmwareUpdate();
113         if (mSupportsFirmwareUpdate) {
114             mFeatureProvider.addFirmwareUpdateCategory(getContext(), getPreferenceScreen());
115         }
116         mIsNewKeyboardSettings = FeatureFlagUtils.isEnabled(
117                 getContext(), FeatureFlagUtils.SETTINGS_NEW_KEYBOARD_UI);
118         boolean isModifierKeySettingsEnabled = FeatureFlagUtils
119                 .isEnabled(getContext(), FeatureFlagUtils.SETTINGS_NEW_KEYBOARD_MODIFIER_KEY);
120         if (!isModifierKeySettingsEnabled) {
121             mKeyboardAssistanceCategory.removePreference(findPreference(MODIFIER_KEYS_SETTINGS));
122         }
123         InputDeviceIdentifier inputDeviceIdentifier = activity.getIntent().getParcelableExtra(
124                 KeyboardLayoutPickerFragment.EXTRA_INPUT_DEVICE_IDENTIFIER);
125         int intentFromWhere =
126                 activity.getIntent().getIntExtra(android.provider.Settings.EXTRA_ENTRYPOINT, -1);
127         if (inputDeviceIdentifier != null) {
128             mAutoInputDeviceIdentifier = inputDeviceIdentifier;
129         }
130         // Don't repeat the autoselection.
131         if (isAutoSelection(bundle, inputDeviceIdentifier)) {
132             showEnabledLocalesKeyboardLayoutList(inputDeviceIdentifier);
133         }
134     }
135 
isAutoSelection(Bundle bundle, InputDeviceIdentifier identifier)136     private static boolean isAutoSelection(Bundle bundle, InputDeviceIdentifier identifier) {
137         if (bundle != null && bundle.getParcelable(EXTRA_AUTO_SELECTION) != null) {
138             return false;
139         }
140         return identifier != null;
141     }
142 
143     @Override
onPreferenceTreeClick(Preference preference)144     public boolean onPreferenceTreeClick(Preference preference) {
145         if (KEYBOARD_SHORTCUTS_HELPER.equals(preference.getKey())) {
146             writePreferenceClickMetric(preference);
147             toggleKeyboardShortcutsMenu();
148             return true;
149         }
150         return super.onPreferenceTreeClick(preference);
151     }
152 
153     @Override
onResume()154     public void onResume() {
155         super.onResume();
156         mLastHardKeyboards.clear();
157         scheduleUpdateHardKeyboards();
158         mIm.registerInputDeviceListener(this, null);
159         mShowVirtualKeyboardSwitch.setOnPreferenceChangeListener(
160                 mShowVirtualKeyboardSwitchPreferenceChangeListener);
161         registerShowVirtualKeyboardSettingsObserver();
162     }
163 
164     @Override
onPause()165     public void onPause() {
166         super.onPause();
167         mLastHardKeyboards.clear();
168         mIm.unregisterInputDeviceListener(this);
169         mShowVirtualKeyboardSwitch.setOnPreferenceChangeListener(null);
170         unregisterShowVirtualKeyboardSettingsObserver();
171     }
172 
173     @Override
onInputDeviceAdded(int deviceId)174     public void onInputDeviceAdded(int deviceId) {
175         scheduleUpdateHardKeyboards();
176     }
177 
178     @Override
onInputDeviceRemoved(int deviceId)179     public void onInputDeviceRemoved(int deviceId) {
180         scheduleUpdateHardKeyboards();
181     }
182 
183     @Override
onInputDeviceChanged(int deviceId)184     public void onInputDeviceChanged(int deviceId) {
185         scheduleUpdateHardKeyboards();
186     }
187 
188     @Override
getMetricsCategory()189     public int getMetricsCategory() {
190         return SettingsEnums.PHYSICAL_KEYBOARDS;
191     }
192 
scheduleUpdateHardKeyboards()193     private void scheduleUpdateHardKeyboards() {
194         final Context context = getContext();
195         ThreadUtils.postOnBackgroundThread(() -> {
196             final List<HardKeyboardDeviceInfo> newHardKeyboards = getHardKeyboards(context);
197             if (newHardKeyboards.isEmpty()) {
198                 getActivity().finish();
199                 return;
200             }
201             ThreadUtils.postOnMainThread(() -> updateHardKeyboards(newHardKeyboards));
202         });
203     }
204 
updateHardKeyboards(@onNull List<HardKeyboardDeviceInfo> newHardKeyboards)205     private void updateHardKeyboards(@NonNull List<HardKeyboardDeviceInfo> newHardKeyboards) {
206         if (Objects.equals(mLastHardKeyboards, newHardKeyboards)) {
207             // Nothing has changed.  Ignore.
208             return;
209         }
210 
211         // TODO(yukawa): Maybe we should follow the style used in ConnectedDeviceDashboardFragment.
212 
213         mLastHardKeyboards.clear();
214         mLastHardKeyboards.addAll(newHardKeyboards);
215 
216         final PreferenceScreen preferenceScreen = getPreferenceScreen();
217         preferenceScreen.removeAll();
218         final PreferenceCategory category = new PreferenceCategory(getPrefContext());
219         category.setTitle(R.string.builtin_keyboard_settings_title);
220         category.setOrder(0);
221         preferenceScreen.addPreference(category);
222 
223         for (HardKeyboardDeviceInfo hardKeyboardDeviceInfo : newHardKeyboards) {
224             // TODO(yukawa): Consider using com.android.settings.widget.GearPreference
225             final Preference pref = new Preference(getPrefContext());
226             pref.setTitle(hardKeyboardDeviceInfo.mDeviceName);
227             if (mIsNewKeyboardSettings) {
228                 List<String> suitableImes = new ArrayList<>();
229                 suitableImes.addAll(
230                         NewKeyboardSettingsUtils.getSuitableImeLabels(
231                                 getContext(), mImm, UserHandle.myUserId()));
232                 if (!suitableImes.isEmpty()) {
233                     String summary = suitableImes.get(0);
234                     StringBuilder result = new StringBuilder(summary);
235                     for (int i = 1; i < suitableImes.size(); i++) {
236                         result.append(", ").append(suitableImes.get(i));
237                     }
238                     pref.setSummary(result.toString());
239                 } else {
240                     pref.setSummary(hardKeyboardDeviceInfo.mLayoutLabel);
241                 }
242                 pref.setOnPreferenceClickListener(
243                         preference -> {
244                             showEnabledLocalesKeyboardLayoutList(
245                                     hardKeyboardDeviceInfo.mDeviceIdentifier);
246                             return true;
247                         });
248             } else {
249                 pref.setSummary(hardKeyboardDeviceInfo.mLayoutLabel);
250                 pref.setOnPreferenceClickListener(
251                         preference -> {
252                             showKeyboardLayoutDialog(hardKeyboardDeviceInfo.mDeviceIdentifier);
253                             return true;
254                         });
255             }
256             category.addPreference(pref);
257         }
258         mKeyboardAssistanceCategory.setOrder(1);
259         preferenceScreen.addPreference(mKeyboardAssistanceCategory);
260         if (mSupportsFirmwareUpdate) {
261             mFeatureProvider.addFirmwareUpdateCategory(getPrefContext(), preferenceScreen);
262         }
263         updateShowVirtualKeyboardSwitch();
264     }
265 
showKeyboardLayoutDialog(InputDeviceIdentifier inputDeviceIdentifier)266     private void showKeyboardLayoutDialog(InputDeviceIdentifier inputDeviceIdentifier) {
267         KeyboardLayoutDialogFragment fragment = new KeyboardLayoutDialogFragment(
268                 inputDeviceIdentifier);
269         fragment.setTargetFragment(this, 0);
270         fragment.show(getActivity().getSupportFragmentManager(), "keyboardLayout");
271     }
272 
showEnabledLocalesKeyboardLayoutList(InputDeviceIdentifier inputDeviceIdentifier)273     private void showEnabledLocalesKeyboardLayoutList(InputDeviceIdentifier inputDeviceIdentifier) {
274         Bundle arguments = new Bundle();
275         arguments.putParcelable(NewKeyboardSettingsUtils.EXTRA_INPUT_DEVICE_IDENTIFIER,
276                 inputDeviceIdentifier);
277         new SubSettingLauncher(getContext())
278                 .setSourceMetricsCategory(getMetricsCategory())
279                 .setDestination(NewKeyboardLayoutEnabledLocalesFragment.class.getName())
280                 .setArguments(arguments)
281                 .launch();
282     }
283 
registerShowVirtualKeyboardSettingsObserver()284     private void registerShowVirtualKeyboardSettingsObserver() {
285         unregisterShowVirtualKeyboardSettingsObserver();
286         getActivity().getContentResolver().registerContentObserver(
287                 Secure.getUriFor(Secure.SHOW_IME_WITH_HARD_KEYBOARD),
288                 false,
289                 mContentObserver,
290                 UserHandle.myUserId());
291         updateShowVirtualKeyboardSwitch();
292     }
293 
unregisterShowVirtualKeyboardSettingsObserver()294     private void unregisterShowVirtualKeyboardSettingsObserver() {
295         getActivity().getContentResolver().unregisterContentObserver(mContentObserver);
296     }
297 
updateShowVirtualKeyboardSwitch()298     private void updateShowVirtualKeyboardSwitch() {
299         mShowVirtualKeyboardSwitch.setChecked(
300                 Secure.getInt(getContentResolver(), Secure.SHOW_IME_WITH_HARD_KEYBOARD, 0) != 0);
301     }
302 
toggleKeyboardShortcutsMenu()303     private void toggleKeyboardShortcutsMenu() {
304         getActivity().requestShowKeyboardShortcuts();
305     }
306 
307     private final OnPreferenceChangeListener mShowVirtualKeyboardSwitchPreferenceChangeListener =
308             (preference, newValue) -> {
309                 final ContentResolver cr = getContentResolver();
310                 Secure.putInt(cr, Secure.SHOW_IME_WITH_HARD_KEYBOARD, ((Boolean) newValue) ? 1 : 0);
311                 cr.notifyChange(Secure.getUriFor(Secure.SHOW_IME_WITH_HARD_KEYBOARD),
312                         null /* observer */, ContentResolver.NOTIFY_NO_DELAY);
313                 return true;
314             };
315 
316     private final ContentObserver mContentObserver = new ContentObserver(new Handler(true)) {
317         @Override
318         public void onChange(boolean selfChange) {
319             updateShowVirtualKeyboardSwitch();
320         }
321     };
322 
323     @Override
onSetupKeyboardLayouts(InputDeviceIdentifier inputDeviceIdentifier)324     public void onSetupKeyboardLayouts(InputDeviceIdentifier inputDeviceIdentifier) {
325         final Intent intent = new Intent(Intent.ACTION_MAIN);
326         intent.setClass(getActivity(), Settings.KeyboardLayoutPickerActivity.class);
327         intent.putExtra(KeyboardLayoutPickerFragment.EXTRA_INPUT_DEVICE_IDENTIFIER,
328                 inputDeviceIdentifier);
329         mIntentWaitingForResult = intent;
330         startActivityForResult(intent, 0);
331     }
332 
333     @Override
onActivityResult(int requestCode, int resultCode, Intent data)334     public void onActivityResult(int requestCode, int resultCode, Intent data) {
335         super.onActivityResult(requestCode, resultCode, data);
336 
337         if (mIntentWaitingForResult != null) {
338             InputDeviceIdentifier inputDeviceIdentifier = mIntentWaitingForResult
339                     .getParcelableExtra(KeyboardLayoutPickerFragment.EXTRA_INPUT_DEVICE_IDENTIFIER);
340             mIntentWaitingForResult = null;
341             showKeyboardLayoutDialog(inputDeviceIdentifier);
342         }
343     }
344 
getLayoutLabel(@onNull InputDevice device, @NonNull Context context, @NonNull InputManager im)345     private static String getLayoutLabel(@NonNull InputDevice device,
346             @NonNull Context context, @NonNull InputManager im) {
347         final String currentLayoutDesc =
348                 im.getCurrentKeyboardLayoutForInputDevice(device.getIdentifier());
349         if (currentLayoutDesc == null) {
350             return context.getString(R.string.keyboard_layout_default_label);
351         }
352         final KeyboardLayout currentLayout = im.getKeyboardLayout(currentLayoutDesc);
353         if (currentLayout == null) {
354             return context.getString(R.string.keyboard_layout_default_label);
355         }
356         // If current layout is specified but the layout is null, just return an empty string
357         // instead of falling back to R.string.keyboard_layout_default_label.
358         return TextUtils.emptyIfNull(currentLayout.getLabel());
359     }
360 
361     @NonNull
getHardKeyboards(@onNull Context context)362     static List<HardKeyboardDeviceInfo> getHardKeyboards(@NonNull Context context) {
363         final List<HardKeyboardDeviceInfo> keyboards = new ArrayList<>();
364         final InputManager im = context.getSystemService(InputManager.class);
365         if (im == null) {
366             return new ArrayList<>();
367         }
368         for (int deviceId : InputDevice.getDeviceIds()) {
369             final InputDevice device = InputDevice.getDevice(deviceId);
370             if (device == null || device.isVirtual() || !device.isFullKeyboard()) {
371                 continue;
372             }
373             keyboards.add(new HardKeyboardDeviceInfo(
374                     device.getName(),
375                     device.getIdentifier(),
376                     getLayoutLabel(device, context, im),
377                     device.getBluetoothAddress()));
378         }
379 
380         // We intentionally don't reuse Comparator because Collator may not be thread-safe.
381         final Collator collator = Collator.getInstance();
382         keyboards.sort((a, b) -> {
383             int result = collator.compare(a.mDeviceName, b.mDeviceName);
384             if (result != 0) {
385                 return result;
386             }
387             result = a.mDeviceIdentifier.getDescriptor().compareTo(
388                     b.mDeviceIdentifier.getDescriptor());
389             if (result != 0) {
390                 return result;
391             }
392             return collator.compare(a.mLayoutLabel, b.mLayoutLabel);
393         });
394         return keyboards;
395     }
396 
397     public static final class HardKeyboardDeviceInfo {
398         @NonNull
399         public final String mDeviceName;
400         @NonNull
401         public final InputDeviceIdentifier mDeviceIdentifier;
402         @NonNull
403         public final String mLayoutLabel;
404         @Nullable
405         public final String mBluetoothAddress;
406 
HardKeyboardDeviceInfo( @ullable String deviceName, @NonNull InputDeviceIdentifier deviceIdentifier, @NonNull String layoutLabel, @Nullable String bluetoothAddress)407         public HardKeyboardDeviceInfo(
408                 @Nullable String deviceName,
409                 @NonNull InputDeviceIdentifier deviceIdentifier,
410                 @NonNull String layoutLabel,
411                 @Nullable String bluetoothAddress) {
412             mDeviceName = TextUtils.emptyIfNull(deviceName);
413             mDeviceIdentifier = deviceIdentifier;
414             mLayoutLabel = layoutLabel;
415             mBluetoothAddress = bluetoothAddress;
416         }
417 
418         @Override
equals(Object o)419         public boolean equals(Object o) {
420             if (o == this) return true;
421             if (o == null) return false;
422 
423             if (!(o instanceof HardKeyboardDeviceInfo)) return false;
424 
425             final HardKeyboardDeviceInfo that = (HardKeyboardDeviceInfo) o;
426             if (!TextUtils.equals(mDeviceName, that.mDeviceName)) {
427                 return false;
428             }
429             if (!Objects.equals(mDeviceIdentifier, that.mDeviceIdentifier)) {
430                 return false;
431             }
432             if (!TextUtils.equals(mLayoutLabel, that.mLayoutLabel)) {
433                 return false;
434             }
435             if (!TextUtils.equals(mBluetoothAddress, that.mBluetoothAddress)) {
436                 return false;
437             }
438 
439             return true;
440         }
441     }
442 
443     public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
444             new BaseSearchIndexProvider() {
445                 @Override
446                 public List<SearchIndexableResource> getXmlResourcesToIndex(
447                         Context context, boolean enabled) {
448                     final SearchIndexableResource sir = new SearchIndexableResource(context);
449                     sir.xmlResId = R.xml.physical_keyboard_settings;
450                     return Arrays.asList(sir);
451                 }
452             };
453 }
454