1 /* 2 * Copyright (C) 2017 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.tv.settings.inputmethod; 18 19 import static com.android.tv.settings.util.InstrumentationUtils.logEntrySelected; 20 21 import android.app.tvsettings.TvSettingsEnums; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.pm.PackageManager; 25 import android.hardware.input.InputManager; 26 import android.os.Bundle; 27 import android.os.UserHandle; 28 import android.text.TextUtils; 29 import android.util.ArraySet; 30 import android.util.Log; 31 import android.view.inputmethod.InputMethodInfo; 32 33 import androidx.annotation.Keep; 34 import androidx.annotation.NonNull; 35 import androidx.annotation.VisibleForTesting; 36 import androidx.preference.ListPreference; 37 import androidx.preference.Preference; 38 import androidx.preference.PreferenceCategory; 39 import androidx.preference.PreferenceScreen; 40 41 import com.android.settingslib.applications.DefaultAppInfo; 42 import com.android.tv.settings.R; 43 import com.android.tv.settings.SettingsPreferenceFragment; 44 import com.android.tv.settings.autofill.AutofillHelper; 45 import com.android.tv.settings.library.util.ThreadUtils; 46 import com.android.tv.settings.overlay.FlavorUtils; 47 import com.android.tv.settings.util.SliceUtils; 48 import com.android.tv.twopanelsettings.slices.SlicePreference; 49 50 import java.util.ArrayList; 51 import java.util.List; 52 import java.util.Objects; 53 import java.util.Set; 54 55 /** 56 * Fragment for managing IMEs, Autofills and physical keyboards 57 */ 58 @Keep 59 public class KeyboardFragment extends SettingsPreferenceFragment implements 60 InputManager.InputDeviceListener { 61 private static final String TAG = "KeyboardFragment"; 62 private static final boolean DEBUG = false; 63 64 // Order of input methods, make sure they are inserted between 1 (currentKeyboard) and 65 // 3 (manageKeyboards). 66 private static final int INPUT_METHOD_PREFERENCE_ORDER = 2; 67 68 // Order of physical keyboard setting, in the end 69 private static final int PHYSICAL_KEYBOARD_PREFERENCE_ORDER = 5; 70 71 @VisibleForTesting 72 static final String KEY_KEYBOARD_CATEGORY = "keyboardCategory"; 73 74 @VisibleForTesting 75 static final String KEY_CURRENT_KEYBOARD = "currentKeyboard"; 76 77 private static final String KEY_KEYBOARD_SETTINGS_PREFIX = "keyboardSettings:"; 78 79 private static final String KEY_PHYSICAL_KEYBOARD_SETTINGS_PREFIX = "physicalKeyboardSettings:"; 80 81 @VisibleForTesting 82 static final String KEY_AUTOFILL_CATEGORY = "autofillCategory"; 83 84 @VisibleForTesting 85 static final String KEY_CURRENT_AUTOFILL = "currentAutofill"; 86 87 private static final String KEY_AUTOFILL_SETTINGS_PREFIX = "autofillSettings:"; 88 89 private PackageManager mPm; 90 91 private InputManager mIm; 92 93 /** 94 * @return New fragment instance 95 */ newInstance()96 public static KeyboardFragment newInstance() { 97 return new KeyboardFragment(); 98 } 99 100 @Override onAttach(Context context)101 public void onAttach(Context context) { 102 super.onAttach(context); 103 mPm = context.getPackageManager(); 104 mIm = Objects.requireNonNull(context.getSystemService(InputManager.class)); 105 } 106 107 @Override onCreatePreferences(Bundle savedInstanceState, String rootKey)108 public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { 109 setPreferencesFromResource(R.xml.keyboard, null); 110 111 findPreference(KEY_CURRENT_KEYBOARD).setOnPreferenceChangeListener( 112 (preference, newValue) -> { 113 logEntrySelected(TvSettingsEnums.SYSTEM_KEYBOARD_CURRENT_KEYBOARD); 114 InputMethodHelper.setDefaultInputMethodId(getContext(), (String) newValue); 115 return true; 116 }); 117 118 updateUi(); 119 } 120 121 @Override onResume()122 public void onResume() { 123 super.onResume(); 124 updateUi(); 125 mIm.registerInputDeviceListener(this, null); 126 } 127 128 @Override onPause()129 public void onPause() { 130 super.onPause(); 131 mIm.unregisterInputDeviceListener(this); 132 } 133 134 @VisibleForTesting updateUi()135 void updateUi() { 136 updateAutofill(); 137 updateKeyboards(); 138 } 139 updateKeyboards()140 private void updateKeyboards() { 141 updateCurrentKeyboardPreference(findPreference(KEY_CURRENT_KEYBOARD)); 142 updateKeyboardsSettings(); 143 scheduleUpdatePhysicalKeyboards(getPreferenceContext()); 144 } 145 updateCurrentKeyboardPreference(ListPreference currentKeyboardPref)146 private void updateCurrentKeyboardPreference(ListPreference currentKeyboardPref) { 147 List<InputMethodInfo> enabledInputMethodInfos = InputMethodHelper 148 .getEnabledSystemInputMethodList(getContext()); 149 final List<CharSequence> entries = new ArrayList<>(enabledInputMethodInfos.size()); 150 final List<CharSequence> values = new ArrayList<>(enabledInputMethodInfos.size()); 151 152 int defaultIndex = 0; 153 final String defaultId = InputMethodHelper.getDefaultInputMethodId(getContext()); 154 155 for (final InputMethodInfo info : enabledInputMethodInfos) { 156 entries.add(info.loadLabel(mPm)); 157 final String id = info.getId(); 158 values.add(id); 159 if (TextUtils.equals(id, defaultId)) { 160 defaultIndex = values.size() - 1; 161 } 162 } 163 164 currentKeyboardPref.setEntries(entries.toArray(new CharSequence[entries.size()])); 165 currentKeyboardPref.setEntryValues(values.toArray(new CharSequence[values.size()])); 166 if (entries.size() > 0) { 167 currentKeyboardPref.setValueIndex(defaultIndex); 168 } 169 } 170 getPreferenceContext()171 Context getPreferenceContext() { 172 return getPreferenceManager().getContext(); 173 } 174 updateKeyboardsSettings()175 private void updateKeyboardsSettings() { 176 final Context preferenceContext = getPreferenceContext(); 177 List<InputMethodInfo> enabledInputMethodInfos = InputMethodHelper 178 .getEnabledSystemInputMethodList(getContext()); 179 PreferenceScreen preferenceScreen = getPreferenceScreen(); 180 final Set<String> enabledInputMethodKeys = new ArraySet<>( 181 enabledInputMethodInfos.size()); 182 // Add per-IME settings 183 for (final InputMethodInfo info : enabledInputMethodInfos) { 184 final String uri = InputMethodHelper.getInputMethodsSettingsUri(getContext(), info); 185 final Intent settingsIntent = InputMethodHelper.getInputMethodSettingsIntent(info); 186 if (uri == null && settingsIntent == null) { 187 continue; 188 } 189 final String key = KEY_KEYBOARD_SETTINGS_PREFIX + info.getId(); 190 191 Preference preference = preferenceScreen.findPreference(key); 192 boolean useSlice = FlavorUtils.isTwoPanel(getContext()) && uri != null; 193 if (preference == null) { 194 if (useSlice) { 195 preference = new SlicePreference(preferenceContext); 196 } else { 197 preference = new Preference(preferenceContext); 198 } 199 preference.setOrder(INPUT_METHOD_PREFERENCE_ORDER); 200 preferenceScreen.addPreference(preference); 201 } 202 preference.setTitle(getContext().getString(R.string.title_settings, 203 info.loadLabel(mPm))); 204 preference.setKey(key); 205 if (useSlice) { 206 ((SlicePreference) preference).setUri(uri); 207 preference.setFragment(SliceUtils.PATH_SLICE_FRAGMENT); 208 } else { 209 preference.setIntent(settingsIntent); 210 } 211 enabledInputMethodKeys.add(key); 212 } 213 removeDisabledPreferencesFromScreen(preferenceScreen, enabledInputMethodKeys, 214 KEY_KEYBOARD_SETTINGS_PREFIX); 215 } 216 scheduleUpdatePhysicalKeyboards(Context context)217 void scheduleUpdatePhysicalKeyboards(Context context) { 218 ThreadUtils.postOnBackgroundThread(() -> { 219 final List<PhysicalKeyboardHelper.DeviceInfo> newPhysicalKeyboards = 220 PhysicalKeyboardHelper.getPhysicalKeyboards(context); 221 ThreadUtils.postOnMainThread(() -> updatePhysicalKeyboards(newPhysicalKeyboards)); 222 }); 223 } 224 updatePhysicalKeyboards( @onNull List<PhysicalKeyboardHelper.DeviceInfo> newPhysicalKeyboards)225 private void updatePhysicalKeyboards( 226 @NonNull List<PhysicalKeyboardHelper.DeviceInfo> newPhysicalKeyboards) { 227 final PreferenceScreen preferenceScreen = getPreferenceScreen(); 228 if (DEBUG) { 229 Log.d(TAG, "updatePhysicalKeyboards: " + newPhysicalKeyboards.toString()); 230 } 231 final Set<String> enabledPhysicalKeyboardKeys = new ArraySet<>(newPhysicalKeyboards.size()); 232 // Add a setting per physical keyboard device 233 for (PhysicalKeyboardHelper.DeviceInfo deviceInfo : 234 newPhysicalKeyboards) { 235 String key = KEY_PHYSICAL_KEYBOARD_SETTINGS_PREFIX 236 + deviceInfo.mDeviceIdentifier.getDescriptor(); 237 Preference pref = preferenceScreen.findPreference(key); 238 if (pref == null) { 239 pref = new Preference(getPreferenceContext()); 240 pref.setOrder(PHYSICAL_KEYBOARD_PREFERENCE_ORDER); 241 preferenceScreen.addPreference(pref); 242 } 243 pref.setKey(key); 244 pref.setTitle(getPreferenceContext().getString( 245 com.android.settingslib.R.string.physical_keyboard_title)); 246 pref.setSummary(deviceInfo.getSummary()); 247 KeyboardLayoutSelectionFragment.prepareArgs(pref.getExtras(), 248 deviceInfo.mDeviceIdentifier, 249 deviceInfo.mDeviceName, 250 deviceInfo.mDeviceId, 251 deviceInfo.mCurrentLayoutDescriptor); 252 pref.setFragment(KeyboardLayoutSelectionFragment.class.getName()); 253 enabledPhysicalKeyboardKeys.add(key); 254 } 255 removeDisabledPreferencesFromScreen(preferenceScreen, enabledPhysicalKeyboardKeys, 256 KEY_PHYSICAL_KEYBOARD_SETTINGS_PREFIX); 257 } 258 259 /** 260 * Removes all preferences which start with the key prefix and are not among the enabled keys 261 * from the preference screen. 262 * 263 * @param preferenceScreen The preference screen. 264 * @param enabledKeys The set of enabled keys. 265 * @param keyPrefix The prefix for the keys to be removed. 266 */ removeDisabledPreferencesFromScreen(PreferenceScreen preferenceScreen, Set<String> enabledKeys, String keyPrefix)267 private void removeDisabledPreferencesFromScreen(PreferenceScreen preferenceScreen, 268 Set<String> enabledKeys, String keyPrefix) { 269 for (int i = 0; i < preferenceScreen.getPreferenceCount(); ) { 270 final Preference preference = preferenceScreen.getPreference(i); 271 final String key = preference.getKey(); 272 if (!TextUtils.isEmpty(key) 273 && key.startsWith(keyPrefix) 274 && !enabledKeys.contains(key)) { 275 preferenceScreen.removePreference(preference); 276 } else { 277 i++; 278 } 279 } 280 } 281 282 /** 283 * Update autofill related preferences. 284 */ updateAutofill()285 private void updateAutofill() { 286 final PreferenceCategory autofillCategory = findPreference(KEY_AUTOFILL_CATEGORY); 287 List<DefaultAppInfo> candidates = getAutofillCandidates(); 288 if (candidates.isEmpty()) { 289 // No need to show keyboard category and autofill category. 290 // Keyboard only preference screen: 291 findPreference(KEY_KEYBOARD_CATEGORY).setVisible(false); 292 autofillCategory.setVisible(false); 293 getPreferenceScreen().setTitle(R.string.system_keyboard); 294 } else { 295 // Show both keyboard category and autofill category in keyboard & autofill screen. 296 findPreference(KEY_KEYBOARD_CATEGORY).setVisible(true); 297 autofillCategory.setVisible(true); 298 final Preference currentAutofillPref = findPreference(KEY_CURRENT_AUTOFILL); 299 updateCurrentAutofillPreference(currentAutofillPref, candidates); 300 updateAutofillSettings(candidates); 301 getPreferenceScreen().setTitle(R.string.system_keyboard_autofill); 302 } 303 } 304 getAutofillCandidates()305 private List<DefaultAppInfo> getAutofillCandidates() { 306 return AutofillHelper.getAutofillCandidates(getContext(), 307 mPm, UserHandle.myUserId()); 308 } 309 updateCurrentAutofillPreference(Preference currentAutofillPref, List<DefaultAppInfo> candidates)310 private void updateCurrentAutofillPreference(Preference currentAutofillPref, 311 List<DefaultAppInfo> candidates) { 312 313 DefaultAppInfo app = AutofillHelper.getCurrentAutofill(getContext(), candidates); 314 315 CharSequence summary = app == null ? getContext().getString(R.string.autofill_none) 316 : app.loadLabel(); 317 currentAutofillPref.setSummary(summary); 318 } 319 updateAutofillSettings(List<DefaultAppInfo> candidates)320 private void updateAutofillSettings(List<DefaultAppInfo> candidates) { 321 final Context preferenceContext = getPreferenceContext(); 322 323 final PreferenceCategory autofillCategory = findPreference(KEY_AUTOFILL_CATEGORY); 324 325 final Set<String> autofillServicesKeys = new ArraySet<>(candidates.size()); 326 for (final DefaultAppInfo info : candidates) { 327 final Intent settingsIntent = AutofillHelper.getAutofillSettingsIntent(getContext(), 328 mPm, info); 329 if (settingsIntent == null) { 330 continue; 331 } 332 final String key = KEY_AUTOFILL_SETTINGS_PREFIX + info.getKey(); 333 334 Preference preference = findPreference(key); 335 if (preference == null) { 336 preference = new Preference(preferenceContext); 337 autofillCategory.addPreference(preference); 338 } 339 preference.setTitle(getContext().getString(R.string.title_settings, info.loadLabel())); 340 preference.setKey(key); 341 preference.setIntent(settingsIntent); 342 autofillServicesKeys.add(key); 343 } 344 345 for (int i = 0; i < autofillCategory.getPreferenceCount(); ) { 346 final Preference preference = autofillCategory.getPreference(i); 347 final String key = preference.getKey(); 348 if (!TextUtils.isEmpty(key) 349 && key.startsWith(KEY_AUTOFILL_SETTINGS_PREFIX) 350 && !autofillServicesKeys.contains(key)) { 351 autofillCategory.removePreference(preference); 352 } else { 353 i++; 354 } 355 } 356 } 357 358 @Override getPageId()359 protected int getPageId() { 360 return TvSettingsEnums.SYSTEM_KEYBOARD; 361 } 362 363 @Override onInputDeviceAdded(int deviceId)364 public void onInputDeviceAdded(int deviceId) { 365 scheduleUpdatePhysicalKeyboards(getPreferenceContext()); 366 } 367 368 @Override onInputDeviceRemoved(int deviceId)369 public void onInputDeviceRemoved(int deviceId) { 370 scheduleUpdatePhysicalKeyboards(getPreferenceContext()); 371 } 372 373 @Override onInputDeviceChanged(int deviceId)374 public void onInputDeviceChanged(int deviceId) { 375 scheduleUpdatePhysicalKeyboards(getPreferenceContext()); 376 } 377 } 378