This CL logically reverts Settings app changes for Bug 25752812, which aimed to improve UX by tightly integrating physical keyboard layout with input method subtype. What went wrong is that the concept of input method subtype is not widely accepted by the ecosystem actually. Until we figoure out any other better way here, let's revert back to the good old way that enables users to specify multiple keyboard layouts per physical keyboard device, not one layout per one input method subtype. Note that we cannot simply revert the CL that originally introduced the new flow [1] because it was indeed a huge CL that also touched IME settings, which we want to continue using. In that sense, this CL is a kind of re-implementation of the previous style on top of the recent language settings flow. Note also that a fix [2] fox Bug 25062009 was also ported from previous InputMethodAndLanguageSetting to KeyboardLayoutPickerFragment. [1]: I728d7ee185827ed328c16cb7abce244557a26518976bb3f459
[2]: I4483dfc89afc8d148b2cfa7c6a5f66d2a02f712a17b6319884
Fix: 66498367 Test: make -j RunSettingsRoboTests Test: Manually done with two Bluetooth keyboards Change-Id: I7a2ed6dd39dcd8207d3d94e12cd01d5d67ba4bb5
358 lines
14 KiB
Java
358 lines
14 KiB
Java
/*
|
|
* Copyright (C) 2016 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package com.android.settings.inputmethod;
|
|
|
|
import android.annotation.NonNull;
|
|
import android.annotation.Nullable;
|
|
import android.app.Activity;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.database.ContentObserver;
|
|
import android.hardware.input.InputDeviceIdentifier;
|
|
import android.hardware.input.InputManager;
|
|
import android.hardware.input.KeyboardLayout;
|
|
import android.os.Bundle;
|
|
import android.os.Handler;
|
|
import android.os.UserHandle;
|
|
import android.provider.SearchIndexableResource;
|
|
import android.provider.Settings.Secure;
|
|
import android.support.v14.preference.SwitchPreference;
|
|
import android.support.v7.preference.Preference;
|
|
import android.support.v7.preference.Preference.OnPreferenceChangeListener;
|
|
import android.support.v7.preference.PreferenceCategory;
|
|
import android.support.v7.preference.PreferenceScreen;
|
|
import android.text.TextUtils;
|
|
import android.view.InputDevice;
|
|
|
|
import com.android.internal.inputmethod.InputMethodUtils;
|
|
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
|
|
import com.android.internal.util.Preconditions;
|
|
import com.android.settings.R;
|
|
import com.android.settings.Settings;
|
|
import com.android.settings.SettingsPreferenceFragment;
|
|
import com.android.settings.search.BaseSearchIndexProvider;
|
|
import com.android.settings.search.Indexable;
|
|
import com.android.settingslib.utils.ThreadUtils;
|
|
|
|
import java.text.Collator;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.Objects;
|
|
|
|
public final class PhysicalKeyboardFragment extends SettingsPreferenceFragment
|
|
implements InputManager.InputDeviceListener,
|
|
KeyboardLayoutDialogFragment.OnSetupKeyboardLayoutsListener, Indexable {
|
|
|
|
private static final String KEYBOARD_ASSISTANCE_CATEGORY = "keyboard_assistance_category";
|
|
private static final String SHOW_VIRTUAL_KEYBOARD_SWITCH = "show_virtual_keyboard_switch";
|
|
private static final String KEYBOARD_SHORTCUTS_HELPER = "keyboard_shortcuts_helper";
|
|
|
|
@NonNull
|
|
private final ArrayList<HardKeyboardDeviceInfo> mLastHardKeyboards = new ArrayList<>();
|
|
|
|
private InputManager mIm;
|
|
@NonNull
|
|
private PreferenceCategory mKeyboardAssistanceCategory;
|
|
@NonNull
|
|
private SwitchPreference mShowVirtualKeyboardSwitch;
|
|
@NonNull
|
|
private InputMethodUtils.InputMethodSettings mSettings;
|
|
|
|
private Intent mIntentWaitingForResult;
|
|
|
|
@Override
|
|
public void onCreatePreferences(Bundle bundle, String s) {
|
|
Activity activity = Preconditions.checkNotNull(getActivity());
|
|
addPreferencesFromResource(R.xml.physical_keyboard_settings);
|
|
mIm = Preconditions.checkNotNull(activity.getSystemService(InputManager.class));
|
|
mSettings = new InputMethodUtils.InputMethodSettings(
|
|
activity.getResources(),
|
|
getContentResolver(),
|
|
new HashMap<>(),
|
|
new ArrayList<>(),
|
|
UserHandle.myUserId(),
|
|
false /* copyOnWrite */);
|
|
mKeyboardAssistanceCategory = Preconditions.checkNotNull(
|
|
(PreferenceCategory) findPreference(KEYBOARD_ASSISTANCE_CATEGORY));
|
|
mShowVirtualKeyboardSwitch = Preconditions.checkNotNull(
|
|
(SwitchPreference) mKeyboardAssistanceCategory.findPreference(
|
|
SHOW_VIRTUAL_KEYBOARD_SWITCH));
|
|
findPreference(KEYBOARD_SHORTCUTS_HELPER).setOnPreferenceClickListener(
|
|
new Preference.OnPreferenceClickListener() {
|
|
@Override
|
|
public boolean onPreferenceClick(Preference preference) {
|
|
toggleKeyboardShortcutsMenu();
|
|
return true;
|
|
}
|
|
});
|
|
}
|
|
|
|
@Override
|
|
public void onResume() {
|
|
super.onResume();
|
|
mLastHardKeyboards.clear();
|
|
scheduleUpdateHardKeyboards();
|
|
mIm.registerInputDeviceListener(this, null);
|
|
mShowVirtualKeyboardSwitch.setOnPreferenceChangeListener(
|
|
mShowVirtualKeyboardSwitchPreferenceChangeListener);
|
|
registerShowVirtualKeyboardSettingsObserver();
|
|
}
|
|
|
|
@Override
|
|
public void onPause() {
|
|
super.onPause();
|
|
mLastHardKeyboards.clear();
|
|
mIm.unregisterInputDeviceListener(this);
|
|
mShowVirtualKeyboardSwitch.setOnPreferenceChangeListener(null);
|
|
unregisterShowVirtualKeyboardSettingsObserver();
|
|
}
|
|
|
|
@Override
|
|
public void onInputDeviceAdded(int deviceId) {
|
|
scheduleUpdateHardKeyboards();
|
|
}
|
|
|
|
@Override
|
|
public void onInputDeviceRemoved(int deviceId) {
|
|
scheduleUpdateHardKeyboards();
|
|
}
|
|
|
|
@Override
|
|
public void onInputDeviceChanged(int deviceId) {
|
|
scheduleUpdateHardKeyboards();
|
|
}
|
|
|
|
@Override
|
|
public int getMetricsCategory() {
|
|
return MetricsEvent.PHYSICAL_KEYBOARDS;
|
|
}
|
|
|
|
private void scheduleUpdateHardKeyboards() {
|
|
final Context context = getContext();
|
|
ThreadUtils.postOnBackgroundThread(() -> {
|
|
final List<HardKeyboardDeviceInfo> newHardKeyboards = getHardKeyboards(context);
|
|
ThreadUtils.postOnMainThread(() -> updateHardKeyboards(newHardKeyboards));
|
|
});
|
|
}
|
|
|
|
private void updateHardKeyboards(@NonNull List<HardKeyboardDeviceInfo> newHardKeyboards) {
|
|
if (Objects.equals(mLastHardKeyboards, newHardKeyboards)) {
|
|
// Nothing has changed. Ignore.
|
|
return;
|
|
}
|
|
|
|
// TODO(yukawa): Maybe we should follow the style used in ConnectedDeviceDashboardFragment.
|
|
|
|
mLastHardKeyboards.clear();
|
|
mLastHardKeyboards.addAll(newHardKeyboards);
|
|
|
|
final PreferenceScreen preferenceScreen = getPreferenceScreen();
|
|
preferenceScreen.removeAll();
|
|
final PreferenceCategory category = new PreferenceCategory(getPrefContext());
|
|
category.setTitle(R.string.builtin_keyboard_settings_title);
|
|
category.setOrder(0);
|
|
preferenceScreen.addPreference(category);
|
|
|
|
for (HardKeyboardDeviceInfo hardKeyboardDeviceInfo : newHardKeyboards) {
|
|
// TODO(yukawa): Consider using com.android.settings.widget.GearPreference
|
|
final Preference pref = new Preference(getPrefContext());
|
|
pref.setTitle(hardKeyboardDeviceInfo.mDeviceName);
|
|
pref.setSummary(hardKeyboardDeviceInfo.mLayoutLabel);
|
|
pref.setOnPreferenceClickListener(preference -> {
|
|
showKeyboardLayoutDialog(hardKeyboardDeviceInfo.mDeviceIdentifier);
|
|
return true;
|
|
});
|
|
category.addPreference(pref);
|
|
}
|
|
|
|
mKeyboardAssistanceCategory.setOrder(1);
|
|
preferenceScreen.addPreference(mKeyboardAssistanceCategory);
|
|
updateShowVirtualKeyboardSwitch();
|
|
}
|
|
|
|
private void showKeyboardLayoutDialog(InputDeviceIdentifier inputDeviceIdentifier) {
|
|
KeyboardLayoutDialogFragment fragment = (KeyboardLayoutDialogFragment)
|
|
getFragmentManager().findFragmentByTag("keyboardLayout");
|
|
if (fragment == null) {
|
|
fragment = new KeyboardLayoutDialogFragment(inputDeviceIdentifier);
|
|
fragment.setTargetFragment(this, 0);
|
|
fragment.show(getActivity().getFragmentManager(), "keyboardLayout");
|
|
}
|
|
}
|
|
|
|
private void registerShowVirtualKeyboardSettingsObserver() {
|
|
unregisterShowVirtualKeyboardSettingsObserver();
|
|
getActivity().getContentResolver().registerContentObserver(
|
|
Secure.getUriFor(Secure.SHOW_IME_WITH_HARD_KEYBOARD),
|
|
false,
|
|
mContentObserver,
|
|
UserHandle.myUserId());
|
|
updateShowVirtualKeyboardSwitch();
|
|
}
|
|
|
|
private void unregisterShowVirtualKeyboardSettingsObserver() {
|
|
getActivity().getContentResolver().unregisterContentObserver(mContentObserver);
|
|
}
|
|
|
|
private void updateShowVirtualKeyboardSwitch() {
|
|
mShowVirtualKeyboardSwitch.setChecked(mSettings.isShowImeWithHardKeyboardEnabled());
|
|
}
|
|
|
|
private void toggleKeyboardShortcutsMenu() {
|
|
getActivity().requestShowKeyboardShortcuts();
|
|
}
|
|
|
|
private final OnPreferenceChangeListener mShowVirtualKeyboardSwitchPreferenceChangeListener =
|
|
new OnPreferenceChangeListener() {
|
|
@Override
|
|
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
|
mSettings.setShowImeWithHardKeyboard((Boolean) newValue);
|
|
return true;
|
|
}
|
|
};
|
|
|
|
private final ContentObserver mContentObserver = new ContentObserver(new Handler(true)) {
|
|
@Override
|
|
public void onChange(boolean selfChange) {
|
|
updateShowVirtualKeyboardSwitch();
|
|
}
|
|
};
|
|
|
|
@Override
|
|
public void onSetupKeyboardLayouts(InputDeviceIdentifier inputDeviceIdentifier) {
|
|
final Intent intent = new Intent(Intent.ACTION_MAIN);
|
|
intent.setClass(getActivity(), Settings.KeyboardLayoutPickerActivity.class);
|
|
intent.putExtra(KeyboardLayoutPickerFragment.EXTRA_INPUT_DEVICE_IDENTIFIER,
|
|
inputDeviceIdentifier);
|
|
mIntentWaitingForResult = intent;
|
|
startActivityForResult(intent, 0);
|
|
}
|
|
|
|
@Override
|
|
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
|
super.onActivityResult(requestCode, resultCode, data);
|
|
|
|
if (mIntentWaitingForResult != null) {
|
|
InputDeviceIdentifier inputDeviceIdentifier = mIntentWaitingForResult
|
|
.getParcelableExtra(KeyboardLayoutPickerFragment.EXTRA_INPUT_DEVICE_IDENTIFIER);
|
|
mIntentWaitingForResult = null;
|
|
showKeyboardLayoutDialog(inputDeviceIdentifier);
|
|
}
|
|
}
|
|
|
|
private static String getLayoutLabel(@NonNull InputDevice device,
|
|
@NonNull Context context, @NonNull InputManager im) {
|
|
final String currentLayoutDesc =
|
|
im.getCurrentKeyboardLayoutForInputDevice(device.getIdentifier());
|
|
if (currentLayoutDesc == null) {
|
|
return context.getString(R.string.keyboard_layout_default_label);
|
|
}
|
|
final KeyboardLayout currentLayout = im.getKeyboardLayout(currentLayoutDesc);
|
|
if (currentLayout == null) {
|
|
return context.getString(R.string.keyboard_layout_default_label);
|
|
}
|
|
// If current layout is specified but the layout is null, just return an empty string
|
|
// instead of falling back to R.string.keyboard_layout_default_label.
|
|
return TextUtils.emptyIfNull(currentLayout.getLabel());
|
|
}
|
|
|
|
@NonNull
|
|
static List<HardKeyboardDeviceInfo> getHardKeyboards(@NonNull Context context) {
|
|
final List<HardKeyboardDeviceInfo> keyboards = new ArrayList<>();
|
|
final InputManager im = context.getSystemService(InputManager.class);
|
|
if (im == null) {
|
|
return new ArrayList<>();
|
|
}
|
|
for (int deviceId : InputDevice.getDeviceIds()) {
|
|
final InputDevice device = InputDevice.getDevice(deviceId);
|
|
if (device == null || device.isVirtual() || !device.isFullKeyboard()) {
|
|
continue;
|
|
}
|
|
keyboards.add(new HardKeyboardDeviceInfo(
|
|
device.getName(), device.getIdentifier(), getLayoutLabel(device, context, im)));
|
|
}
|
|
|
|
// We intentionally don't reuse Comparator because Collator may not be thread-safe.
|
|
final Collator collator = Collator.getInstance();
|
|
keyboards.sort((a, b) -> {
|
|
int result = collator.compare(a.mDeviceName, b.mDeviceName);
|
|
if (result != 0) {
|
|
return result;
|
|
}
|
|
result = a.mDeviceIdentifier.getDescriptor().compareTo(
|
|
b.mDeviceIdentifier.getDescriptor());
|
|
if (result != 0) {
|
|
return result;
|
|
}
|
|
return collator.compare(a.mLayoutLabel, b.mLayoutLabel);
|
|
});
|
|
return keyboards;
|
|
}
|
|
|
|
public static final class HardKeyboardDeviceInfo {
|
|
@NonNull
|
|
public final String mDeviceName;
|
|
@NonNull
|
|
public final InputDeviceIdentifier mDeviceIdentifier;
|
|
@NonNull
|
|
public final String mLayoutLabel;
|
|
|
|
public HardKeyboardDeviceInfo(
|
|
@Nullable String deviceName,
|
|
@NonNull InputDeviceIdentifier deviceIdentifier,
|
|
@NonNull String layoutLabel) {
|
|
mDeviceName = TextUtils.emptyIfNull(deviceName);
|
|
mDeviceIdentifier = deviceIdentifier;
|
|
mLayoutLabel = layoutLabel;
|
|
}
|
|
|
|
@Override
|
|
public boolean equals(Object o) {
|
|
if (o == this) return true;
|
|
if (o == null) return false;
|
|
|
|
if (!(o instanceof HardKeyboardDeviceInfo)) return false;
|
|
|
|
final HardKeyboardDeviceInfo that = (HardKeyboardDeviceInfo) o;
|
|
if (!TextUtils.equals(mDeviceName, that.mDeviceName)) {
|
|
return false;
|
|
}
|
|
if (!Objects.equals(mDeviceIdentifier, that.mDeviceIdentifier)) {
|
|
return false;
|
|
}
|
|
if (!TextUtils.equals(mLayoutLabel, that.mLayoutLabel)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
|
|
new BaseSearchIndexProvider() {
|
|
@Override
|
|
public List<SearchIndexableResource> getXmlResourcesToIndex(
|
|
Context context, boolean enabled) {
|
|
final SearchIndexableResource sir = new SearchIndexableResource(context);
|
|
sir.xmlResId = R.xml.physical_keyboard_settings;
|
|
return Arrays.asList(sir);
|
|
}
|
|
};
|
|
}
|