/* * 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.LoaderManager; import android.content.AsyncTaskLoader; import android.content.Context; import android.content.Intent; import android.content.Loader; 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.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 android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodSubtype; import com.android.internal.inputmethod.InputMethodUtils; import com.android.internal.logging.MetricsProto.MetricsEvent; import com.android.internal.util.Preconditions; import com.android.settings.R; import com.android.settings.Settings; import com.android.settings.SettingsPreferenceFragment; import java.text.Collator; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Objects; public final class PhysicalKeyboardFragment extends SettingsPreferenceFragment implements InputManager.InputDeviceListener { 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"; private static final String IM_SUBTYPE_MODE_KEYBOARD = "keyboard"; @NonNull private final List<HardKeyboardDeviceInfo> mLastHardKeyboards = new ArrayList<>(); @NonNull private final List<KeyboardInfoPreference> mTempKeyboardInfoList = new ArrayList<>(); @NonNull private final HashSet<Integer> mLoaderIDs = new HashSet<>(); private int mNextLoaderId = 0; private InputManager mIm; @NonNull private PreferenceCategory mKeyboardAssistanceCategory; @NonNull private SwitchPreference mShowVirtualKeyboardSwitch; @NonNull private InputMethodUtils.InputMethodSettings mSettings; @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(); clearLoader(); mLastHardKeyboards.clear(); updateHardKeyboards(); mIm.registerInputDeviceListener(this, null); mShowVirtualKeyboardSwitch.setOnPreferenceChangeListener( mShowVirtualKeyboardSwitchPreferenceChangeListener); registerShowVirtualKeyboardSettingsObserver(); } @Override public void onPause() { super.onPause(); clearLoader(); mLastHardKeyboards.clear(); mIm.unregisterInputDeviceListener(this); mShowVirtualKeyboardSwitch.setOnPreferenceChangeListener(null); unregisterShowVirtualKeyboardSettingsObserver(); } public void onLoadFinishedInternal( final int loaderId, @NonNull final List<Keyboards> keyboardsList) { if (!mLoaderIDs.remove(loaderId)) { // Already destroyed loader. Ignore. return; } Collections.sort(keyboardsList); final PreferenceScreen preferenceScreen = getPreferenceScreen(); preferenceScreen.removeAll(); for (Keyboards keyboards : keyboardsList) { final PreferenceCategory category = new PreferenceCategory(getPrefContext(), null); category.setTitle(keyboards.mDeviceInfo.mDeviceName); category.setOrder(0); preferenceScreen.addPreference(category); for (Keyboards.KeyboardInfo info : keyboards.mKeyboardInfoList) { mTempKeyboardInfoList.clear(); final InputMethodInfo imi = info.mImi; final InputMethodSubtype imSubtype = info.mImSubtype; if (imi != null) { KeyboardInfoPreference pref = new KeyboardInfoPreference(getPrefContext(), info); pref.setOnPreferenceClickListener(preference -> { showKeyboardLayoutScreen( keyboards.mDeviceInfo.mDeviceIdentifier, imi, imSubtype); return true; }); mTempKeyboardInfoList.add(pref); Collections.sort(mTempKeyboardInfoList); } for (KeyboardInfoPreference pref : mTempKeyboardInfoList) { category.addPreference(pref); } } } mTempKeyboardInfoList.clear(); mKeyboardAssistanceCategory.setOrder(1); preferenceScreen.addPreference(mKeyboardAssistanceCategory); updateShowVirtualKeyboardSwitch(); } @Override public void onInputDeviceAdded(int deviceId) { updateHardKeyboards(); } @Override public void onInputDeviceRemoved(int deviceId) { updateHardKeyboards(); } @Override public void onInputDeviceChanged(int deviceId) { updateHardKeyboards(); } @Override protected int getMetricsCategory() { return MetricsEvent.PHYSICAL_KEYBOARDS; } @NonNull private static ArrayList<HardKeyboardDeviceInfo> getHardKeyboards() { final ArrayList<HardKeyboardDeviceInfo> keyboards = new ArrayList<>(); final int[] devicesIds = InputDevice.getDeviceIds(); for (int deviceId : devicesIds) { final InputDevice device = InputDevice.getDevice(deviceId); if (device != null && !device.isVirtual() && device.isFullKeyboard()) { keyboards.add(new HardKeyboardDeviceInfo(device.getName(), device.getIdentifier())); } } return keyboards; } private void updateHardKeyboards() { final ArrayList<HardKeyboardDeviceInfo> newHardKeyboards = getHardKeyboards(); if (!Objects.equals(newHardKeyboards, mLastHardKeyboards)) { clearLoader(); mLastHardKeyboards.clear(); mLastHardKeyboards.addAll(newHardKeyboards); mLoaderIDs.add(mNextLoaderId); getLoaderManager().initLoader(mNextLoaderId, null, new Callbacks(getContext(), this, mLastHardKeyboards)); ++mNextLoaderId; } } private void showKeyboardLayoutScreen( @NonNull InputDeviceIdentifier inputDeviceIdentifier, @NonNull InputMethodInfo imi, @Nullable InputMethodSubtype imSubtype) { final Intent intent = new Intent(Intent.ACTION_MAIN); intent.setClass(getActivity(), Settings.KeyboardLayoutPickerActivity.class); intent.putExtra(KeyboardLayoutPickerFragment2.EXTRA_INPUT_DEVICE_IDENTIFIER, inputDeviceIdentifier); intent.putExtra(KeyboardLayoutPickerFragment2.EXTRA_INPUT_METHOD_INFO, imi); intent.putExtra(KeyboardLayoutPickerFragment2.EXTRA_INPUT_METHOD_SUBTYPE, imSubtype); startActivity(intent); } private void clearLoader() { for (final int loaderId : mLoaderIDs) { getLoaderManager().destroyLoader(loaderId); } mLoaderIDs.clear(); } 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(); } }; private static final class Callbacks implements LoaderManager.LoaderCallbacks<List<Keyboards>> { @NonNull final Context mContext; @NonNull final PhysicalKeyboardFragment mPhysicalKeyboardFragment; @NonNull final List<HardKeyboardDeviceInfo> mHardKeyboards; public Callbacks( @NonNull Context context, @NonNull PhysicalKeyboardFragment physicalKeyboardFragment, @NonNull List<HardKeyboardDeviceInfo> hardKeyboards) { mContext = context; mPhysicalKeyboardFragment = physicalKeyboardFragment; mHardKeyboards = hardKeyboards; } @Override public Loader<List<Keyboards>> onCreateLoader(int id, Bundle args) { return new KeyboardLayoutLoader(mContext, mHardKeyboards); } @Override public void onLoadFinished(Loader<List<Keyboards>> loader, List<Keyboards> data) { mPhysicalKeyboardFragment.onLoadFinishedInternal(loader.getId(), data); } @Override public void onLoaderReset(Loader<List<Keyboards>> loader) { } } private static final class KeyboardLayoutLoader extends AsyncTaskLoader<List<Keyboards>> { @NonNull private final List<HardKeyboardDeviceInfo> mHardKeyboards; public KeyboardLayoutLoader( @NonNull Context context, @NonNull List<HardKeyboardDeviceInfo> hardKeyboards) { super(context); mHardKeyboards = Preconditions.checkNotNull(hardKeyboards); } private Keyboards loadInBackground(HardKeyboardDeviceInfo deviceInfo) { final ArrayList<Keyboards.KeyboardInfo> keyboardInfoList = new ArrayList<>(); final InputMethodManager imm = getContext().getSystemService(InputMethodManager.class); final InputManager im = getContext().getSystemService(InputManager.class); if (imm != null && im != null) { for (InputMethodInfo imi : imm.getEnabledInputMethodList()) { final List<InputMethodSubtype> subtypes = imm.getEnabledInputMethodSubtypeList( imi, true /* allowsImplicitlySelectedSubtypes */); if (subtypes.isEmpty()) { // Here we use null to indicate that this IME has no subtype. final InputMethodSubtype nullSubtype = null; final KeyboardLayout layout = im.getKeyboardLayoutForInputDevice( deviceInfo.mDeviceIdentifier, imi, nullSubtype); keyboardInfoList.add(new Keyboards.KeyboardInfo(imi, nullSubtype, layout)); continue; } // If the IME supports subtypes, we pick up "keyboard" subtypes only. final int N = subtypes.size(); for (int i = 0; i < N; ++i) { final InputMethodSubtype subtype = subtypes.get(i); if (!IM_SUBTYPE_MODE_KEYBOARD.equalsIgnoreCase(subtype.getMode())) { continue; } final KeyboardLayout layout = im.getKeyboardLayoutForInputDevice( deviceInfo.mDeviceIdentifier, imi, subtype); keyboardInfoList.add(new Keyboards.KeyboardInfo(imi, subtype, layout)); } } } return new Keyboards(deviceInfo, keyboardInfoList); } @Override public List<Keyboards> loadInBackground() { List<Keyboards> keyboardsList = new ArrayList<>(mHardKeyboards.size()); for (HardKeyboardDeviceInfo deviceInfo : mHardKeyboards) { keyboardsList.add(loadInBackground(deviceInfo)); } return keyboardsList; } @Override protected void onStartLoading() { super.onStartLoading(); forceLoad(); } @Override protected void onStopLoading() { super.onStopLoading(); cancelLoad(); } } public static final class HardKeyboardDeviceInfo { @NonNull public final String mDeviceName; @NonNull public final InputDeviceIdentifier mDeviceIdentifier; public HardKeyboardDeviceInfo( @Nullable final String deviceName, @NonNull final InputDeviceIdentifier deviceIdentifier) { mDeviceName = deviceName != null ? deviceName : ""; mDeviceIdentifier = deviceIdentifier; } @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 (mDeviceIdentifier.getVendorId() != that.mDeviceIdentifier.getVendorId()) { return false; } if (mDeviceIdentifier.getProductId() != that.mDeviceIdentifier.getProductId()) { return false; } if (!TextUtils.equals(mDeviceIdentifier.getDescriptor(), that.mDeviceIdentifier.getDescriptor())) { return false; } return true; } } public static final class Keyboards implements Comparable<Keyboards> { @NonNull public final HardKeyboardDeviceInfo mDeviceInfo; @NonNull public final ArrayList<KeyboardInfo> mKeyboardInfoList; @NonNull public final Collator mCollator = Collator.getInstance(); public Keyboards( @NonNull final HardKeyboardDeviceInfo deviceInfo, @NonNull final ArrayList<KeyboardInfo> keyboardInfoList) { mDeviceInfo = deviceInfo; mKeyboardInfoList = keyboardInfoList; } @Override public int compareTo(@NonNull Keyboards another) { return mCollator.compare(mDeviceInfo.mDeviceName, another.mDeviceInfo.mDeviceName); } public static final class KeyboardInfo { @NonNull public final InputMethodInfo mImi; @Nullable public final InputMethodSubtype mImSubtype; @NonNull public final KeyboardLayout mLayout; public KeyboardInfo( @NonNull final InputMethodInfo imi, @Nullable final InputMethodSubtype imSubtype, @NonNull final KeyboardLayout layout) { mImi = imi; mImSubtype = imSubtype; mLayout = layout; } } } static final class KeyboardInfoPreference extends Preference { @NonNull private final CharSequence mImeName; @Nullable private final CharSequence mImSubtypeName; @NonNull private final Collator collator = Collator.getInstance(); private KeyboardInfoPreference( @NonNull Context context, @NonNull Keyboards.KeyboardInfo info) { super(context); mImeName = info.mImi.loadLabel(context.getPackageManager()); mImSubtypeName = getImSubtypeName(context, info.mImi, info.mImSubtype); setTitle(formatDisplayName(context, mImeName, mImSubtypeName)); if (info.mLayout != null) { setSummary(info.mLayout.getLabel()); } } @NonNull static CharSequence getDisplayName( @NonNull Context context, @NonNull InputMethodInfo imi, @Nullable InputMethodSubtype imSubtype) { final CharSequence imeName = imi.loadLabel(context.getPackageManager()); final CharSequence imSubtypeName = getImSubtypeName(context, imi, imSubtype); return formatDisplayName(context, imeName, imSubtypeName); } private static CharSequence formatDisplayName( @NonNull Context context, @NonNull CharSequence imeName, @Nullable CharSequence imSubtypeName) { if (imSubtypeName == null) { return imeName; } return String.format( context.getString(R.string.physical_device_title), imeName, imSubtypeName); } @Nullable private static CharSequence getImSubtypeName( @NonNull Context context, @NonNull InputMethodInfo imi, @Nullable InputMethodSubtype imSubtype) { if (imSubtype != null) { return InputMethodAndSubtypeUtil.getSubtypeLocaleNameAsSentence( imSubtype, context, imi); } return null; } @Override public int compareTo(@NonNull Preference object) { if (!(object instanceof KeyboardInfoPreference)) { return super.compareTo(object); } KeyboardInfoPreference another = (KeyboardInfoPreference) object; int result = compare(mImeName, another.mImeName); if (result == 0) { result = compare(mImSubtypeName, another.mImSubtypeName); } return result; } private int compare(@Nullable CharSequence lhs, @Nullable CharSequence rhs) { if (!TextUtils.isEmpty(lhs) && !TextUtils.isEmpty(rhs)) { return collator.compare(lhs.toString(), rhs.toString()); } else if (TextUtils.isEmpty(lhs) && TextUtils.isEmpty(rhs)) { return 0; } else if (!TextUtils.isEmpty(lhs)) { return -1; } else { return 1; } } } }