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