Files
app_Settings/src/com/android/settings/inputmethod/PhysicalKeyboardFragment.java
danielwbhuang 1f3261467d Set preference visible to false when there is no keyboard.
1. Physical keyboard settings should only show up if at least one PK is connected.
2. Update tests.
3. Fix NullPointerException.

Bug: 269983475
Bug: 270109384
Bug: 271357910
Test: atest and manual
Change-Id: If7798587fb386dbf669fc249cab304d91a26879b
2023-03-03 18:16:55 +08:00

428 lines
17 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.app.settings.SettingsEnums;
import android.content.ContentResolver;
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.text.TextUtils;
import android.util.FeatureFlagUtils;
import android.view.InputDevice;
import android.view.inputmethod.InputMethodManager;
import androidx.preference.Preference;
import androidx.preference.Preference.OnPreferenceChangeListener;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceScreen;
import androidx.preference.SwitchPreference;
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.core.SubSettingLauncher;
import com.android.settings.search.BaseSearchIndexProvider;
import com.android.settingslib.search.SearchIndexable;
import com.android.settingslib.utils.ThreadUtils;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
@SearchIndexable
public final class PhysicalKeyboardFragment extends SettingsPreferenceFragment
implements InputManager.InputDeviceListener,
KeyboardLayoutDialogFragment.OnSetupKeyboardLayoutsListener {
private static final String KEYBOARD_OPTIONS_CATEGORY = "keyboard_options_category";
private static final String SHOW_VIRTUAL_KEYBOARD_SWITCH = "show_virtual_keyboard_switch";
private static final String KEYBOARD_SHORTCUTS_HELPER = "keyboard_shortcuts_helper";
private static final String MODIFIER_KEYS_SETTINGS = "modifier_keys_settings";
@NonNull
private final ArrayList<HardKeyboardDeviceInfo> mLastHardKeyboards = new ArrayList<>();
private InputManager mIm;
private InputMethodManager mImm;
@NonNull
private PreferenceCategory mKeyboardAssistanceCategory;
@NonNull
private SwitchPreference mShowVirtualKeyboardSwitch;
private Intent mIntentWaitingForResult;
private boolean mIsNewKeyboardSettings;
static final String EXTRA_BT_ADDRESS = "extra_bt_address";
private String mBluetoothAddress;
@Override
public void onCreatePreferences(Bundle bundle, String s) {
Activity activity = Preconditions.checkNotNull(getActivity());
mBluetoothAddress = activity.getIntent().getStringExtra(EXTRA_BT_ADDRESS);
addPreferencesFromResource(R.xml.physical_keyboard_settings);
mIm = Preconditions.checkNotNull(activity.getSystemService(InputManager.class));
mImm = Preconditions.checkNotNull(activity.getSystemService(InputMethodManager.class));
mKeyboardAssistanceCategory = Preconditions.checkNotNull(
(PreferenceCategory) findPreference(KEYBOARD_OPTIONS_CATEGORY));
mShowVirtualKeyboardSwitch = Preconditions.checkNotNull(
(SwitchPreference) mKeyboardAssistanceCategory.findPreference(
SHOW_VIRTUAL_KEYBOARD_SWITCH));
mIsNewKeyboardSettings = FeatureFlagUtils.isEnabled(
getContext(), FeatureFlagUtils.SETTINGS_NEW_KEYBOARD_UI);
boolean isModifierKeySettingsEnabled = FeatureFlagUtils
.isEnabled(getContext(), FeatureFlagUtils.SETTINGS_NEW_KEYBOARD_MODIFIER_KEY);
if (!isModifierKeySettingsEnabled) {
mKeyboardAssistanceCategory.removePreference(findPreference(MODIFIER_KEYS_SETTINGS));
}
}
@Override
public boolean onPreferenceTreeClick(Preference preference) {
if (KEYBOARD_SHORTCUTS_HELPER.equals(preference.getKey())) {
writePreferenceClickMetric(preference);
toggleKeyboardShortcutsMenu();
return true;
}
return super.onPreferenceTreeClick(preference);
}
@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 SettingsEnums.PHYSICAL_KEYBOARDS;
}
private void scheduleUpdateHardKeyboards() {
final Context context = getContext();
ThreadUtils.postOnBackgroundThread(() -> {
final List<HardKeyboardDeviceInfo> newHardKeyboards = getHardKeyboards(context);
if (newHardKeyboards.isEmpty()) {
getActivity().finish();
return;
}
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) {
// if user go into this page from Connected devices entry, we should distinguish the
// user-selected keyboard from all enabled keyboards.
if (mBluetoothAddress != null
&& !mBluetoothAddress.equals(hardKeyboardDeviceInfo.mBluetoothAddress)) {
continue;
}
// TODO(yukawa): Consider using com.android.settings.widget.GearPreference
final Preference pref = new Preference(getPrefContext());
pref.setTitle(hardKeyboardDeviceInfo.mDeviceName);
if (mIsNewKeyboardSettings) {
List<String> suitableImes = new ArrayList<>();
suitableImes.addAll(
NewKeyboardSettingsUtils.getSuitableImeLabels(
getContext(), mImm, UserHandle.myUserId()));
if (!suitableImes.isEmpty()) {
String summary = suitableImes.get(0);
StringBuilder result = new StringBuilder(summary);
for (int i = 1; i < suitableImes.size(); i++) {
result.append(", ").append(suitableImes.get(i));
}
pref.setSummary(result.toString());
} else {
pref.setSummary(hardKeyboardDeviceInfo.mLayoutLabel);
}
pref.setOnPreferenceClickListener(
preference -> {
showEnabledLocalesKeyboardLayoutList(
hardKeyboardDeviceInfo.mDeviceName,
hardKeyboardDeviceInfo.mDeviceIdentifier);
return true;
});
} else {
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 = new KeyboardLayoutDialogFragment(
inputDeviceIdentifier);
fragment.setTargetFragment(this, 0);
fragment.show(getActivity().getSupportFragmentManager(), "keyboardLayout");
}
private void showEnabledLocalesKeyboardLayoutList(String keyboardName,
InputDeviceIdentifier inputDeviceIdentifier) {
Bundle arguments = new Bundle();
arguments.putParcelable(NewKeyboardSettingsUtils.EXTRA_INPUT_DEVICE_IDENTIFIER,
inputDeviceIdentifier);
arguments.putString(NewKeyboardSettingsUtils.EXTRA_KEYBOARD_DEVICE_NAME, keyboardName);
new SubSettingLauncher(getContext())
.setSourceMetricsCategory(getMetricsCategory())
.setDestination(NewKeyboardLayoutEnabledLocalesFragment.class.getName())
.setArguments(arguments)
.launch();
}
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(
Secure.getInt(getContentResolver(), Secure.SHOW_IME_WITH_HARD_KEYBOARD, 0) != 0);
}
private void toggleKeyboardShortcutsMenu() {
getActivity().requestShowKeyboardShortcuts();
}
private final OnPreferenceChangeListener mShowVirtualKeyboardSwitchPreferenceChangeListener =
(preference, newValue) -> {
final ContentResolver cr = getContentResolver();
Secure.putInt(cr, Secure.SHOW_IME_WITH_HARD_KEYBOARD, ((Boolean) newValue) ? 1 : 0);
cr.notifyChange(Secure.getUriFor(Secure.SHOW_IME_WITH_HARD_KEYBOARD),
null /* observer */, ContentResolver.NOTIFY_NO_DELAY);
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),
device.getBluetoothAddress()));
}
// 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;
@Nullable
public final String mBluetoothAddress;
public HardKeyboardDeviceInfo(
@Nullable String deviceName,
@NonNull InputDeviceIdentifier deviceIdentifier,
@NonNull String layoutLabel,
@Nullable String bluetoothAddress) {
mDeviceName = TextUtils.emptyIfNull(deviceName);
mDeviceIdentifier = deviceIdentifier;
mLayoutLabel = layoutLabel;
mBluetoothAddress = bluetoothAddress;
}
@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;
}
if (!TextUtils.equals(mBluetoothAddress, that.mBluetoothAddress)) {
return false;
}
return true;
}
}
public static final BaseSearchIndexProvider 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);
}
};
}