Home | History | Annotate | Download | only in inputmethod
      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.app.LoaderManager;
     23 import android.content.AsyncTaskLoader;
     24 import android.content.Context;
     25 import android.content.Intent;
     26 import android.content.Loader;
     27 import android.database.ContentObserver;
     28 import android.hardware.input.InputDeviceIdentifier;
     29 import android.hardware.input.InputManager;
     30 import android.hardware.input.KeyboardLayout;
     31 import android.os.Bundle;
     32 import android.os.Handler;
     33 import android.os.UserHandle;
     34 import android.provider.Settings.Secure;
     35 import android.support.v14.preference.SwitchPreference;
     36 import android.support.v7.preference.Preference;
     37 import android.support.v7.preference.Preference.OnPreferenceChangeListener;
     38 import android.support.v7.preference.PreferenceCategory;
     39 import android.support.v7.preference.PreferenceScreen;
     40 import android.text.TextUtils;
     41 import android.view.InputDevice;
     42 import android.view.inputmethod.InputMethodInfo;
     43 import android.view.inputmethod.InputMethodManager;
     44 import android.view.inputmethod.InputMethodSubtype;
     45 
     46 import com.android.internal.inputmethod.InputMethodUtils;
     47 import com.android.internal.logging.MetricsProto.MetricsEvent;
     48 import com.android.internal.util.Preconditions;
     49 import com.android.settings.R;
     50 import com.android.settings.Settings;
     51 import com.android.settings.SettingsPreferenceFragment;
     52 
     53 import java.text.Collator;
     54 import java.util.ArrayList;
     55 import java.util.Collections;
     56 import java.util.HashMap;
     57 import java.util.HashSet;
     58 import java.util.List;
     59 import java.util.Objects;
     60 
     61 public final class PhysicalKeyboardFragment extends SettingsPreferenceFragment
     62         implements InputManager.InputDeviceListener {
     63 
     64     private static final String KEYBOARD_ASSISTANCE_CATEGORY = "keyboard_assistance_category";
     65     private static final String SHOW_VIRTUAL_KEYBOARD_SWITCH = "show_virtual_keyboard_switch";
     66     private static final String KEYBOARD_SHORTCUTS_HELPER = "keyboard_shortcuts_helper";
     67     private static final String IM_SUBTYPE_MODE_KEYBOARD = "keyboard";
     68 
     69     @NonNull
     70     private final List<HardKeyboardDeviceInfo> mLastHardKeyboards = new ArrayList<>();
     71     @NonNull
     72     private final List<KeyboardInfoPreference> mTempKeyboardInfoList = new ArrayList<>();
     73 
     74     @NonNull
     75     private final HashSet<Integer> mLoaderIDs = new HashSet<>();
     76     private int mNextLoaderId = 0;
     77 
     78     private InputManager mIm;
     79     @NonNull
     80     private PreferenceCategory mKeyboardAssistanceCategory;
     81     @NonNull
     82     private SwitchPreference mShowVirtualKeyboardSwitch;
     83     @NonNull
     84     private InputMethodUtils.InputMethodSettings mSettings;
     85 
     86     @Override
     87     public void onCreatePreferences(Bundle bundle, String s) {
     88         Activity activity = Preconditions.checkNotNull(getActivity());
     89         addPreferencesFromResource(R.xml.physical_keyboard_settings);
     90         mIm = Preconditions.checkNotNull(activity.getSystemService(InputManager.class));
     91         mSettings = new InputMethodUtils.InputMethodSettings(
     92                 activity.getResources(),
     93                 getContentResolver(),
     94                 new HashMap<>(),
     95                 new ArrayList<>(),
     96                 UserHandle.myUserId(),
     97                 false /* copyOnWrite */);
     98         mKeyboardAssistanceCategory = Preconditions.checkNotNull(
     99                 (PreferenceCategory) findPreference(KEYBOARD_ASSISTANCE_CATEGORY));
    100         mShowVirtualKeyboardSwitch = Preconditions.checkNotNull(
    101                 (SwitchPreference) mKeyboardAssistanceCategory.findPreference(
    102                         SHOW_VIRTUAL_KEYBOARD_SWITCH));
    103         findPreference(KEYBOARD_SHORTCUTS_HELPER).setOnPreferenceClickListener(
    104                 new Preference.OnPreferenceClickListener() {
    105                     @Override
    106                     public boolean onPreferenceClick(Preference preference) {
    107                         toggleKeyboardShortcutsMenu();
    108                         return true;
    109                     }
    110                 });
    111     }
    112 
    113     @Override
    114     public void onResume() {
    115         super.onResume();
    116         clearLoader();
    117         mLastHardKeyboards.clear();
    118         updateHardKeyboards();
    119         mIm.registerInputDeviceListener(this, null);
    120         mShowVirtualKeyboardSwitch.setOnPreferenceChangeListener(
    121                 mShowVirtualKeyboardSwitchPreferenceChangeListener);
    122         registerShowVirtualKeyboardSettingsObserver();
    123     }
    124 
    125     @Override
    126     public void onPause() {
    127         super.onPause();
    128         clearLoader();
    129         mLastHardKeyboards.clear();
    130         mIm.unregisterInputDeviceListener(this);
    131         mShowVirtualKeyboardSwitch.setOnPreferenceChangeListener(null);
    132         unregisterShowVirtualKeyboardSettingsObserver();
    133     }
    134 
    135     public void onLoadFinishedInternal(
    136             final int loaderId, @NonNull final List<Keyboards> keyboardsList) {
    137         if (!mLoaderIDs.remove(loaderId)) {
    138             // Already destroyed loader.  Ignore.
    139             return;
    140         }
    141 
    142         Collections.sort(keyboardsList);
    143         final PreferenceScreen preferenceScreen = getPreferenceScreen();
    144         preferenceScreen.removeAll();
    145         for (Keyboards keyboards : keyboardsList) {
    146             final PreferenceCategory category = new PreferenceCategory(getPrefContext(), null);
    147             category.setTitle(keyboards.mDeviceInfo.mDeviceName);
    148             category.setOrder(0);
    149             preferenceScreen.addPreference(category);
    150             for (Keyboards.KeyboardInfo info : keyboards.mKeyboardInfoList) {
    151                 mTempKeyboardInfoList.clear();
    152                 final InputMethodInfo imi = info.mImi;
    153                 final InputMethodSubtype imSubtype = info.mImSubtype;
    154                 if (imi != null) {
    155                     KeyboardInfoPreference pref =
    156                             new KeyboardInfoPreference(getPrefContext(), info);
    157                     pref.setOnPreferenceClickListener(preference -> {
    158                         showKeyboardLayoutScreen(
    159                                 keyboards.mDeviceInfo.mDeviceIdentifier, imi, imSubtype);
    160                         return true;
    161                     });
    162                     mTempKeyboardInfoList.add(pref);
    163                     Collections.sort(mTempKeyboardInfoList);
    164                 }
    165                 for (KeyboardInfoPreference pref : mTempKeyboardInfoList) {
    166                     category.addPreference(pref);
    167                 }
    168             }
    169         }
    170         mTempKeyboardInfoList.clear();
    171         mKeyboardAssistanceCategory.setOrder(1);
    172         preferenceScreen.addPreference(mKeyboardAssistanceCategory);
    173         updateShowVirtualKeyboardSwitch();
    174     }
    175 
    176     @Override
    177     public void onInputDeviceAdded(int deviceId) {
    178         updateHardKeyboards();
    179     }
    180 
    181     @Override
    182     public void onInputDeviceRemoved(int deviceId) {
    183         updateHardKeyboards();
    184     }
    185 
    186     @Override
    187     public void onInputDeviceChanged(int deviceId) {
    188         updateHardKeyboards();
    189     }
    190 
    191     @Override
    192     protected int getMetricsCategory() {
    193         return MetricsEvent.PHYSICAL_KEYBOARDS;
    194     }
    195 
    196     @NonNull
    197     private static ArrayList<HardKeyboardDeviceInfo> getHardKeyboards() {
    198         final ArrayList<HardKeyboardDeviceInfo> keyboards = new ArrayList<>();
    199         final int[] devicesIds = InputDevice.getDeviceIds();
    200         for (int deviceId : devicesIds) {
    201             final InputDevice device = InputDevice.getDevice(deviceId);
    202             if (device != null && !device.isVirtual() && device.isFullKeyboard()) {
    203                 keyboards.add(new HardKeyboardDeviceInfo(device.getName(), device.getIdentifier()));
    204             }
    205         }
    206         return keyboards;
    207     }
    208 
    209     private void updateHardKeyboards() {
    210         final ArrayList<HardKeyboardDeviceInfo> newHardKeyboards = getHardKeyboards();
    211         if (!Objects.equals(newHardKeyboards, mLastHardKeyboards)) {
    212             clearLoader();
    213             mLastHardKeyboards.clear();
    214             mLastHardKeyboards.addAll(newHardKeyboards);
    215             getLoaderManager().initLoader(mNextLoaderId, null,
    216                     new Callbacks(getContext(), this, mLastHardKeyboards));
    217             mLoaderIDs.add(mNextLoaderId);
    218             ++mNextLoaderId;
    219         }
    220     }
    221 
    222     private void showKeyboardLayoutScreen(
    223             @NonNull InputDeviceIdentifier inputDeviceIdentifier,
    224             @NonNull InputMethodInfo imi,
    225             @Nullable InputMethodSubtype imSubtype) {
    226         final Intent intent = new Intent(Intent.ACTION_MAIN);
    227         intent.setClass(getActivity(), Settings.KeyboardLayoutPickerActivity.class);
    228         intent.putExtra(KeyboardLayoutPickerFragment2.EXTRA_INPUT_DEVICE_IDENTIFIER,
    229                 inputDeviceIdentifier);
    230         intent.putExtra(KeyboardLayoutPickerFragment2.EXTRA_INPUT_METHOD_INFO, imi);
    231         intent.putExtra(KeyboardLayoutPickerFragment2.EXTRA_INPUT_METHOD_SUBTYPE, imSubtype);
    232         startActivity(intent);
    233     }
    234 
    235     private void clearLoader() {
    236         for (final int loaderId : mLoaderIDs) {
    237             getLoaderManager().destroyLoader(loaderId);
    238         }
    239         mLoaderIDs.clear();
    240     }
    241 
    242     private void registerShowVirtualKeyboardSettingsObserver() {
    243         unregisterShowVirtualKeyboardSettingsObserver();
    244         getActivity().getContentResolver().registerContentObserver(
    245                 Secure.getUriFor(Secure.SHOW_IME_WITH_HARD_KEYBOARD),
    246                 false,
    247                 mContentObserver,
    248                 UserHandle.myUserId());
    249         updateShowVirtualKeyboardSwitch();
    250     }
    251 
    252     private void unregisterShowVirtualKeyboardSettingsObserver() {
    253         getActivity().getContentResolver().unregisterContentObserver(mContentObserver);
    254     }
    255 
    256     private void updateShowVirtualKeyboardSwitch() {
    257         mShowVirtualKeyboardSwitch.setChecked(mSettings.isShowImeWithHardKeyboardEnabled());
    258     }
    259 
    260     private void toggleKeyboardShortcutsMenu() {
    261         getActivity().requestShowKeyboardShortcuts();
    262     }
    263 
    264     private final OnPreferenceChangeListener mShowVirtualKeyboardSwitchPreferenceChangeListener =
    265             new OnPreferenceChangeListener() {
    266                 @Override
    267                 public boolean onPreferenceChange(Preference preference, Object newValue) {
    268                     mSettings.setShowImeWithHardKeyboard((Boolean) newValue);
    269                     return false;
    270                 }
    271             };
    272 
    273     private final ContentObserver mContentObserver = new ContentObserver(new Handler(true)) {
    274         @Override
    275         public void onChange(boolean selfChange) {
    276             updateShowVirtualKeyboardSwitch();
    277         }
    278     };
    279 
    280     private static final class Callbacks implements LoaderManager.LoaderCallbacks<List<Keyboards>> {
    281         @NonNull
    282         final Context mContext;
    283         @NonNull
    284         final PhysicalKeyboardFragment mPhysicalKeyboardFragment;
    285         @NonNull
    286         final List<HardKeyboardDeviceInfo> mHardKeyboards;
    287         public Callbacks(
    288                 @NonNull Context context,
    289                 @NonNull PhysicalKeyboardFragment physicalKeyboardFragment,
    290                 @NonNull List<HardKeyboardDeviceInfo> hardKeyboards) {
    291             mContext = context;
    292             mPhysicalKeyboardFragment = physicalKeyboardFragment;
    293             mHardKeyboards = hardKeyboards;
    294         }
    295 
    296         @Override
    297         public Loader<List<Keyboards>> onCreateLoader(int id, Bundle args) {
    298             return new KeyboardLayoutLoader(mContext, mHardKeyboards);
    299         }
    300 
    301         @Override
    302         public void onLoadFinished(Loader<List<Keyboards>> loader, List<Keyboards> data) {
    303             mPhysicalKeyboardFragment.onLoadFinishedInternal(loader.getId(), data);
    304         }
    305 
    306         @Override
    307         public void onLoaderReset(Loader<List<Keyboards>> loader) {
    308         }
    309     }
    310 
    311     private static final class KeyboardLayoutLoader extends AsyncTaskLoader<List<Keyboards>> {
    312         @NonNull
    313         private final List<HardKeyboardDeviceInfo> mHardKeyboards;
    314 
    315         public KeyboardLayoutLoader(
    316                 @NonNull Context context,
    317                 @NonNull List<HardKeyboardDeviceInfo> hardKeyboards) {
    318             super(context);
    319             mHardKeyboards = Preconditions.checkNotNull(hardKeyboards);
    320         }
    321 
    322         private Keyboards loadInBackground(HardKeyboardDeviceInfo deviceInfo) {
    323             final ArrayList<Keyboards.KeyboardInfo> keyboardInfoList = new ArrayList<>();
    324             final InputMethodManager imm = getContext().getSystemService(InputMethodManager.class);
    325             final InputManager im = getContext().getSystemService(InputManager.class);
    326             if (imm != null && im != null) {
    327                 for (InputMethodInfo imi : imm.getEnabledInputMethodList()) {
    328                     final List<InputMethodSubtype> subtypes = imm.getEnabledInputMethodSubtypeList(
    329                             imi, true /* allowsImplicitlySelectedSubtypes */);
    330                     if (subtypes.isEmpty()) {
    331                         // Here we use null to indicate that this IME has no subtype.
    332                         final InputMethodSubtype nullSubtype = null;
    333                         final KeyboardLayout layout = im.getKeyboardLayoutForInputDevice(
    334                                 deviceInfo.mDeviceIdentifier, imi, nullSubtype);
    335                         keyboardInfoList.add(new Keyboards.KeyboardInfo(imi, nullSubtype, layout));
    336                         continue;
    337                     }
    338 
    339                     // If the IME supports subtypes, we pick up "keyboard" subtypes only.
    340                     final int N = subtypes.size();
    341                     for (int i = 0; i < N; ++i) {
    342                         final InputMethodSubtype subtype = subtypes.get(i);
    343                         if (!IM_SUBTYPE_MODE_KEYBOARD.equalsIgnoreCase(subtype.getMode())) {
    344                             continue;
    345                         }
    346                         final KeyboardLayout layout = im.getKeyboardLayoutForInputDevice(
    347                                 deviceInfo.mDeviceIdentifier, imi, subtype);
    348                         keyboardInfoList.add(new Keyboards.KeyboardInfo(imi, subtype, layout));
    349                     }
    350                 }
    351             }
    352             return new Keyboards(deviceInfo, keyboardInfoList);
    353         }
    354 
    355         @Override
    356         public List<Keyboards> loadInBackground() {
    357             List<Keyboards> keyboardsList = new ArrayList<>(mHardKeyboards.size());
    358             for (HardKeyboardDeviceInfo deviceInfo : mHardKeyboards) {
    359                 keyboardsList.add(loadInBackground(deviceInfo));
    360             }
    361             return keyboardsList;
    362         }
    363 
    364         @Override
    365         protected void onStartLoading() {
    366             super.onStartLoading();
    367             forceLoad();
    368         }
    369 
    370         @Override
    371         protected void onStopLoading() {
    372             super.onStopLoading();
    373             cancelLoad();
    374         }
    375     }
    376 
    377     public static final class HardKeyboardDeviceInfo {
    378         @NonNull
    379         public final String mDeviceName;
    380         @NonNull
    381         public final InputDeviceIdentifier mDeviceIdentifier;
    382 
    383         public HardKeyboardDeviceInfo(
    384                 @Nullable final String deviceName,
    385                 @NonNull final InputDeviceIdentifier deviceIdentifier) {
    386             mDeviceName = deviceName != null ? deviceName : "";
    387             mDeviceIdentifier = deviceIdentifier;
    388         }
    389 
    390         @Override
    391         public boolean equals(Object o) {
    392             if (o == this) return true;
    393             if (o == null) return false;
    394 
    395             if (!(o instanceof HardKeyboardDeviceInfo)) return false;
    396 
    397             final HardKeyboardDeviceInfo that = (HardKeyboardDeviceInfo) o;
    398             if (!TextUtils.equals(mDeviceName, that.mDeviceName)) {
    399                 return false;
    400             }
    401             if (mDeviceIdentifier.getVendorId() != that.mDeviceIdentifier.getVendorId()) {
    402                 return false;
    403             }
    404             if (mDeviceIdentifier.getProductId() != that.mDeviceIdentifier.getProductId()) {
    405                 return false;
    406             }
    407             if (!TextUtils.equals(mDeviceIdentifier.getDescriptor(),
    408                     that.mDeviceIdentifier.getDescriptor())) {
    409                 return false;
    410             }
    411 
    412             return true;
    413         }
    414     }
    415 
    416     public static final class Keyboards implements Comparable<Keyboards> {
    417         @NonNull
    418         public final HardKeyboardDeviceInfo mDeviceInfo;
    419         @NonNull
    420         public final ArrayList<KeyboardInfo> mKeyboardInfoList;
    421         @NonNull
    422         public final Collator mCollator = Collator.getInstance();
    423 
    424         public Keyboards(
    425                 @NonNull final HardKeyboardDeviceInfo deviceInfo,
    426                 @NonNull final ArrayList<KeyboardInfo> keyboardInfoList) {
    427             mDeviceInfo = deviceInfo;
    428             mKeyboardInfoList = keyboardInfoList;
    429         }
    430 
    431         @Override
    432         public int compareTo(@NonNull Keyboards another) {
    433             return mCollator.compare(mDeviceInfo.mDeviceName, another.mDeviceInfo.mDeviceName);
    434         }
    435 
    436         public static final class KeyboardInfo {
    437             @NonNull
    438             public final InputMethodInfo mImi;
    439             @Nullable
    440             public final InputMethodSubtype mImSubtype;
    441             @NonNull
    442             public final KeyboardLayout mLayout;
    443 
    444             public KeyboardInfo(
    445                     @NonNull final InputMethodInfo imi,
    446                     @Nullable final InputMethodSubtype imSubtype,
    447                     @NonNull final KeyboardLayout layout) {
    448                 mImi = imi;
    449                 mImSubtype = imSubtype;
    450                 mLayout = layout;
    451             }
    452         }
    453     }
    454 
    455     static final class KeyboardInfoPreference extends Preference {
    456 
    457         @NonNull
    458         private final CharSequence mImeName;
    459         @Nullable
    460         private final CharSequence mImSubtypeName;
    461         @NonNull
    462         private final Collator collator = Collator.getInstance();
    463 
    464         private KeyboardInfoPreference(
    465                 @NonNull Context context, @NonNull Keyboards.KeyboardInfo info) {
    466             super(context);
    467             mImeName = info.mImi.loadLabel(context.getPackageManager());
    468             mImSubtypeName = getImSubtypeName(context, info.mImi, info.mImSubtype);
    469             setTitle(formatDisplayName(context, mImeName, mImSubtypeName));
    470             if (info.mLayout != null) {
    471                 setSummary(info.mLayout.getLabel());
    472             }
    473         }
    474 
    475         @NonNull
    476         static CharSequence getDisplayName(
    477                 @NonNull Context context, @NonNull InputMethodInfo imi,
    478                 @Nullable InputMethodSubtype imSubtype) {
    479             final CharSequence imeName = imi.loadLabel(context.getPackageManager());
    480             final CharSequence imSubtypeName = getImSubtypeName(context, imi, imSubtype);
    481             return formatDisplayName(context, imeName, imSubtypeName);
    482         }
    483 
    484         private static CharSequence formatDisplayName(
    485                 @NonNull Context context,
    486                 @NonNull CharSequence imeName, @Nullable CharSequence imSubtypeName) {
    487             if (imSubtypeName == null) {
    488                 return imeName;
    489             }
    490             return String.format(
    491                     context.getString(R.string.physical_device_title), imeName, imSubtypeName);
    492         }
    493 
    494         @Nullable
    495         private static CharSequence getImSubtypeName(
    496                 @NonNull Context context, @NonNull InputMethodInfo imi,
    497                 @Nullable InputMethodSubtype imSubtype) {
    498             if (imSubtype != null) {
    499                 return InputMethodAndSubtypeUtil.getSubtypeLocaleNameAsSentence(
    500                         imSubtype, context, imi);
    501             }
    502             return null;
    503         }
    504 
    505         @Override
    506         public int compareTo(@NonNull Preference object) {
    507             if (!(object instanceof KeyboardInfoPreference)) {
    508                 return super.compareTo(object);
    509             }
    510             KeyboardInfoPreference another = (KeyboardInfoPreference) object;
    511             int result = compare(mImeName, another.mImeName);
    512             if (result == 0) {
    513                 result = compare(mImSubtypeName, another.mImSubtypeName);
    514             }
    515             return result;
    516         }
    517 
    518         private int compare(@Nullable CharSequence lhs, @Nullable CharSequence rhs) {
    519             if (!TextUtils.isEmpty(lhs) && !TextUtils.isEmpty(rhs)) {
    520                 return collator.compare(lhs.toString(), rhs.toString());
    521             } else if (TextUtils.isEmpty(lhs) && TextUtils.isEmpty(rhs)) {
    522                 return 0;
    523             } else if (!TextUtils.isEmpty(lhs)) {
    524                 return -1;
    525             } else {
    526                 return 1;
    527             }
    528         }
    529     }
    530 
    531 }
    532