blob: 936de38ecac560a0d4f84b57c4e8584c6b535e28 [file] [log] [blame]
/*
* 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";
private static final String EXTRA_AUTO_SELECTION = "auto_selection";
@NonNull
private final ArrayList<HardKeyboardDeviceInfo> mLastHardKeyboards = new ArrayList<>();
private InputManager mIm;
private InputMethodManager mImm;
private InputDeviceIdentifier mAutoInputDeviceIdentifier;
@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 onSaveInstanceState(Bundle outState) {
outState.putParcelable(EXTRA_AUTO_SELECTION, mAutoInputDeviceIdentifier);
super.onSaveInstanceState(outState);
}
@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));
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));
}
InputDeviceIdentifier inputDeviceIdentifier = activity.getIntent().getParcelableExtra(
KeyboardLayoutPickerFragment.EXTRA_INPUT_DEVICE_IDENTIFIER);
// TODO (b/271391879): The EXTRA_INTENT_FROM is used for the future metrics.
if (inputDeviceIdentifier != null) {
mAutoInputDeviceIdentifier = inputDeviceIdentifier;
}
// Don't repeat the autoselection.
if (isAutoSelection(bundle, inputDeviceIdentifier)) {
showEnabledLocalesKeyboardLayoutList(inputDeviceIdentifier);
}
}
private static boolean isAutoSelection(Bundle bundle, InputDeviceIdentifier identifier) {
if (bundle != null && bundle.getParcelable(EXTRA_AUTO_SELECTION) != null) {
return false;
}
return identifier != null;
}
@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) {
// 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.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(InputDeviceIdentifier inputDeviceIdentifier) {
Bundle arguments = new Bundle();
arguments.putParcelable(NewKeyboardSettingsUtils.EXTRA_INPUT_DEVICE_IDENTIFIER,
inputDeviceIdentifier);
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);
}
};
}