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