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