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