1 /* 2 * Copyright (C) 2011 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 android.text.style; 18 19 import android.annotation.ColorInt; 20 import android.annotation.NonNull; 21 import android.annotation.Nullable; 22 import android.compat.annotation.UnsupportedAppUsage; 23 import android.content.Context; 24 import android.content.res.TypedArray; 25 import android.graphics.Color; 26 import android.os.Build; 27 import android.os.Parcel; 28 import android.os.Parcelable; 29 import android.os.SystemClock; 30 import android.text.ParcelableSpan; 31 import android.text.TextPaint; 32 import android.text.TextUtils; 33 import android.util.Log; 34 import android.widget.TextView; 35 36 import java.util.Arrays; 37 import java.util.Locale; 38 39 /** 40 * Holds suggestion candidates for the text enclosed in this span. 41 * 42 * When such a span is edited in an EditText, double tapping on the text enclosed in this span will 43 * display a popup dialog listing suggestion replacement for that text. The user can then replace 44 * the original text by one of the suggestions. 45 * 46 * These spans should typically be created by the input method to provide correction and alternates 47 * for the text. 48 * 49 * @see TextView#isSuggestionsEnabled() 50 */ 51 @android.ravenwood.annotation.RavenwoodKeepWholeClass 52 public class SuggestionSpan extends CharacterStyle implements ParcelableSpan { 53 54 private static final String TAG = "SuggestionSpan"; 55 56 /** 57 * Sets this flag if the suggestions should be easily accessible with few interactions. 58 * This flag should be set for every suggestions that the user is likely to use. 59 */ 60 public static final int FLAG_EASY_CORRECT = 0x0001; 61 62 /** 63 * Sets this flag if the suggestions apply to a misspelled word/text. This type of suggestion is 64 * rendered differently to highlight the error. 65 */ 66 public static final int FLAG_MISSPELLED = 0x0002; 67 68 /** 69 * Sets this flag if the auto correction is about to be applied to a word/text 70 * that the user is typing/composing. This type of suggestion is rendered differently 71 * to indicate the auto correction is happening. 72 */ 73 public static final int FLAG_AUTO_CORRECTION = 0x0004; 74 75 /** 76 * Sets this flag if the suggestions apply to a grammar error. This type of suggestion is 77 * rendered differently to highlight the error. 78 */ 79 public static final int FLAG_GRAMMAR_ERROR = 0x0008; 80 81 /** 82 * This action is deprecated in {@link android.os.Build.VERSION_CODES#Q}. 83 * 84 * @deprecated For IMEs to receive this kind of user interaction signals, implement IMEs' own 85 * suggestion picker UI instead of relying on {@link SuggestionSpan}. To retrieve 86 * bounding boxes for each character of the composing text, use 87 * {@link android.view.inputmethod.CursorAnchorInfo}. 88 */ 89 @Deprecated 90 public static final String ACTION_SUGGESTION_PICKED = "android.text.style.SUGGESTION_PICKED"; 91 92 /** 93 * This is deprecated in {@link android.os.Build.VERSION_CODES#Q}. 94 * 95 * @deprecated See {@link #ACTION_SUGGESTION_PICKED}. 96 */ 97 @Deprecated 98 public static final String SUGGESTION_SPAN_PICKED_AFTER = "after"; 99 /** 100 * This is deprecated in {@link android.os.Build.VERSION_CODES#Q}. 101 * 102 * @deprecated See {@link #ACTION_SUGGESTION_PICKED}. 103 */ 104 @Deprecated 105 public static final String SUGGESTION_SPAN_PICKED_BEFORE = "before"; 106 /** 107 * This is deprecated in {@link android.os.Build.VERSION_CODES#Q}. 108 * 109 * @deprecated See {@link #ACTION_SUGGESTION_PICKED}. 110 */ 111 @Deprecated 112 public static final String SUGGESTION_SPAN_PICKED_HASHCODE = "hashcode"; 113 114 public static final int SUGGESTIONS_MAX_SIZE = 5; 115 116 /* 117 * TODO: Needs to check the validity and add a feature that TextView will change 118 * the current IME to the other IME which is specified in SuggestionSpan. 119 * An IME needs to set the span by specifying the target IME and Subtype of SuggestionSpan. 120 * And the current IME might want to specify any IME as the target IME including other IMEs. 121 */ 122 123 private int mFlags; 124 private final String[] mSuggestions; 125 /** 126 * Kept for compatibility for apps that rely on invalid locale strings e.g. 127 * {@code new Locale(" an ", " i n v a l i d ", "data")}, which cannot be handled by 128 * {@link #mLanguageTag}. 129 */ 130 @NonNull 131 private final String mLocaleStringForCompatibility; 132 @NonNull 133 private final String mLanguageTag; 134 private final int mHashCode; 135 136 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 137 private float mEasyCorrectUnderlineThickness; 138 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 139 private int mEasyCorrectUnderlineColor; 140 141 private float mMisspelledUnderlineThickness; 142 private int mMisspelledUnderlineColor; 143 144 private float mAutoCorrectionUnderlineThickness; 145 private int mAutoCorrectionUnderlineColor; 146 147 private float mGrammarErrorUnderlineThickness; 148 private int mGrammarErrorUnderlineColor; 149 150 /** 151 * @param context Context for the application 152 * @param suggestions Suggestions for the string under the span 153 * @param flags Additional flags indicating how this span is handled in TextView 154 */ SuggestionSpan(Context context, String[] suggestions, int flags)155 public SuggestionSpan(Context context, String[] suggestions, int flags) { 156 this(context, null, suggestions, flags, null); 157 } 158 159 /** 160 * @param locale Locale of the suggestions 161 * @param suggestions Suggestions for the string under the span 162 * @param flags Additional flags indicating how this span is handled in TextView 163 */ SuggestionSpan(Locale locale, String[] suggestions, int flags)164 public SuggestionSpan(Locale locale, String[] suggestions, int flags) { 165 this(null, locale, suggestions, flags, null); 166 } 167 168 /** 169 * @param context Context for the application 170 * @param locale locale Locale of the suggestions 171 * @param suggestions Suggestions for the string under the span. Only the first up to 172 * {@link SuggestionSpan#SUGGESTIONS_MAX_SIZE} will be considered. Null values not permitted. 173 * @param flags Additional flags indicating how this span is handled in TextView 174 * @param notificationTargetClass if not null, this class will get notified when the user 175 * selects one of the suggestions. On Android 176 * {@link android.os.Build.VERSION_CODES#Q} and later this 177 * parameter is always ignored. 178 */ SuggestionSpan(Context context, Locale locale, String[] suggestions, int flags, Class<?> notificationTargetClass)179 public SuggestionSpan(Context context, Locale locale, String[] suggestions, int flags, 180 Class<?> notificationTargetClass) { 181 final int N = Math.min(SUGGESTIONS_MAX_SIZE, suggestions.length); 182 mSuggestions = Arrays.copyOf(suggestions, N); 183 mFlags = flags; 184 final Locale sourceLocale; 185 if (locale != null) { 186 sourceLocale = locale; 187 } else if (context != null) { 188 // TODO: Consider to context.getResources().getResolvedLocale() instead. 189 sourceLocale = context.getResources().getConfiguration().locale; 190 } else { 191 Log.e("SuggestionSpan", "No locale or context specified in SuggestionSpan constructor"); 192 sourceLocale = null; 193 } 194 mLocaleStringForCompatibility = sourceLocale == null ? "" : sourceLocale.toString(); 195 mLanguageTag = sourceLocale == null ? "" : sourceLocale.toLanguageTag(); 196 mHashCode = hashCodeInternal(mSuggestions, mLanguageTag, mLocaleStringForCompatibility); 197 198 initStyle(context); 199 } 200 initStyle(Context context)201 private void initStyle(Context context) { 202 if (context == null) { 203 mMisspelledUnderlineThickness = 0; 204 mGrammarErrorUnderlineThickness = 0; 205 mEasyCorrectUnderlineThickness = 0; 206 mAutoCorrectionUnderlineThickness = 0; 207 mMisspelledUnderlineColor = Color.BLACK; 208 mGrammarErrorUnderlineColor = Color.BLACK; 209 mEasyCorrectUnderlineColor = Color.BLACK; 210 mAutoCorrectionUnderlineColor = Color.BLACK; 211 return; 212 } 213 214 int defStyleAttr = com.android.internal.R.attr.textAppearanceMisspelledSuggestion; 215 TypedArray typedArray = context.obtainStyledAttributes( 216 null, com.android.internal.R.styleable.SuggestionSpan, defStyleAttr, 0); 217 mMisspelledUnderlineThickness = typedArray.getDimension( 218 com.android.internal.R.styleable.SuggestionSpan_textUnderlineThickness, 0); 219 mMisspelledUnderlineColor = typedArray.getColor( 220 com.android.internal.R.styleable.SuggestionSpan_textUnderlineColor, Color.BLACK); 221 typedArray.recycle(); 222 223 defStyleAttr = com.android.internal.R.attr.textAppearanceGrammarErrorSuggestion; 224 typedArray = context.obtainStyledAttributes( 225 null, com.android.internal.R.styleable.SuggestionSpan, defStyleAttr, 0); 226 mGrammarErrorUnderlineThickness = typedArray.getDimension( 227 com.android.internal.R.styleable.SuggestionSpan_textUnderlineThickness, 0); 228 mGrammarErrorUnderlineColor = typedArray.getColor( 229 com.android.internal.R.styleable.SuggestionSpan_textUnderlineColor, Color.BLACK); 230 typedArray.recycle(); 231 232 defStyleAttr = com.android.internal.R.attr.textAppearanceEasyCorrectSuggestion; 233 typedArray = context.obtainStyledAttributes( 234 null, com.android.internal.R.styleable.SuggestionSpan, defStyleAttr, 0); 235 mEasyCorrectUnderlineThickness = typedArray.getDimension( 236 com.android.internal.R.styleable.SuggestionSpan_textUnderlineThickness, 0); 237 mEasyCorrectUnderlineColor = typedArray.getColor( 238 com.android.internal.R.styleable.SuggestionSpan_textUnderlineColor, Color.BLACK); 239 typedArray.recycle(); 240 241 defStyleAttr = com.android.internal.R.attr.textAppearanceAutoCorrectionSuggestion; 242 typedArray = context.obtainStyledAttributes( 243 null, com.android.internal.R.styleable.SuggestionSpan, defStyleAttr, 0); 244 mAutoCorrectionUnderlineThickness = typedArray.getDimension( 245 com.android.internal.R.styleable.SuggestionSpan_textUnderlineThickness, 0); 246 mAutoCorrectionUnderlineColor = typedArray.getColor( 247 com.android.internal.R.styleable.SuggestionSpan_textUnderlineColor, Color.BLACK); 248 typedArray.recycle(); 249 } 250 SuggestionSpan(Parcel src)251 public SuggestionSpan(Parcel src) { 252 this(/* suggestions= */ src.readStringArray(), /* flags= */ src.readInt(), 253 /* localStringForCompatibility= */ src.readString(), 254 /* languageTag= */ src.readString(), /* hashCode= */ src.readInt(), 255 /* easyCorrectUnderlineColor= */ src.readInt(), 256 /* easyCorrectUnderlineThickness= */ src.readFloat(), 257 /* misspelledUnderlineColor= */ src.readInt(), 258 /* misspelledUnderlineThickness= */ src.readFloat(), 259 /* autoCorrectionUnderlineColor= */ src.readInt(), 260 /* autoCorrectionUnderlineThickness= */ src.readFloat(), 261 /* grammarErrorUnderlineColor= */ src.readInt(), 262 /* grammarErrorUnderlineThickness= */ src.readFloat()); 263 } 264 265 /** @hide */ SuggestionSpan(String[] suggestions, int flags, String localeStringForCompatibility, String languageTag, int hashCode, int easyCorrectUnderlineColor, float easyCorrectUnderlineThickness, int misspelledUnderlineColor, float misspelledUnderlineThickness, int autoCorrectionUnderlineColor, float autoCorrectionUnderlineThickness, int grammarErrorUnderlineColor, float grammarErrorUnderlineThickness)266 public SuggestionSpan(String[] suggestions, int flags, String localeStringForCompatibility, 267 String languageTag, int hashCode, int easyCorrectUnderlineColor, 268 float easyCorrectUnderlineThickness, int misspelledUnderlineColor, 269 float misspelledUnderlineThickness, int autoCorrectionUnderlineColor, 270 float autoCorrectionUnderlineThickness, int grammarErrorUnderlineColor, 271 float grammarErrorUnderlineThickness) { 272 mSuggestions = suggestions; 273 mFlags = flags; 274 mLocaleStringForCompatibility = localeStringForCompatibility; 275 mLanguageTag = languageTag; 276 mHashCode = hashCode; 277 mEasyCorrectUnderlineColor = easyCorrectUnderlineColor; 278 mEasyCorrectUnderlineThickness = easyCorrectUnderlineThickness; 279 mMisspelledUnderlineColor = misspelledUnderlineColor; 280 mMisspelledUnderlineThickness = misspelledUnderlineThickness; 281 mAutoCorrectionUnderlineColor = autoCorrectionUnderlineColor; 282 mAutoCorrectionUnderlineThickness = autoCorrectionUnderlineThickness; 283 mGrammarErrorUnderlineColor = grammarErrorUnderlineColor; 284 mGrammarErrorUnderlineThickness = grammarErrorUnderlineThickness; 285 } 286 287 288 /** 289 * @return an array of suggestion texts for this span 290 */ getSuggestions()291 public String[] getSuggestions() { 292 return mSuggestions; 293 } 294 295 /** 296 * @deprecated use {@link #getLocaleObject()} instead. 297 * @return the locale of the suggestions. An empty string is returned if no locale is specified. 298 */ 299 @NonNull 300 @Deprecated getLocale()301 public String getLocale() { 302 return mLocaleStringForCompatibility; 303 } 304 305 /** 306 * Returns a well-formed BCP 47 language tag representation of the suggestions, as a 307 * {@link Locale} object. 308 * 309 * <p><b>Caveat</b>: The returned object is guaranteed to be a a well-formed BCP 47 language tag 310 * representation. For example, this method can return an empty locale rather than returning a 311 * malformed data when this object is initialized with an malformed {@link Locale} object, e.g. 312 * {@code new Locale(" a ", " b c d ", " "}.</p> 313 * 314 * @return the locale of the suggestions. {@code null} is returned if no locale is specified. 315 */ 316 @Nullable getLocaleObject()317 public Locale getLocaleObject() { 318 return mLanguageTag.isEmpty() ? null : Locale.forLanguageTag(mLanguageTag); 319 } 320 321 /** 322 * @return {@code null}. 323 * 324 * @hide 325 * @deprecated Do not use. Always returns {@code null}. 326 */ 327 @Deprecated 328 @UnsupportedAppUsage getNotificationTargetClassName()329 public String getNotificationTargetClassName() { 330 return null; 331 } 332 getFlags()333 public int getFlags() { 334 return mFlags; 335 } 336 setFlags(int flags)337 public void setFlags(int flags) { 338 mFlags = flags; 339 } 340 341 @Override describeContents()342 public int describeContents() { 343 return 0; 344 } 345 346 @Override writeToParcel(Parcel dest, int flags)347 public void writeToParcel(Parcel dest, int flags) { 348 writeToParcelInternal(dest, flags); 349 } 350 351 /** @hide */ writeToParcelInternal(Parcel dest, int flags)352 public void writeToParcelInternal(Parcel dest, int flags) { 353 dest.writeStringArray(mSuggestions); 354 dest.writeInt(mFlags); 355 dest.writeString(mLocaleStringForCompatibility); 356 dest.writeString(mLanguageTag); 357 dest.writeInt(mHashCode); 358 dest.writeInt(mEasyCorrectUnderlineColor); 359 dest.writeFloat(mEasyCorrectUnderlineThickness); 360 dest.writeInt(mMisspelledUnderlineColor); 361 dest.writeFloat(mMisspelledUnderlineThickness); 362 dest.writeInt(mAutoCorrectionUnderlineColor); 363 dest.writeFloat(mAutoCorrectionUnderlineThickness); 364 dest.writeInt(mGrammarErrorUnderlineColor); 365 dest.writeFloat(mGrammarErrorUnderlineThickness); 366 } 367 368 @Override getSpanTypeId()369 public int getSpanTypeId() { 370 return getSpanTypeIdInternal(); 371 } 372 373 /** @hide */ getSpanTypeIdInternal()374 public int getSpanTypeIdInternal() { 375 return TextUtils.SUGGESTION_SPAN; 376 } 377 378 @Override equals(@ullable Object o)379 public boolean equals(@Nullable Object o) { 380 if (o instanceof SuggestionSpan) { 381 return ((SuggestionSpan)o).hashCode() == mHashCode; 382 } 383 return false; 384 } 385 386 @Override hashCode()387 public int hashCode() { 388 return mHashCode; 389 } 390 hashCodeInternal(String[] suggestions, @NonNull String languageTag, @NonNull String localeStringForCompatibility)391 private static int hashCodeInternal(String[] suggestions, @NonNull String languageTag, 392 @NonNull String localeStringForCompatibility) { 393 return Arrays.hashCode(new Object[] {Long.valueOf(SystemClock.uptimeMillis()), suggestions, 394 languageTag, localeStringForCompatibility}); 395 } 396 397 public static final @android.annotation.NonNull Parcelable.Creator<SuggestionSpan> CREATOR = 398 new Parcelable.Creator<SuggestionSpan>() { 399 @Override 400 public SuggestionSpan createFromParcel(Parcel source) { 401 return new SuggestionSpan(source); 402 } 403 404 @Override 405 public SuggestionSpan[] newArray(int size) { 406 return new SuggestionSpan[size]; 407 } 408 }; 409 410 @Override updateDrawState(TextPaint tp)411 public void updateDrawState(TextPaint tp) { 412 final boolean misspelled = (mFlags & FLAG_MISSPELLED) != 0; 413 final boolean easy = (mFlags & FLAG_EASY_CORRECT) != 0; 414 final boolean autoCorrection = (mFlags & FLAG_AUTO_CORRECTION) != 0; 415 final boolean grammarError = (mFlags & FLAG_GRAMMAR_ERROR) != 0; 416 if (easy) { 417 if (!misspelled && !grammarError) { 418 tp.setUnderlineText(mEasyCorrectUnderlineColor, mEasyCorrectUnderlineThickness); 419 } else if (tp.underlineColor == 0) { 420 // Spans are rendered in an arbitrary order. Since misspelled is less prioritary 421 // than just easy, do not apply misspelled if an easy (or a mispelled) has been set 422 if (grammarError) { 423 tp.setUnderlineText( 424 mGrammarErrorUnderlineColor, mGrammarErrorUnderlineThickness); 425 } else { 426 tp.setUnderlineText(mMisspelledUnderlineColor, mMisspelledUnderlineThickness); 427 } 428 } 429 } else if (autoCorrection) { 430 tp.setUnderlineText(mAutoCorrectionUnderlineColor, mAutoCorrectionUnderlineThickness); 431 } else if (misspelled) { 432 tp.setUnderlineText(mMisspelledUnderlineColor, mMisspelledUnderlineThickness); 433 } else if (grammarError) { 434 tp.setUnderlineText(mGrammarErrorUnderlineColor, mGrammarErrorUnderlineThickness); 435 } 436 } 437 438 /** 439 * @return The color of the underline for that span, or 0 if there is no underline 440 */ 441 @ColorInt getUnderlineColor()442 public int getUnderlineColor() { 443 // The order here should match what is used in updateDrawState 444 final boolean misspelled = (mFlags & FLAG_MISSPELLED) != 0; 445 final boolean easy = (mFlags & FLAG_EASY_CORRECT) != 0; 446 final boolean autoCorrection = (mFlags & FLAG_AUTO_CORRECTION) != 0; 447 final boolean grammarError = (mFlags & FLAG_GRAMMAR_ERROR) != 0; 448 if (easy) { 449 if (grammarError) { 450 return mGrammarErrorUnderlineColor; 451 } else if (misspelled) { 452 return mMisspelledUnderlineColor; 453 } else { 454 return mEasyCorrectUnderlineColor; 455 } 456 } else if (autoCorrection) { 457 return mAutoCorrectionUnderlineColor; 458 } else if (misspelled) { 459 return mMisspelledUnderlineColor; 460 } else if (grammarError) { 461 return mGrammarErrorUnderlineColor; 462 } 463 return 0; 464 } 465 466 /** 467 * Does nothing. 468 * 469 * @deprecated this is deprecated in {@link android.os.Build.VERSION_CODES#Q}. 470 * @hide 471 */ 472 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 473 @Deprecated notifySelection(Context context, String original, int index)474 public void notifySelection(Context context, String original, int index) { 475 Log.w(TAG, "notifySelection() is deprecated. Does nothing."); 476 } 477 478 /** @hide */ getEasyCorrectUnderlineThickness()479 public float getEasyCorrectUnderlineThickness() { 480 return mEasyCorrectUnderlineThickness; 481 } 482 483 /** @hide */ getEasyCorrectUnderlineColor()484 public int getEasyCorrectUnderlineColor() { 485 return mEasyCorrectUnderlineColor; 486 } 487 488 /** @hide */ getMisspelledUnderlineThickness()489 public float getMisspelledUnderlineThickness() { 490 return mMisspelledUnderlineThickness; 491 } 492 493 /** @hide */ getMisspelledUnderlineColor()494 public int getMisspelledUnderlineColor() { 495 return mMisspelledUnderlineColor; 496 } 497 498 /** @hide */ getAutoCorrectionUnderlineThickness()499 public float getAutoCorrectionUnderlineThickness() { 500 return mAutoCorrectionUnderlineThickness; 501 } 502 503 /** @hide */ getAutoCorrectionUnderlineColor()504 public int getAutoCorrectionUnderlineColor() { 505 return mAutoCorrectionUnderlineColor; 506 } 507 508 /** @hide */ getGrammarErrorUnderlineThickness()509 public float getGrammarErrorUnderlineThickness() { 510 return mGrammarErrorUnderlineThickness; 511 } 512 513 /** @hide */ getGrammarErrorUnderlineColor()514 public int getGrammarErrorUnderlineColor() { 515 return mGrammarErrorUnderlineColor; 516 } 517 } 518