• 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.Context;
24 import android.content.Intent;
25 import android.database.ContentObserver;
26 import android.hardware.input.InputDeviceIdentifier;
27 import android.hardware.input.InputManager;
28 import android.hardware.input.KeyboardLayout;
29 import android.os.Bundle;
30 import android.os.Handler;
31 import android.os.UserHandle;
32 import android.provider.SearchIndexableResource;
33 import android.provider.Settings.Secure;
34 import android.text.TextUtils;
35 import android.view.InputDevice;
36 
37 import androidx.preference.Preference;
38 import androidx.preference.Preference.OnPreferenceChangeListener;
39 import androidx.preference.PreferenceCategory;
40 import androidx.preference.PreferenceScreen;
41 import androidx.preference.SwitchPreference;
42 
43 import com.android.internal.util.Preconditions;
44 import com.android.settings.R;
45 import com.android.settings.Settings;
46 import com.android.settings.SettingsPreferenceFragment;
47 import com.android.settings.search.BaseSearchIndexProvider;
48 import com.android.settings.search.Indexable;
49 import com.android.settingslib.search.SearchIndexable;
50 import com.android.settingslib.utils.ThreadUtils;
51 
52 import java.text.Collator;
53 import java.util.ArrayList;
54 import java.util.Arrays;
55 import java.util.List;
56 import java.util.Objects;
57 
58 @SearchIndexable
59 public final class PhysicalKeyboardFragment extends SettingsPreferenceFragment
60         implements InputManager.InputDeviceListener,
61         KeyboardLayoutDialogFragment.OnSetupKeyboardLayoutsListener {
62 
63     private static final String KEYBOARD_ASSISTANCE_CATEGORY = "keyboard_assistance_category";
64     private static final String SHOW_VIRTUAL_KEYBOARD_SWITCH = "show_virtual_keyboard_switch";
65     private static final String KEYBOARD_SHORTCUTS_HELPER = "keyboard_shortcuts_helper";
66 
67     @NonNull
68     private final ArrayList<HardKeyboardDeviceInfo> mLastHardKeyboards = new ArrayList<>();
69 
70     private InputManager mIm;
71     @NonNull
72     private PreferenceCategory mKeyboardAssistanceCategory;
73     @NonNull
74     private SwitchPreference mShowVirtualKeyboardSwitch;
75 
76     private Intent mIntentWaitingForResult;
77 
78     @Override
onCreatePreferences(Bundle bundle, String s)79     public void onCreatePreferences(Bundle bundle, String s) {
80         Activity activity = Preconditions.checkNotNull(getActivity());
81         addPreferencesFromResource(R.xml.physical_keyboard_settings);
82         mIm = Preconditions.checkNotNull(activity.getSystemService(InputManager.class));
83         mKeyboardAssistanceCategory = Preconditions.checkNotNull(
84                 (PreferenceCategory) findPreference(KEYBOARD_ASSISTANCE_CATEGORY));
85         mShowVirtualKeyboardSwitch = Preconditions.checkNotNull(
86                 (SwitchPreference) mKeyboardAssistanceCategory.findPreference(
87                         SHOW_VIRTUAL_KEYBOARD_SWITCH));
88         findPreference(KEYBOARD_SHORTCUTS_HELPER).setOnPreferenceClickListener(
89                 new Preference.OnPreferenceClickListener() {
90                     @Override
91                     public boolean onPreferenceClick(Preference preference) {
92                         toggleKeyboardShortcutsMenu();
93                         return true;
94                     }
95                 });
96     }
97 
98     @Override
onResume()99     public void onResume() {
100         super.onResume();
101         mLastHardKeyboards.clear();
102         scheduleUpdateHardKeyboards();
103         mIm.registerInputDeviceListener(this, null);
104         mShowVirtualKeyboardSwitch.setOnPreferenceChangeListener(
105                 mShowVirtualKeyboardSwitchPreferenceChangeListener);
106         registerShowVirtualKeyboardSettingsObserver();
107     }
108 
109     @Override
onPause()110     public void onPause() {
111         super.onPause();
112         mLastHardKeyboards.clear();
113         mIm.unregisterInputDeviceListener(this);
114         mShowVirtualKeyboardSwitch.setOnPreferenceChangeListener(null);
115         unregisterShowVirtualKeyboardSettingsObserver();
116     }
117 
118     @Override
onInputDeviceAdded(int deviceId)119     public void onInputDeviceAdded(int deviceId) {
120         scheduleUpdateHardKeyboards();
121     }
122 
123     @Override
onInputDeviceRemoved(int deviceId)124     public void onInputDeviceRemoved(int deviceId) {
125         scheduleUpdateHardKeyboards();
126     }
127 
128     @Override
onInputDeviceChanged(int deviceId)129     public void onInputDeviceChanged(int deviceId) {
130         scheduleUpdateHardKeyboards();
131     }
132 
133     @Override
getMetricsCategory()134     public int getMetricsCategory() {
135         return SettingsEnums.PHYSICAL_KEYBOARDS;
136     }
137 
scheduleUpdateHardKeyboards()138     private void scheduleUpdateHardKeyboards() {
139         final Context context = getContext();
140         ThreadUtils.postOnBackgroundThread(() -> {
141             final List<HardKeyboardDeviceInfo> newHardKeyboards = getHardKeyboards(context);
142             ThreadUtils.postOnMainThread(() -> updateHardKeyboards(newHardKeyboards));
143         });
144     }
145 
updateHardKeyboards(@onNull List<HardKeyboardDeviceInfo> newHardKeyboards)146     private void updateHardKeyboards(@NonNull List<HardKeyboardDeviceInfo> newHardKeyboards) {
147         if (Objects.equals(mLastHardKeyboards, newHardKeyboards)) {
148             // Nothing has changed.  Ignore.
149             return;
150         }
151 
152         // TODO(yukawa): Maybe we should follow the style used in ConnectedDeviceDashboardFragment.
153 
154         mLastHardKeyboards.clear();
155         mLastHardKeyboards.addAll(newHardKeyboards);
156 
157         final PreferenceScreen preferenceScreen = getPreferenceScreen();
158         preferenceScreen.removeAll();
159         final PreferenceCategory category = new PreferenceCategory(getPrefContext());
160         category.setTitle(R.string.builtin_keyboard_settings_title);
161         category.setOrder(0);
162         preferenceScreen.addPreference(category);
163 
164         for (HardKeyboardDeviceInfo hardKeyboardDeviceInfo : newHardKeyboards) {
165             // TODO(yukawa): Consider using com.android.settings.widget.GearPreference
166             final Preference pref = new Preference(getPrefContext());
167             pref.setTitle(hardKeyboardDeviceInfo.mDeviceName);
168             pref.setSummary(hardKeyboardDeviceInfo.mLayoutLabel);
169             pref.setOnPreferenceClickListener(preference -> {
170                 showKeyboardLayoutDialog(hardKeyboardDeviceInfo.mDeviceIdentifier);
171                 return true;
172             });
173             category.addPreference(pref);
174         }
175 
176         mKeyboardAssistanceCategory.setOrder(1);
177         preferenceScreen.addPreference(mKeyboardAssistanceCategory);
178         updateShowVirtualKeyboardSwitch();
179     }
180 
showKeyboardLayoutDialog(InputDeviceIdentifier inputDeviceIdentifier)181     private void showKeyboardLayoutDialog(InputDeviceIdentifier inputDeviceIdentifier) {
182         KeyboardLayoutDialogFragment fragment = new KeyboardLayoutDialogFragment(
183                 inputDeviceIdentifier);
184         fragment.setTargetFragment(this, 0);
185         fragment.show(getActivity().getSupportFragmentManager(), "keyboardLayout");
186     }
187 
registerShowVirtualKeyboardSettingsObserver()188     private void registerShowVirtualKeyboardSettingsObserver() {
189         unregisterShowVirtualKeyboardSettingsObserver();
190         getActivity().getContentResolver().registerContentObserver(
191                 Secure.getUriFor(Secure.SHOW_IME_WITH_HARD_KEYBOARD),
192                 false,
193                 mContentObserver,
194                 UserHandle.myUserId());
195         updateShowVirtualKeyboardSwitch();
196     }
197 
unregisterShowVirtualKeyboardSettingsObserver()198     private void unregisterShowVirtualKeyboardSettingsObserver() {
199         getActivity().getContentResolver().unregisterContentObserver(mContentObserver);
200     }
201 
updateShowVirtualKeyboardSwitch()202     private void updateShowVirtualKeyboardSwitch() {
203         mShowVirtualKeyboardSwitch.setChecked(
204                 Secure.getInt(getContentResolver(), Secure.SHOW_IME_WITH_HARD_KEYBOARD, 0) != 0);
205     }
206 
toggleKeyboardShortcutsMenu()207     private void toggleKeyboardShortcutsMenu() {
208         getActivity().requestShowKeyboardShortcuts();
209     }
210 
211     private final OnPreferenceChangeListener mShowVirtualKeyboardSwitchPreferenceChangeListener =
212             (preference, newValue) -> {
213                 Secure.putInt(getContentResolver(), Secure.SHOW_IME_WITH_HARD_KEYBOARD,
214                         ((Boolean) newValue) ? 1 : 0);
215                 return true;
216             };
217 
218     private final ContentObserver mContentObserver = new ContentObserver(new Handler(true)) {
219         @Override
220         public void onChange(boolean selfChange) {
221             updateShowVirtualKeyboardSwitch();
222         }
223     };
224 
225     @Override
onSetupKeyboardLayouts(InputDeviceIdentifier inputDeviceIdentifier)226     public void onSetupKeyboardLayouts(InputDeviceIdentifier inputDeviceIdentifier) {
227         final Intent intent = new Intent(Intent.ACTION_MAIN);
228         intent.setClass(getActivity(), Settings.KeyboardLayoutPickerActivity.class);
229         intent.putExtra(KeyboardLayoutPickerFragment.EXTRA_INPUT_DEVICE_IDENTIFIER,
230                 inputDeviceIdentifier);
231         mIntentWaitingForResult = intent;
232         startActivityForResult(intent, 0);
233     }
234 
235     @Override
onActivityResult(int requestCode, int resultCode, Intent data)236     public void onActivityResult(int requestCode, int resultCode, Intent data) {
237         super.onActivityResult(requestCode, resultCode, data);
238 
239         if (mIntentWaitingForResult != null) {
240             InputDeviceIdentifier inputDeviceIdentifier = mIntentWaitingForResult
241                     .getParcelableExtra(KeyboardLayoutPickerFragment.EXTRA_INPUT_DEVICE_IDENTIFIER);
242             mIntentWaitingForResult = null;
243             showKeyboardLayoutDialog(inputDeviceIdentifier);
244         }
245     }
246 
getLayoutLabel(@onNull InputDevice device, @NonNull Context context, @NonNull InputManager im)247     private static String getLayoutLabel(@NonNull InputDevice device,
248             @NonNull Context context, @NonNull InputManager im) {
249         final String currentLayoutDesc =
250                 im.getCurrentKeyboardLayoutForInputDevice(device.getIdentifier());
251         if (currentLayoutDesc == null) {
252             return context.getString(R.string.keyboard_layout_default_label);
253         }
254         final KeyboardLayout currentLayout = im.getKeyboardLayout(currentLayoutDesc);
255         if (currentLayout == null) {
256             return context.getString(R.string.keyboard_layout_default_label);
257         }
258         // If current layout is specified but the layout is null, just return an empty string
259         // instead of falling back to R.string.keyboard_layout_default_label.
260         return TextUtils.emptyIfNull(currentLayout.getLabel());
261     }
262 
263     @NonNull
getHardKeyboards(@onNull Context context)264     static List<HardKeyboardDeviceInfo> getHardKeyboards(@NonNull Context context) {
265         final List<HardKeyboardDeviceInfo> keyboards = new ArrayList<>();
266         final InputManager im = context.getSystemService(InputManager.class);
267         if (im == null) {
268             return new ArrayList<>();
269         }
270         for (int deviceId : InputDevice.getDeviceIds()) {
271             final InputDevice device = InputDevice.getDevice(deviceId);
272             if (device == null || device.isVirtual() || !device.isFullKeyboard()) {
273                 continue;
274             }
275             keyboards.add(new HardKeyboardDeviceInfo(
276                     device.getName(), device.getIdentifier(), getLayoutLabel(device, context, im)));
277         }
278 
279         // We intentionally don't reuse Comparator because Collator may not be thread-safe.
280         final Collator collator = Collator.getInstance();
281         keyboards.sort((a, b) -> {
282             int result = collator.compare(a.mDeviceName, b.mDeviceName);
283             if (result != 0) {
284                 return result;
285             }
286             result = a.mDeviceIdentifier.getDescriptor().compareTo(
287                     b.mDeviceIdentifier.getDescriptor());
288             if (result != 0) {
289                 return result;
290             }
291             return collator.compare(a.mLayoutLabel, b.mLayoutLabel);
292         });
293         return keyboards;
294     }
295 
296     public static final class HardKeyboardDeviceInfo {
297         @NonNull
298         public final String mDeviceName;
299         @NonNull
300         public final InputDeviceIdentifier mDeviceIdentifier;
301         @NonNull
302         public final String mLayoutLabel;
303 
HardKeyboardDeviceInfo( @ullable String deviceName, @NonNull InputDeviceIdentifier deviceIdentifier, @NonNull String layoutLabel)304         public HardKeyboardDeviceInfo(
305                 @Nullable String deviceName,
306                 @NonNull InputDeviceIdentifier deviceIdentifier,
307                 @NonNull String layoutLabel) {
308             mDeviceName = TextUtils.emptyIfNull(deviceName);
309             mDeviceIdentifier = deviceIdentifier;
310             mLayoutLabel = layoutLabel;
311         }
312 
313         @Override
equals(Object o)314         public boolean equals(Object o) {
315             if (o == this) return true;
316             if (o == null) return false;
317 
318             if (!(o instanceof HardKeyboardDeviceInfo)) return false;
319 
320             final HardKeyboardDeviceInfo that = (HardKeyboardDeviceInfo) o;
321             if (!TextUtils.equals(mDeviceName, that.mDeviceName)) {
322                 return false;
323             }
324             if (!Objects.equals(mDeviceIdentifier, that.mDeviceIdentifier)) {
325                 return false;
326             }
327             if (!TextUtils.equals(mLayoutLabel, that.mLayoutLabel)) {
328                 return false;
329             }
330 
331             return true;
332         }
333     }
334 
335     public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
336             new BaseSearchIndexProvider() {
337                 @Override
338                 public List<SearchIndexableResource> getXmlResourcesToIndex(
339                         Context context, boolean enabled) {
340                     final SearchIndexableResource sir = new SearchIndexableResource(context);
341                     sir.xmlResId = R.xml.physical_keyboard_settings;
342                     return Arrays.asList(sir);
343                 }
344             };
345 }
346