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 typedArray.recycle(); 221 222 defStyleAttr = com.android.internal.R.attr.textAppearanceGrammarErrorSuggestion; 223 typedArray = context.obtainStyledAttributes( 224 null, com.android.internal.R.styleable.SuggestionSpan, defStyleAttr, 0); 225 mGrammarErrorUnderlineThickness = typedArray.getDimension( 226 com.android.internal.R.styleable.SuggestionSpan_textUnderlineThickness, 0); 227 mGrammarErrorUnderlineColor = typedArray.getColor( 228 com.android.internal.R.styleable.SuggestionSpan_textUnderlineColor, Color.BLACK); 229 typedArray.recycle(); 230 231 defStyleAttr = com.android.internal.R.attr.textAppearanceEasyCorrectSuggestion; 232 typedArray = context.obtainStyledAttributes( 233 null, com.android.internal.R.styleable.SuggestionSpan, defStyleAttr, 0); 234 mEasyCorrectUnderlineThickness = typedArray.getDimension( 235 com.android.internal.R.styleable.SuggestionSpan_textUnderlineThickness, 0); 236 mEasyCorrectUnderlineColor = typedArray.getColor( 237 com.android.internal.R.styleable.SuggestionSpan_textUnderlineColor, Color.BLACK); 238 typedArray.recycle(); 239 240 defStyleAttr = com.android.internal.R.attr.textAppearanceAutoCorrectionSuggestion; 241 typedArray = context.obtainStyledAttributes( 242 null, com.android.internal.R.styleable.SuggestionSpan, defStyleAttr, 0); 243 mAutoCorrectionUnderlineThickness = typedArray.getDimension( 244 com.android.internal.R.styleable.SuggestionSpan_textUnderlineThickness, 0); 245 mAutoCorrectionUnderlineColor = typedArray.getColor( 246 com.android.internal.R.styleable.SuggestionSpan_textUnderlineColor, Color.BLACK); 247 typedArray.recycle(); 248 } 249 SuggestionSpan(Parcel src)250 public SuggestionSpan(Parcel src) { 251 mSuggestions = src.readStringArray(); 252 mFlags = src.readInt(); 253 mLocaleStringForCompatibility = src.readString(); 254 mLanguageTag = src.readString(); 255 mHashCode = src.readInt(); 256 mEasyCorrectUnderlineColor = src.readInt(); 257 mEasyCorrectUnderlineThickness = src.readFloat(); 258 mMisspelledUnderlineColor = src.readInt(); 259 mMisspelledUnderlineThickness = src.readFloat(); 260 mAutoCorrectionUnderlineColor = src.readInt(); 261 mAutoCorrectionUnderlineThickness = src.readFloat(); 262 mGrammarErrorUnderlineColor = src.readInt(); 263 mGrammarErrorUnderlineThickness = src.readFloat(); 264 } 265 266 /** 267 * @return an array of suggestion texts for this span 268 */ getSuggestions()269 public String[] getSuggestions() { 270 return mSuggestions; 271 } 272 273 /** 274 * @deprecated use {@link #getLocaleObject()} instead. 275 * @return the locale of the suggestions. An empty string is returned if no locale is specified. 276 */ 277 @NonNull 278 @Deprecated getLocale()279 public String getLocale() { 280 return mLocaleStringForCompatibility; 281 } 282 283 /** 284 * Returns a well-formed BCP 47 language tag representation of the suggestions, as a 285 * {@link Locale} object. 286 * 287 * <p><b>Caveat</b>: The returned object is guaranteed to be a a well-formed BCP 47 language tag 288 * representation. For example, this method can return an empty locale rather than returning a 289 * malformed data when this object is initialized with an malformed {@link Locale} object, e.g. 290 * {@code new Locale(" a ", " b c d ", " "}.</p> 291 * 292 * @return the locale of the suggestions. {@code null} is returned if no locale is specified. 293 */ 294 @Nullable getLocaleObject()295 public Locale getLocaleObject() { 296 return mLanguageTag.isEmpty() ? null : Locale.forLanguageTag(mLanguageTag); 297 } 298 299 /** 300 * @return {@code null}. 301 * 302 * @hide 303 * @deprecated Do not use. Always returns {@code null}. 304 */ 305 @Deprecated 306 @UnsupportedAppUsage getNotificationTargetClassName()307 public String getNotificationTargetClassName() { 308 return null; 309 } 310 getFlags()311 public int getFlags() { 312 return mFlags; 313 } 314 setFlags(int flags)315 public void setFlags(int flags) { 316 mFlags = flags; 317 } 318 319 @Override describeContents()320 public int describeContents() { 321 return 0; 322 } 323 324 @Override writeToParcel(Parcel dest, int flags)325 public void writeToParcel(Parcel dest, int flags) { 326 writeToParcelInternal(dest, flags); 327 } 328 329 /** @hide */ writeToParcelInternal(Parcel dest, int flags)330 public void writeToParcelInternal(Parcel dest, int flags) { 331 dest.writeStringArray(mSuggestions); 332 dest.writeInt(mFlags); 333 dest.writeString(mLocaleStringForCompatibility); 334 dest.writeString(mLanguageTag); 335 dest.writeInt(mHashCode); 336 dest.writeInt(mEasyCorrectUnderlineColor); 337 dest.writeFloat(mEasyCorrectUnderlineThickness); 338 dest.writeInt(mMisspelledUnderlineColor); 339 dest.writeFloat(mMisspelledUnderlineThickness); 340 dest.writeInt(mAutoCorrectionUnderlineColor); 341 dest.writeFloat(mAutoCorrectionUnderlineThickness); 342 dest.writeInt(mGrammarErrorUnderlineColor); 343 dest.writeFloat(mGrammarErrorUnderlineThickness); 344 } 345 346 @Override getSpanTypeId()347 public int getSpanTypeId() { 348 return getSpanTypeIdInternal(); 349 } 350 351 /** @hide */ getSpanTypeIdInternal()352 public int getSpanTypeIdInternal() { 353 return TextUtils.SUGGESTION_SPAN; 354 } 355 356 @Override equals(@ullable Object o)357 public boolean equals(@Nullable Object o) { 358 if (o instanceof SuggestionSpan) { 359 return ((SuggestionSpan)o).hashCode() == mHashCode; 360 } 361 return false; 362 } 363 364 @Override hashCode()365 public int hashCode() { 366 return mHashCode; 367 } 368 hashCodeInternal(String[] suggestions, @NonNull String languageTag, @NonNull String localeStringForCompatibility)369 private static int hashCodeInternal(String[] suggestions, @NonNull String languageTag, 370 @NonNull String localeStringForCompatibility) { 371 return Arrays.hashCode(new Object[] {Long.valueOf(SystemClock.uptimeMillis()), suggestions, 372 languageTag, localeStringForCompatibility}); 373 } 374 375 public static final @android.annotation.NonNull Parcelable.Creator<SuggestionSpan> CREATOR = 376 new Parcelable.Creator<SuggestionSpan>() { 377 @Override 378 public SuggestionSpan createFromParcel(Parcel source) { 379 return new SuggestionSpan(source); 380 } 381 382 @Override 383 public SuggestionSpan[] newArray(int size) { 384 return new SuggestionSpan[size]; 385 } 386 }; 387 388 @Override updateDrawState(TextPaint tp)389 public void updateDrawState(TextPaint tp) { 390 final boolean misspelled = (mFlags & FLAG_MISSPELLED) != 0; 391 final boolean easy = (mFlags & FLAG_EASY_CORRECT) != 0; 392 final boolean autoCorrection = (mFlags & FLAG_AUTO_CORRECTION) != 0; 393 final boolean grammarError = (mFlags & FLAG_GRAMMAR_ERROR) != 0; 394 if (easy) { 395 if (!misspelled && !grammarError) { 396 tp.setUnderlineText(mEasyCorrectUnderlineColor, mEasyCorrectUnderlineThickness); 397 } else if (tp.underlineColor == 0) { 398 // Spans are rendered in an arbitrary order. Since misspelled is less prioritary 399 // than just easy, do not apply misspelled if an easy (or a mispelled) has been set 400 if (grammarError) { 401 tp.setUnderlineText( 402 mGrammarErrorUnderlineColor, mGrammarErrorUnderlineThickness); 403 } else { 404 tp.setUnderlineText(mMisspelledUnderlineColor, mMisspelledUnderlineThickness); 405 } 406 } 407 } else if (autoCorrection) { 408 tp.setUnderlineText(mAutoCorrectionUnderlineColor, mAutoCorrectionUnderlineThickness); 409 } else if (misspelled) { 410 tp.setUnderlineText(mMisspelledUnderlineColor, mMisspelledUnderlineThickness); 411 } else if (grammarError) { 412 tp.setUnderlineText(mGrammarErrorUnderlineColor, mGrammarErrorUnderlineThickness); 413 } 414 } 415 416 /** 417 * @return The color of the underline for that span, or 0 if there is no underline 418 */ 419 @ColorInt getUnderlineColor()420 public int getUnderlineColor() { 421 // The order here should match what is used in updateDrawState 422 final boolean misspelled = (mFlags & FLAG_MISSPELLED) != 0; 423 final boolean easy = (mFlags & FLAG_EASY_CORRECT) != 0; 424 final boolean autoCorrection = (mFlags & FLAG_AUTO_CORRECTION) != 0; 425 final boolean grammarError = (mFlags & FLAG_GRAMMAR_ERROR) != 0; 426 if (easy) { 427 if (grammarError) { 428 return mGrammarErrorUnderlineColor; 429 } else if (misspelled) { 430 return mMisspelledUnderlineColor; 431 } else { 432 return mEasyCorrectUnderlineColor; 433 } 434 } else if (autoCorrection) { 435 return mAutoCorrectionUnderlineColor; 436 } else if (misspelled) { 437 return mMisspelledUnderlineColor; 438 } else if (grammarError) { 439 return mGrammarErrorUnderlineColor; 440 } 441 return 0; 442 } 443 444 /** 445 * Does nothing. 446 * 447 * @deprecated this is deprecated in {@link android.os.Build.VERSION_CODES#Q}. 448 * @hide 449 */ 450 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 451 @Deprecated notifySelection(Context context, String original, int index)452 public void notifySelection(Context context, String original, int index) { 453 Log.w(TAG, "notifySelection() is deprecated. Does nothing."); 454 } 455 } 456