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