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