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 17package android.text.style; 18 19import android.annotation.NonNull; 20import android.annotation.Nullable; 21import android.content.Context; 22import android.content.Intent; 23import android.content.res.TypedArray; 24import android.graphics.Color; 25import android.os.Parcel; 26import android.os.Parcelable; 27import android.os.SystemClock; 28import android.text.ParcelableSpan; 29import android.text.TextPaint; 30import android.text.TextUtils; 31import android.util.Log; 32import android.view.inputmethod.InputMethodManager; 33import android.widget.TextView; 34 35import java.util.Arrays; 36import 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 */ 50public 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 public static final String ACTION_SUGGESTION_PICKED = "android.text.style.SUGGESTION_PICKED"; 74 public static final String SUGGESTION_SPAN_PICKED_AFTER = "after"; 75 public static final String SUGGESTION_SPAN_PICKED_BEFORE = "before"; 76 public static final String SUGGESTION_SPAN_PICKED_HASHCODE = "hashcode"; 77 78 public static final int SUGGESTIONS_MAX_SIZE = 5; 79 80 /* 81 * TODO: Needs to check the validity and add a feature that TextView will change 82 * the current IME to the other IME which is specified in SuggestionSpan. 83 * An IME needs to set the span by specifying the target IME and Subtype of SuggestionSpan. 84 * And the current IME might want to specify any IME as the target IME including other IMEs. 85 */ 86 87 private int mFlags; 88 private final String[] mSuggestions; 89 /** 90 * Kept for compatibility for apps that rely on invalid locale strings e.g. 91 * {@code new Locale(" an ", " i n v a l i d ", "data")}, which cannot be handled by 92 * {@link #mLanguageTag}. 93 */ 94 @NonNull 95 private final String mLocaleStringForCompatibility; 96 @NonNull 97 private final String mLanguageTag; 98 private final String mNotificationTargetClassName; 99 private final String mNotificationTargetPackageName; 100 private final int mHashCode; 101 102 private float mEasyCorrectUnderlineThickness; 103 private int mEasyCorrectUnderlineColor; 104 105 private float mMisspelledUnderlineThickness; 106 private int mMisspelledUnderlineColor; 107 108 private float mAutoCorrectionUnderlineThickness; 109 private int mAutoCorrectionUnderlineColor; 110 111 /** 112 * @param context Context for the application 113 * @param suggestions Suggestions for the string under the span 114 * @param flags Additional flags indicating how this span is handled in TextView 115 */ 116 public SuggestionSpan(Context context, String[] suggestions, int flags) { 117 this(context, null, suggestions, flags, null); 118 } 119 120 /** 121 * @param locale Locale of the suggestions 122 * @param suggestions Suggestions for the string under the span 123 * @param flags Additional flags indicating how this span is handled in TextView 124 */ 125 public SuggestionSpan(Locale locale, String[] suggestions, int flags) { 126 this(null, locale, suggestions, flags, null); 127 } 128 129 /** 130 * @param context Context for the application 131 * @param locale locale Locale of the suggestions 132 * @param suggestions Suggestions for the string under the span. Only the first up to 133 * {@link SuggestionSpan#SUGGESTIONS_MAX_SIZE} will be considered. Null values not permitted. 134 * @param flags Additional flags indicating how this span is handled in TextView 135 * @param notificationTargetClass if not null, this class will get notified when the user 136 * selects one of the suggestions. 137 */ 138 public SuggestionSpan(Context context, Locale locale, String[] suggestions, int flags, 139 Class<?> notificationTargetClass) { 140 final int N = Math.min(SUGGESTIONS_MAX_SIZE, suggestions.length); 141 mSuggestions = Arrays.copyOf(suggestions, N); 142 mFlags = flags; 143 final Locale sourceLocale; 144 if (locale != null) { 145 sourceLocale = locale; 146 } else if (context != null) { 147 // TODO: Consider to context.getResources().getResolvedLocale() instead. 148 sourceLocale = context.getResources().getConfiguration().locale; 149 } else { 150 Log.e("SuggestionSpan", "No locale or context specified in SuggestionSpan constructor"); 151 sourceLocale = null; 152 } 153 mLocaleStringForCompatibility = sourceLocale == null ? "" : sourceLocale.toString(); 154 mLanguageTag = sourceLocale == null ? "" : sourceLocale.toLanguageTag(); 155 156 if (context != null) { 157 mNotificationTargetPackageName = context.getPackageName(); 158 } else { 159 mNotificationTargetPackageName = null; 160 } 161 162 if (notificationTargetClass != null) { 163 mNotificationTargetClassName = notificationTargetClass.getCanonicalName(); 164 } else { 165 mNotificationTargetClassName = ""; 166 } 167 mHashCode = hashCodeInternal(mSuggestions, mLanguageTag, mLocaleStringForCompatibility, 168 mNotificationTargetClassName); 169 170 initStyle(context); 171 } 172 173 private void initStyle(Context context) { 174 if (context == null) { 175 mMisspelledUnderlineThickness = 0; 176 mEasyCorrectUnderlineThickness = 0; 177 mAutoCorrectionUnderlineThickness = 0; 178 mMisspelledUnderlineColor = Color.BLACK; 179 mEasyCorrectUnderlineColor = Color.BLACK; 180 mAutoCorrectionUnderlineColor = Color.BLACK; 181 return; 182 } 183 184 int defStyleAttr = com.android.internal.R.attr.textAppearanceMisspelledSuggestion; 185 TypedArray typedArray = context.obtainStyledAttributes( 186 null, com.android.internal.R.styleable.SuggestionSpan, defStyleAttr, 0); 187 mMisspelledUnderlineThickness = typedArray.getDimension( 188 com.android.internal.R.styleable.SuggestionSpan_textUnderlineThickness, 0); 189 mMisspelledUnderlineColor = typedArray.getColor( 190 com.android.internal.R.styleable.SuggestionSpan_textUnderlineColor, Color.BLACK); 191 192 defStyleAttr = com.android.internal.R.attr.textAppearanceEasyCorrectSuggestion; 193 typedArray = context.obtainStyledAttributes( 194 null, com.android.internal.R.styleable.SuggestionSpan, defStyleAttr, 0); 195 mEasyCorrectUnderlineThickness = typedArray.getDimension( 196 com.android.internal.R.styleable.SuggestionSpan_textUnderlineThickness, 0); 197 mEasyCorrectUnderlineColor = typedArray.getColor( 198 com.android.internal.R.styleable.SuggestionSpan_textUnderlineColor, Color.BLACK); 199 200 defStyleAttr = com.android.internal.R.attr.textAppearanceAutoCorrectionSuggestion; 201 typedArray = context.obtainStyledAttributes( 202 null, com.android.internal.R.styleable.SuggestionSpan, defStyleAttr, 0); 203 mAutoCorrectionUnderlineThickness = typedArray.getDimension( 204 com.android.internal.R.styleable.SuggestionSpan_textUnderlineThickness, 0); 205 mAutoCorrectionUnderlineColor = typedArray.getColor( 206 com.android.internal.R.styleable.SuggestionSpan_textUnderlineColor, Color.BLACK); 207 } 208 209 public SuggestionSpan(Parcel src) { 210 mSuggestions = src.readStringArray(); 211 mFlags = src.readInt(); 212 mLocaleStringForCompatibility = src.readString(); 213 mLanguageTag = src.readString(); 214 mNotificationTargetClassName = src.readString(); 215 mNotificationTargetPackageName = src.readString(); 216 mHashCode = src.readInt(); 217 mEasyCorrectUnderlineColor = src.readInt(); 218 mEasyCorrectUnderlineThickness = src.readFloat(); 219 mMisspelledUnderlineColor = src.readInt(); 220 mMisspelledUnderlineThickness = src.readFloat(); 221 mAutoCorrectionUnderlineColor = src.readInt(); 222 mAutoCorrectionUnderlineThickness = src.readFloat(); 223 } 224 225 /** 226 * @return an array of suggestion texts for this span 227 */ 228 public String[] getSuggestions() { 229 return mSuggestions; 230 } 231 232 /** 233 * @deprecated use {@link #getLocaleObject()} instead. 234 * @return the locale of the suggestions. An empty string is returned if no locale is specified. 235 */ 236 @NonNull 237 @Deprecated 238 public String getLocale() { 239 return mLocaleStringForCompatibility; 240 } 241 242 /** 243 * Returns a well-formed BCP 47 language tag representation of the suggestions, as a 244 * {@link Locale} object. 245 * 246 * <p><b>Caveat</b>: The returned object is guaranteed to be a a well-formed BCP 47 language tag 247 * representation. For example, this method can return an empty locale rather than returning a 248 * malformed data when this object is initialized with an malformed {@link Locale} object, e.g. 249 * {@code new Locale(" a ", " b c d ", " "}.</p> 250 * 251 * @return the locale of the suggestions. {@code null} is returned if no locale is specified. 252 */ 253 @Nullable 254 public Locale getLocaleObject() { 255 return mLanguageTag.isEmpty() ? null : Locale.forLanguageTag(mLanguageTag); 256 } 257 258 /** 259 * @return The name of the class to notify. The class of the original IME package will receive 260 * a notification when the user selects one of the suggestions. The notification will include 261 * the original string, the suggested replacement string as well as the hashCode of this span. 262 * The class will get notified by an intent that has those information. 263 * This is an internal API because only the framework should know the class name. 264 * 265 * @hide 266 */ 267 public String getNotificationTargetClassName() { 268 return mNotificationTargetClassName; 269 } 270 271 public int getFlags() { 272 return mFlags; 273 } 274 275 public void setFlags(int flags) { 276 mFlags = flags; 277 } 278 279 @Override 280 public int describeContents() { 281 return 0; 282 } 283 284 @Override 285 public void writeToParcel(Parcel dest, int flags) { 286 writeToParcelInternal(dest, flags); 287 } 288 289 /** @hide */ 290 public void writeToParcelInternal(Parcel dest, int flags) { 291 dest.writeStringArray(mSuggestions); 292 dest.writeInt(mFlags); 293 dest.writeString(mLocaleStringForCompatibility); 294 dest.writeString(mLanguageTag); 295 dest.writeString(mNotificationTargetClassName); 296 dest.writeString(mNotificationTargetPackageName); 297 dest.writeInt(mHashCode); 298 dest.writeInt(mEasyCorrectUnderlineColor); 299 dest.writeFloat(mEasyCorrectUnderlineThickness); 300 dest.writeInt(mMisspelledUnderlineColor); 301 dest.writeFloat(mMisspelledUnderlineThickness); 302 dest.writeInt(mAutoCorrectionUnderlineColor); 303 dest.writeFloat(mAutoCorrectionUnderlineThickness); 304 } 305 306 @Override 307 public int getSpanTypeId() { 308 return getSpanTypeIdInternal(); 309 } 310 311 /** @hide */ 312 public int getSpanTypeIdInternal() { 313 return TextUtils.SUGGESTION_SPAN; 314 } 315 316 @Override 317 public boolean equals(Object o) { 318 if (o instanceof SuggestionSpan) { 319 return ((SuggestionSpan)o).hashCode() == mHashCode; 320 } 321 return false; 322 } 323 324 @Override 325 public int hashCode() { 326 return mHashCode; 327 } 328 329 private static int hashCodeInternal(String[] suggestions, @NonNull String languageTag, 330 @NonNull String localeStringForCompatibility, String notificationTargetClassName) { 331 return Arrays.hashCode(new Object[] {Long.valueOf(SystemClock.uptimeMillis()), suggestions, 332 languageTag, localeStringForCompatibility, notificationTargetClassName}); 333 } 334 335 public static final Parcelable.Creator<SuggestionSpan> CREATOR = 336 new Parcelable.Creator<SuggestionSpan>() { 337 @Override 338 public SuggestionSpan createFromParcel(Parcel source) { 339 return new SuggestionSpan(source); 340 } 341 342 @Override 343 public SuggestionSpan[] newArray(int size) { 344 return new SuggestionSpan[size]; 345 } 346 }; 347 348 @Override 349 public void updateDrawState(TextPaint tp) { 350 final boolean misspelled = (mFlags & FLAG_MISSPELLED) != 0; 351 final boolean easy = (mFlags & FLAG_EASY_CORRECT) != 0; 352 final boolean autoCorrection = (mFlags & FLAG_AUTO_CORRECTION) != 0; 353 if (easy) { 354 if (!misspelled) { 355 tp.setUnderlineText(mEasyCorrectUnderlineColor, mEasyCorrectUnderlineThickness); 356 } else if (tp.underlineColor == 0) { 357 // Spans are rendered in an arbitrary order. Since misspelled is less prioritary 358 // than just easy, do not apply misspelled if an easy (or a mispelled) has been set 359 tp.setUnderlineText(mMisspelledUnderlineColor, mMisspelledUnderlineThickness); 360 } 361 } else if (autoCorrection) { 362 tp.setUnderlineText(mAutoCorrectionUnderlineColor, mAutoCorrectionUnderlineThickness); 363 } 364 } 365 366 /** 367 * @return The color of the underline for that span, or 0 if there is no underline 368 * 369 * @hide 370 */ 371 public int getUnderlineColor() { 372 // The order here should match what is used in updateDrawState 373 final boolean misspelled = (mFlags & FLAG_MISSPELLED) != 0; 374 final boolean easy = (mFlags & FLAG_EASY_CORRECT) != 0; 375 final boolean autoCorrection = (mFlags & FLAG_AUTO_CORRECTION) != 0; 376 if (easy) { 377 if (!misspelled) { 378 return mEasyCorrectUnderlineColor; 379 } else { 380 return mMisspelledUnderlineColor; 381 } 382 } else if (autoCorrection) { 383 return mAutoCorrectionUnderlineColor; 384 } 385 return 0; 386 } 387 388 /** 389 * Notifies a suggestion selection. 390 * 391 * @hide 392 */ 393 public void notifySelection(Context context, String original, int index) { 394 final Intent intent = new Intent(); 395 396 if (context == null || mNotificationTargetClassName == null) { 397 return; 398 } 399 // Ensures that only a class in the original IME package will receive the 400 // notification. 401 if (mSuggestions == null || index < 0 || index >= mSuggestions.length) { 402 Log.w(TAG, "Unable to notify the suggestion as the index is out of range index=" + index 403 + " length=" + mSuggestions.length); 404 return; 405 } 406 407 // The package name is not mandatory (legacy from JB), and if the package name 408 // is missing, we try to notify the suggestion through the input method manager. 409 if (mNotificationTargetPackageName != null) { 410 intent.setClassName(mNotificationTargetPackageName, mNotificationTargetClassName); 411 intent.setAction(SuggestionSpan.ACTION_SUGGESTION_PICKED); 412 intent.putExtra(SuggestionSpan.SUGGESTION_SPAN_PICKED_BEFORE, original); 413 intent.putExtra(SuggestionSpan.SUGGESTION_SPAN_PICKED_AFTER, mSuggestions[index]); 414 intent.putExtra(SuggestionSpan.SUGGESTION_SPAN_PICKED_HASHCODE, hashCode()); 415 context.sendBroadcast(intent); 416 } else { 417 InputMethodManager imm = InputMethodManager.peekInstance(); 418 if (imm != null) { 419 imm.notifySuggestionPicked(this, original, index); 420 } 421 } 422 } 423} 424