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