1 /** 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy 6 * 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, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations 14 * under the License. 15 */ 16 17 package com.android.inputmethod.latin; 18 19 import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.ASCII_CAPABLE; 20 21 import android.app.AlertDialog; 22 import android.app.Dialog; 23 import android.content.Context; 24 import android.content.DialogInterface; 25 import android.content.Intent; 26 import android.content.SharedPreferences; 27 import android.content.res.Resources; 28 import android.os.Bundle; 29 import android.os.Parcel; 30 import android.os.Parcelable; 31 import android.preference.DialogPreference; 32 import android.preference.Preference; 33 import android.preference.PreferenceFragment; 34 import android.preference.PreferenceGroup; 35 import android.util.Pair; 36 import android.view.Menu; 37 import android.view.MenuInflater; 38 import android.view.MenuItem; 39 import android.view.View; 40 import android.view.inputmethod.InputMethodInfo; 41 import android.view.inputmethod.InputMethodSubtype; 42 import android.widget.ArrayAdapter; 43 import android.widget.Spinner; 44 import android.widget.SpinnerAdapter; 45 import android.widget.Toast; 46 47 import com.android.inputmethod.compat.CompatUtils; 48 49 import java.util.ArrayList; 50 import java.util.TreeSet; 51 52 public final class AdditionalSubtypeSettings extends PreferenceFragment { 53 private SharedPreferences mPrefs; 54 private SubtypeLocaleAdapter mSubtypeLocaleAdapter; 55 private KeyboardLayoutSetAdapter mKeyboardLayoutSetAdapter; 56 57 private boolean mIsAddingNewSubtype; 58 private AlertDialog mSubtypeEnablerNotificationDialog; 59 private String mSubtypePreferenceKeyForSubtypeEnabler; 60 61 private static final int MENU_ADD_SUBTYPE = Menu.FIRST; 62 private static final String KEY_IS_ADDING_NEW_SUBTYPE = "is_adding_new_subtype"; 63 private static final String KEY_IS_SUBTYPE_ENABLER_NOTIFICATION_DIALOG_OPEN = 64 "is_subtype_enabler_notification_dialog_open"; 65 private static final String KEY_SUBTYPE_FOR_SUBTYPE_ENABLER = "subtype_for_subtype_enabler"; 66 static final class SubtypeLocaleItem extends Pair<String, String> 67 implements Comparable<SubtypeLocaleItem> { 68 public SubtypeLocaleItem(final String localeString, final String displayName) { 69 super(localeString, displayName); 70 } 71 72 public SubtypeLocaleItem(final String localeString) { 73 this(localeString, SubtypeLocale.getSubtypeLocaleDisplayName(localeString)); 74 } 75 76 @Override 77 public String toString() { 78 return second; 79 } 80 81 @Override 82 public int compareTo(final SubtypeLocaleItem o) { 83 return first.compareTo(o.first); 84 } 85 } 86 87 static final class SubtypeLocaleAdapter extends ArrayAdapter<SubtypeLocaleItem> { 88 public SubtypeLocaleAdapter(final Context context) { 89 super(context, android.R.layout.simple_spinner_item); 90 setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); 91 92 final TreeSet<SubtypeLocaleItem> items = CollectionUtils.newTreeSet(); 93 final InputMethodInfo imi = ImfUtils.getInputMethodInfoOfThisIme(context); 94 final int count = imi.getSubtypeCount(); 95 for (int i = 0; i < count; i++) { 96 final InputMethodSubtype subtype = imi.getSubtypeAt(i); 97 if (subtype.containsExtraValueKey(ASCII_CAPABLE)) { 98 items.add(createItem(context, subtype.getLocale())); 99 } 100 } 101 // TODO: Should filter out already existing combinations of locale and layout. 102 addAll(items); 103 } 104 105 public static SubtypeLocaleItem createItem(final Context context, 106 final String localeString) { 107 if (localeString.equals(SubtypeLocale.NO_LANGUAGE)) { 108 final String displayName = context.getString(R.string.subtype_no_language); 109 return new SubtypeLocaleItem(localeString, displayName); 110 } else { 111 return new SubtypeLocaleItem(localeString); 112 } 113 } 114 } 115 116 static final class KeyboardLayoutSetItem extends Pair<String, String> { 117 public KeyboardLayoutSetItem(final InputMethodSubtype subtype) { 118 super(SubtypeLocale.getKeyboardLayoutSetName(subtype), 119 SubtypeLocale.getKeyboardLayoutSetDisplayName(subtype)); 120 } 121 122 @Override 123 public String toString() { 124 return second; 125 } 126 } 127 128 static final class KeyboardLayoutSetAdapter extends ArrayAdapter<KeyboardLayoutSetItem> { 129 public KeyboardLayoutSetAdapter(final Context context) { 130 super(context, android.R.layout.simple_spinner_item); 131 setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); 132 133 // TODO: Should filter out already existing combinations of locale and layout. 134 for (final String layout : SubtypeLocale.getPredefinedKeyboardLayoutSet()) { 135 // This is a dummy subtype with NO_LANGUAGE, only for display. 136 final InputMethodSubtype subtype = AdditionalSubtype.createAdditionalSubtype( 137 SubtypeLocale.NO_LANGUAGE, layout, null); 138 add(new KeyboardLayoutSetItem(subtype)); 139 } 140 } 141 } 142 143 private interface SubtypeDialogProxy { 144 public void onRemovePressed(SubtypePreference subtypePref); 145 public void onSavePressed(SubtypePreference subtypePref); 146 public void onAddPressed(SubtypePreference subtypePref); 147 public SubtypeLocaleAdapter getSubtypeLocaleAdapter(); 148 public KeyboardLayoutSetAdapter getKeyboardLayoutSetAdapter(); 149 } 150 151 static final class SubtypePreference extends DialogPreference 152 implements DialogInterface.OnCancelListener { 153 private static final String KEY_PREFIX = "subtype_pref_"; 154 private static final String KEY_NEW_SUBTYPE = KEY_PREFIX + "new"; 155 156 private InputMethodSubtype mSubtype; 157 private InputMethodSubtype mPreviousSubtype; 158 159 private final SubtypeDialogProxy mProxy; 160 private Spinner mSubtypeLocaleSpinner; 161 private Spinner mKeyboardLayoutSetSpinner; 162 163 public static SubtypePreference newIncompleteSubtypePreference(final Context context, 164 final SubtypeDialogProxy proxy) { 165 return new SubtypePreference(context, null, proxy); 166 } 167 168 public SubtypePreference(final Context context, final InputMethodSubtype subtype, 169 final SubtypeDialogProxy proxy) { 170 super(context, null); 171 setDialogLayoutResource(R.layout.additional_subtype_dialog); 172 setPersistent(false); 173 mProxy = proxy; 174 setSubtype(subtype); 175 } 176 177 public void show() { 178 showDialog(null); 179 } 180 181 public final boolean isIncomplete() { 182 return mSubtype == null; 183 } 184 185 public InputMethodSubtype getSubtype() { 186 return mSubtype; 187 } 188 189 public void setSubtype(final InputMethodSubtype subtype) { 190 mPreviousSubtype = mSubtype; 191 mSubtype = subtype; 192 if (isIncomplete()) { 193 setTitle(null); 194 setDialogTitle(R.string.add_style); 195 setKey(KEY_NEW_SUBTYPE); 196 } else { 197 final String displayName = SubtypeLocale.getSubtypeDisplayName( 198 subtype, getContext().getResources()); 199 setTitle(displayName); 200 setDialogTitle(displayName); 201 setKey(KEY_PREFIX + subtype.getLocale() + "_" 202 + SubtypeLocale.getKeyboardLayoutSetName(subtype)); 203 } 204 } 205 206 public void revert() { 207 setSubtype(mPreviousSubtype); 208 } 209 210 public boolean hasBeenModified() { 211 return mSubtype != null && !mSubtype.equals(mPreviousSubtype); 212 } 213 214 @Override 215 protected View onCreateDialogView() { 216 final View v = super.onCreateDialogView(); 217 mSubtypeLocaleSpinner = (Spinner) v.findViewById(R.id.subtype_locale_spinner); 218 mSubtypeLocaleSpinner.setAdapter(mProxy.getSubtypeLocaleAdapter()); 219 mKeyboardLayoutSetSpinner = (Spinner) v.findViewById(R.id.keyboard_layout_set_spinner); 220 mKeyboardLayoutSetSpinner.setAdapter(mProxy.getKeyboardLayoutSetAdapter()); 221 return v; 222 } 223 224 @Override 225 protected void onPrepareDialogBuilder(final AlertDialog.Builder builder) { 226 final Context context = builder.getContext(); 227 builder.setCancelable(true).setOnCancelListener(this); 228 if (isIncomplete()) { 229 builder.setPositiveButton(R.string.add, this) 230 .setNegativeButton(android.R.string.cancel, this); 231 } else { 232 builder.setPositiveButton(R.string.save, this) 233 .setNeutralButton(android.R.string.cancel, this) 234 .setNegativeButton(R.string.remove, this); 235 final SubtypeLocaleItem localeItem = SubtypeLocaleAdapter.createItem( 236 context, mSubtype.getLocale()); 237 final KeyboardLayoutSetItem layoutItem = new KeyboardLayoutSetItem(mSubtype); 238 setSpinnerPosition(mSubtypeLocaleSpinner, localeItem); 239 setSpinnerPosition(mKeyboardLayoutSetSpinner, layoutItem); 240 } 241 } 242 243 private static void setSpinnerPosition(final Spinner spinner, final Object itemToSelect) { 244 final SpinnerAdapter adapter = spinner.getAdapter(); 245 final int count = adapter.getCount(); 246 for (int i = 0; i < count; i++) { 247 final Object item = spinner.getItemAtPosition(i); 248 if (item.equals(itemToSelect)) { 249 spinner.setSelection(i); 250 return; 251 } 252 } 253 } 254 255 @Override 256 public void onCancel(final DialogInterface dialog) { 257 if (isIncomplete()) { 258 mProxy.onRemovePressed(this); 259 } 260 } 261 262 @Override 263 public void onClick(final DialogInterface dialog, final int which) { 264 super.onClick(dialog, which); 265 switch (which) { 266 case DialogInterface.BUTTON_POSITIVE: 267 final boolean isEditing = !isIncomplete(); 268 final SubtypeLocaleItem locale = 269 (SubtypeLocaleItem) mSubtypeLocaleSpinner.getSelectedItem(); 270 final KeyboardLayoutSetItem layout = 271 (KeyboardLayoutSetItem) mKeyboardLayoutSetSpinner.getSelectedItem(); 272 final InputMethodSubtype subtype = AdditionalSubtype.createAdditionalSubtype( 273 locale.first, layout.first, ASCII_CAPABLE); 274 setSubtype(subtype); 275 notifyChanged(); 276 if (isEditing) { 277 mProxy.onSavePressed(this); 278 } else { 279 mProxy.onAddPressed(this); 280 } 281 break; 282 case DialogInterface.BUTTON_NEUTRAL: 283 // Nothing to do 284 break; 285 case DialogInterface.BUTTON_NEGATIVE: 286 mProxy.onRemovePressed(this); 287 break; 288 } 289 } 290 291 private static int getSpinnerPosition(final Spinner spinner) { 292 if (spinner == null) return -1; 293 return spinner.getSelectedItemPosition(); 294 } 295 296 private static void setSpinnerPosition(final Spinner spinner, final int position) { 297 if (spinner == null || position < 0) return; 298 spinner.setSelection(position); 299 } 300 301 @Override 302 protected Parcelable onSaveInstanceState() { 303 final Parcelable superState = super.onSaveInstanceState(); 304 final Dialog dialog = getDialog(); 305 if (dialog == null || !dialog.isShowing()) { 306 return superState; 307 } 308 309 final SavedState myState = new SavedState(superState); 310 myState.mSubtype = mSubtype; 311 myState.mSubtypeLocaleSelectedPos = getSpinnerPosition(mSubtypeLocaleSpinner); 312 myState.mKeyboardLayoutSetSelectedPos = getSpinnerPosition(mKeyboardLayoutSetSpinner); 313 return myState; 314 } 315 316 @Override 317 protected void onRestoreInstanceState(final Parcelable state) { 318 if (!(state instanceof SavedState)) { 319 super.onRestoreInstanceState(state); 320 return; 321 } 322 323 final SavedState myState = (SavedState) state; 324 super.onRestoreInstanceState(myState.getSuperState()); 325 setSpinnerPosition(mSubtypeLocaleSpinner, myState.mSubtypeLocaleSelectedPos); 326 setSpinnerPosition(mKeyboardLayoutSetSpinner, myState.mKeyboardLayoutSetSelectedPos); 327 setSubtype(myState.mSubtype); 328 } 329 330 static final class SavedState extends Preference.BaseSavedState { 331 InputMethodSubtype mSubtype; 332 int mSubtypeLocaleSelectedPos; 333 int mKeyboardLayoutSetSelectedPos; 334 335 public SavedState(final Parcelable superState) { 336 super(superState); 337 } 338 339 @Override 340 public void writeToParcel(final Parcel dest, final int flags) { 341 super.writeToParcel(dest, flags); 342 dest.writeInt(mSubtypeLocaleSelectedPos); 343 dest.writeInt(mKeyboardLayoutSetSelectedPos); 344 dest.writeParcelable(mSubtype, 0); 345 } 346 347 public SavedState(final Parcel source) { 348 super(source); 349 mSubtypeLocaleSelectedPos = source.readInt(); 350 mKeyboardLayoutSetSelectedPos = source.readInt(); 351 mSubtype = (InputMethodSubtype)source.readParcelable(null); 352 } 353 354 @SuppressWarnings("hiding") 355 public static final Parcelable.Creator<SavedState> CREATOR = 356 new Parcelable.Creator<SavedState>() { 357 @Override 358 public SavedState createFromParcel(final Parcel source) { 359 return new SavedState(source); 360 } 361 362 @Override 363 public SavedState[] newArray(final int size) { 364 return new SavedState[size]; 365 } 366 }; 367 } 368 } 369 370 public AdditionalSubtypeSettings() { 371 // Empty constructor for fragment generation. 372 } 373 374 @Override 375 public void onCreate(final Bundle savedInstanceState) { 376 super.onCreate(savedInstanceState); 377 378 addPreferencesFromResource(R.xml.additional_subtype_settings); 379 setHasOptionsMenu(true); 380 381 mPrefs = getPreferenceManager().getSharedPreferences(); 382 } 383 384 @Override 385 public void onActivityCreated(final Bundle savedInstanceState) { 386 final Context context = getActivity(); 387 mSubtypeLocaleAdapter = new SubtypeLocaleAdapter(context); 388 mKeyboardLayoutSetAdapter = new KeyboardLayoutSetAdapter(context); 389 390 final String prefSubtypes = 391 SettingsValues.getPrefAdditionalSubtypes(mPrefs, getResources()); 392 setPrefSubtypes(prefSubtypes, context); 393 394 mIsAddingNewSubtype = (savedInstanceState != null) 395 && savedInstanceState.containsKey(KEY_IS_ADDING_NEW_SUBTYPE); 396 if (mIsAddingNewSubtype) { 397 getPreferenceScreen().addPreference( 398 SubtypePreference.newIncompleteSubtypePreference(context, mSubtypeProxy)); 399 } 400 401 super.onActivityCreated(savedInstanceState); 402 403 if (savedInstanceState != null && savedInstanceState.containsKey( 404 KEY_IS_SUBTYPE_ENABLER_NOTIFICATION_DIALOG_OPEN)) { 405 mSubtypePreferenceKeyForSubtypeEnabler = savedInstanceState.getString( 406 KEY_SUBTYPE_FOR_SUBTYPE_ENABLER); 407 final SubtypePreference subtypePref = (SubtypePreference)findPreference( 408 mSubtypePreferenceKeyForSubtypeEnabler); 409 mSubtypeEnablerNotificationDialog = createDialog(subtypePref); 410 mSubtypeEnablerNotificationDialog.show(); 411 } 412 } 413 414 @Override 415 public void onSaveInstanceState(final Bundle outState) { 416 super.onSaveInstanceState(outState); 417 if (mIsAddingNewSubtype) { 418 outState.putBoolean(KEY_IS_ADDING_NEW_SUBTYPE, true); 419 } 420 if (mSubtypeEnablerNotificationDialog != null 421 && mSubtypeEnablerNotificationDialog.isShowing()) { 422 outState.putBoolean(KEY_IS_SUBTYPE_ENABLER_NOTIFICATION_DIALOG_OPEN, true); 423 outState.putString( 424 KEY_SUBTYPE_FOR_SUBTYPE_ENABLER, mSubtypePreferenceKeyForSubtypeEnabler); 425 } 426 } 427 428 private final SubtypeDialogProxy mSubtypeProxy = new SubtypeDialogProxy() { 429 @Override 430 public void onRemovePressed(final SubtypePreference subtypePref) { 431 mIsAddingNewSubtype = false; 432 final PreferenceGroup group = getPreferenceScreen(); 433 group.removePreference(subtypePref); 434 ImfUtils.setAdditionalInputMethodSubtypes(getActivity(), getSubtypes()); 435 } 436 437 @Override 438 public void onSavePressed(final SubtypePreference subtypePref) { 439 final InputMethodSubtype subtype = subtypePref.getSubtype(); 440 if (!subtypePref.hasBeenModified()) { 441 return; 442 } 443 if (findDuplicatedSubtype(subtype) == null) { 444 ImfUtils.setAdditionalInputMethodSubtypes(getActivity(), getSubtypes()); 445 return; 446 } 447 448 // Saved subtype is duplicated. 449 final PreferenceGroup group = getPreferenceScreen(); 450 group.removePreference(subtypePref); 451 subtypePref.revert(); 452 group.addPreference(subtypePref); 453 showSubtypeAlreadyExistsToast(subtype); 454 } 455 456 @Override 457 public void onAddPressed(final SubtypePreference subtypePref) { 458 mIsAddingNewSubtype = false; 459 final InputMethodSubtype subtype = subtypePref.getSubtype(); 460 if (findDuplicatedSubtype(subtype) == null) { 461 ImfUtils.setAdditionalInputMethodSubtypes(getActivity(), getSubtypes()); 462 mSubtypePreferenceKeyForSubtypeEnabler = subtypePref.getKey(); 463 mSubtypeEnablerNotificationDialog = createDialog(subtypePref); 464 mSubtypeEnablerNotificationDialog.show(); 465 return; 466 } 467 468 // Newly added subtype is duplicated. 469 final PreferenceGroup group = getPreferenceScreen(); 470 group.removePreference(subtypePref); 471 showSubtypeAlreadyExistsToast(subtype); 472 } 473 474 @Override 475 public SubtypeLocaleAdapter getSubtypeLocaleAdapter() { 476 return mSubtypeLocaleAdapter; 477 } 478 479 @Override 480 public KeyboardLayoutSetAdapter getKeyboardLayoutSetAdapter() { 481 return mKeyboardLayoutSetAdapter; 482 } 483 }; 484 485 private void showSubtypeAlreadyExistsToast(final InputMethodSubtype subtype) { 486 final Context context = getActivity(); 487 final Resources res = context.getResources(); 488 final String message = res.getString(R.string.custom_input_style_already_exists, 489 SubtypeLocale.getSubtypeDisplayName(subtype, res)); 490 Toast.makeText(context, message, Toast.LENGTH_SHORT).show(); 491 } 492 493 private InputMethodSubtype findDuplicatedSubtype(final InputMethodSubtype subtype) { 494 final String localeString = subtype.getLocale(); 495 final String keyboardLayoutSetName = SubtypeLocale.getKeyboardLayoutSetName(subtype); 496 return ImfUtils.findSubtypeByLocaleAndKeyboardLayoutSet( 497 getActivity(), localeString, keyboardLayoutSetName); 498 } 499 500 private AlertDialog createDialog( 501 @SuppressWarnings("unused") final SubtypePreference subtypePref) { 502 final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); 503 builder.setTitle(R.string.custom_input_styles_title) 504 .setMessage(R.string.custom_input_style_note_message) 505 .setNegativeButton(R.string.not_now, null) 506 .setPositiveButton(R.string.enable, new DialogInterface.OnClickListener() { 507 @Override 508 public void onClick(DialogInterface dialog, int which) { 509 final Intent intent = CompatUtils.getInputLanguageSelectionIntent( 510 ImfUtils.getInputMethodIdOfThisIme(getActivity()), 511 Intent.FLAG_ACTIVITY_NEW_TASK 512 | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED 513 | Intent.FLAG_ACTIVITY_CLEAR_TOP); 514 // TODO: Add newly adding subtype to extra value of the intent as a hint 515 // for the input language selection activity. 516 // intent.putExtra("newlyAddedSubtype", subtypePref.getSubtype()); 517 startActivity(intent); 518 } 519 }); 520 521 return builder.create(); 522 } 523 524 private void setPrefSubtypes(final String prefSubtypes, final Context context) { 525 final PreferenceGroup group = getPreferenceScreen(); 526 group.removeAll(); 527 final InputMethodSubtype[] subtypesArray = 528 AdditionalSubtype.createAdditionalSubtypesArray(prefSubtypes); 529 for (final InputMethodSubtype subtype : subtypesArray) { 530 final SubtypePreference pref = new SubtypePreference( 531 context, subtype, mSubtypeProxy); 532 group.addPreference(pref); 533 } 534 } 535 536 private InputMethodSubtype[] getSubtypes() { 537 final PreferenceGroup group = getPreferenceScreen(); 538 final ArrayList<InputMethodSubtype> subtypes = CollectionUtils.newArrayList(); 539 final int count = group.getPreferenceCount(); 540 for (int i = 0; i < count; i++) { 541 final Preference pref = group.getPreference(i); 542 if (pref instanceof SubtypePreference) { 543 final SubtypePreference subtypePref = (SubtypePreference)pref; 544 // We should not save newly adding subtype to preference because it is incomplete. 545 if (subtypePref.isIncomplete()) continue; 546 subtypes.add(subtypePref.getSubtype()); 547 } 548 } 549 return subtypes.toArray(new InputMethodSubtype[subtypes.size()]); 550 } 551 552 @Override 553 public void onPause() { 554 super.onPause(); 555 final String oldSubtypes = SettingsValues.getPrefAdditionalSubtypes(mPrefs, getResources()); 556 final InputMethodSubtype[] subtypes = getSubtypes(); 557 final String prefSubtypes = AdditionalSubtype.createPrefSubtypes(subtypes); 558 if (prefSubtypes.equals(oldSubtypes)) { 559 return; 560 } 561 562 final SharedPreferences.Editor editor = mPrefs.edit(); 563 try { 564 editor.putString(Settings.PREF_CUSTOM_INPUT_STYLES, prefSubtypes); 565 } finally { 566 editor.apply(); 567 } 568 ImfUtils.setAdditionalInputMethodSubtypes(getActivity(), subtypes); 569 } 570 571 @Override 572 public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { 573 final MenuItem addSubtypeMenu = menu.add(0, MENU_ADD_SUBTYPE, 0, R.string.add_style); 574 addSubtypeMenu.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); 575 } 576 577 @Override 578 public boolean onOptionsItemSelected(final MenuItem item) { 579 final int itemId = item.getItemId(); 580 if (itemId == MENU_ADD_SUBTYPE) { 581 final SubtypePreference newSubtype = 582 SubtypePreference.newIncompleteSubtypePreference(getActivity(), mSubtypeProxy); 583 getPreferenceScreen().addPreference(newSubtype); 584 newSubtype.show(); 585 mIsAddingNewSubtype = true; 586 return true; 587 } 588 return super.onOptionsItemSelected(item); 589 } 590 } 591