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