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.content.Context; 20 import android.content.Intent; 21 import android.content.res.TypedArray; 22 import android.graphics.Color; 23 import android.os.Parcel; 24 import android.os.Parcelable; 25 import android.os.SystemClock; 26 import android.text.ParcelableSpan; 27 import android.text.TextPaint; 28 import android.text.TextUtils; 29 import android.util.Log; 30 import android.view.inputmethod.InputMethodManager; 31 import android.widget.TextView; 32 33 import java.util.Arrays; 34 import java.util.Locale; 35 36 /** 37 * Holds suggestion candidates for the text enclosed in this span. 38 * 39 * When such a span is edited in an EditText, double tapping on the text enclosed in this span will 40 * display a popup dialog listing suggestion replacement for that text. The user can then replace 41 * the original text by one of the suggestions. 42 * 43 * These spans should typically be created by the input method to provide correction and alternates 44 * for the text. 45 * 46 * @see TextView#isSuggestionsEnabled() 47 */ 48 public class SuggestionSpan extends CharacterStyle implements ParcelableSpan { 49 50 private static final String TAG = "SuggestionSpan"; 51 52 /** 53 * Sets this flag if the suggestions should be easily accessible with few interactions. 54 * This flag should be set for every suggestions that the user is likely to use. 55 */ 56 public static final int FLAG_EASY_CORRECT = 0x0001; 57 58 /** 59 * Sets this flag if the suggestions apply to a misspelled word/text. This type of suggestion is 60 * rendered differently to highlight the error. 61 */ 62 public static final int FLAG_MISSPELLED = 0x0002; 63 64 /** 65 * Sets this flag if the auto correction is about to be applied to a word/text 66 * that the user is typing/composing. This type of suggestion is rendered differently 67 * to indicate the auto correction is happening. 68 */ 69 public static final int FLAG_AUTO_CORRECTION = 0x0004; 70 71 public static final String ACTION_SUGGESTION_PICKED = "android.text.style.SUGGESTION_PICKED"; 72 public static final String SUGGESTION_SPAN_PICKED_AFTER = "after"; 73 public static final String SUGGESTION_SPAN_PICKED_BEFORE = "before"; 74 public static final String SUGGESTION_SPAN_PICKED_HASHCODE = "hashcode"; 75 76 public static final int SUGGESTIONS_MAX_SIZE = 5; 77 78 /* 79 * TODO: Needs to check the validity and add a feature that TextView will change 80 * the current IME to the other IME which is specified in SuggestionSpan. 81 * An IME needs to set the span by specifying the target IME and Subtype of SuggestionSpan. 82 * And the current IME might want to specify any IME as the target IME including other IMEs. 83 */ 84 85 private int mFlags; 86 private final String[] mSuggestions; 87 private final String mLocaleString; 88 private final String mNotificationTargetClassName; 89 private final String mNotificationTargetPackageName; 90 private final int mHashCode; 91 92 private float mEasyCorrectUnderlineThickness; 93 private int mEasyCorrectUnderlineColor; 94 95 private float mMisspelledUnderlineThickness; 96 private int mMisspelledUnderlineColor; 97 98 private float mAutoCorrectionUnderlineThickness; 99 private int mAutoCorrectionUnderlineColor; 100 101 /** 102 * @param context Context for the application 103 * @param suggestions Suggestions for the string under the span 104 * @param flags Additional flags indicating how this span is handled in TextView 105 */ SuggestionSpan(Context context, String[] suggestions, int flags)106 public SuggestionSpan(Context context, String[] suggestions, int flags) { 107 this(context, null, suggestions, flags, null); 108 } 109 110 /** 111 * @param locale Locale of the suggestions 112 * @param suggestions Suggestions for the string under the span 113 * @param flags Additional flags indicating how this span is handled in TextView 114 */ SuggestionSpan(Locale locale, String[] suggestions, int flags)115 public SuggestionSpan(Locale locale, String[] suggestions, int flags) { 116 this(null, locale, suggestions, flags, null); 117 } 118 119 /** 120 * @param context Context for the application 121 * @param locale locale Locale of the suggestions 122 * @param suggestions Suggestions for the string under the span. Only the first up to 123 * {@link SuggestionSpan#SUGGESTIONS_MAX_SIZE} will be considered. Null values not permitted. 124 * @param flags Additional flags indicating how this span is handled in TextView 125 * @param notificationTargetClass if not null, this class will get notified when the user 126 * selects one of the suggestions. 127 */ SuggestionSpan(Context context, Locale locale, String[] suggestions, int flags, Class<?> notificationTargetClass)128 public SuggestionSpan(Context context, Locale locale, String[] suggestions, int flags, 129 Class<?> notificationTargetClass) { 130 final int N = Math.min(SUGGESTIONS_MAX_SIZE, suggestions.length); 131 mSuggestions = Arrays.copyOf(suggestions, N); 132 mFlags = flags; 133 if (locale != null) { 134 mLocaleString = locale.toString(); 135 } else if (context != null) { 136 mLocaleString = context.getResources().getConfiguration().locale.toString(); 137 } else { 138 Log.e("SuggestionSpan", "No locale or context specified in SuggestionSpan constructor"); 139 mLocaleString = ""; 140 } 141 142 if (context != null) { 143 mNotificationTargetPackageName = context.getPackageName(); 144 } else { 145 mNotificationTargetPackageName = null; 146 } 147 148 if (notificationTargetClass != null) { 149 mNotificationTargetClassName = notificationTargetClass.getCanonicalName(); 150 } else { 151 mNotificationTargetClassName = ""; 152 } 153 mHashCode = hashCodeInternal(mSuggestions, mLocaleString, mNotificationTargetClassName); 154 155 initStyle(context); 156 } 157 initStyle(Context context)158 private void initStyle(Context context) { 159 if (context == null) { 160 mMisspelledUnderlineThickness = 0; 161 mEasyCorrectUnderlineThickness = 0; 162 mAutoCorrectionUnderlineThickness = 0; 163 mMisspelledUnderlineColor = Color.BLACK; 164 mEasyCorrectUnderlineColor = Color.BLACK; 165 mAutoCorrectionUnderlineColor = Color.BLACK; 166 return; 167 } 168 169 int defStyleAttr = com.android.internal.R.attr.textAppearanceMisspelledSuggestion; 170 TypedArray typedArray = context.obtainStyledAttributes( 171 null, com.android.internal.R.styleable.SuggestionSpan, defStyleAttr, 0); 172 mMisspelledUnderlineThickness = typedArray.getDimension( 173 com.android.internal.R.styleable.SuggestionSpan_textUnderlineThickness, 0); 174 mMisspelledUnderlineColor = typedArray.getColor( 175 com.android.internal.R.styleable.SuggestionSpan_textUnderlineColor, Color.BLACK); 176 177 defStyleAttr = com.android.internal.R.attr.textAppearanceEasyCorrectSuggestion; 178 typedArray = context.obtainStyledAttributes( 179 null, com.android.internal.R.styleable.SuggestionSpan, defStyleAttr, 0); 180 mEasyCorrectUnderlineThickness = typedArray.getDimension( 181 com.android.internal.R.styleable.SuggestionSpan_textUnderlineThickness, 0); 182 mEasyCorrectUnderlineColor = typedArray.getColor( 183 com.android.internal.R.styleable.SuggestionSpan_textUnderlineColor, Color.BLACK); 184 185 defStyleAttr = com.android.internal.R.attr.textAppearanceAutoCorrectionSuggestion; 186 typedArray = context.obtainStyledAttributes( 187 null, com.android.internal.R.styleable.SuggestionSpan, defStyleAttr, 0); 188 mAutoCorrectionUnderlineThickness = typedArray.getDimension( 189 com.android.internal.R.styleable.SuggestionSpan_textUnderlineThickness, 0); 190 mAutoCorrectionUnderlineColor = typedArray.getColor( 191 com.android.internal.R.styleable.SuggestionSpan_textUnderlineColor, Color.BLACK); 192 } 193 SuggestionSpan(Parcel src)194 public SuggestionSpan(Parcel src) { 195 mSuggestions = src.readStringArray(); 196 mFlags = src.readInt(); 197 mLocaleString = src.readString(); 198 mNotificationTargetClassName = src.readString(); 199 mNotificationTargetPackageName = src.readString(); 200 mHashCode = src.readInt(); 201 mEasyCorrectUnderlineColor = src.readInt(); 202 mEasyCorrectUnderlineThickness = src.readFloat(); 203 mMisspelledUnderlineColor = src.readInt(); 204 mMisspelledUnderlineThickness = src.readFloat(); 205 mAutoCorrectionUnderlineColor = src.readInt(); 206 mAutoCorrectionUnderlineThickness = src.readFloat(); 207 } 208 209 /** 210 * @return an array of suggestion texts for this span 211 */ getSuggestions()212 public String[] getSuggestions() { 213 return mSuggestions; 214 } 215 216 /** 217 * @return the locale of the suggestions 218 */ getLocale()219 public String getLocale() { 220 return mLocaleString; 221 } 222 223 /** 224 * @return The name of the class to notify. The class of the original IME package will receive 225 * a notification when the user selects one of the suggestions. The notification will include 226 * the original string, the suggested replacement string as well as the hashCode of this span. 227 * The class will get notified by an intent that has those information. 228 * This is an internal API because only the framework should know the class name. 229 * 230 * @hide 231 */ getNotificationTargetClassName()232 public String getNotificationTargetClassName() { 233 return mNotificationTargetClassName; 234 } 235 getFlags()236 public int getFlags() { 237 return mFlags; 238 } 239 setFlags(int flags)240 public void setFlags(int flags) { 241 mFlags = flags; 242 } 243 244 @Override describeContents()245 public int describeContents() { 246 return 0; 247 } 248 249 @Override writeToParcel(Parcel dest, int flags)250 public void writeToParcel(Parcel dest, int flags) { 251 writeToParcelInternal(dest, flags); 252 } 253 254 /** @hide */ writeToParcelInternal(Parcel dest, int flags)255 public void writeToParcelInternal(Parcel dest, int flags) { 256 dest.writeStringArray(mSuggestions); 257 dest.writeInt(mFlags); 258 dest.writeString(mLocaleString); 259 dest.writeString(mNotificationTargetClassName); 260 dest.writeString(mNotificationTargetPackageName); 261 dest.writeInt(mHashCode); 262 dest.writeInt(mEasyCorrectUnderlineColor); 263 dest.writeFloat(mEasyCorrectUnderlineThickness); 264 dest.writeInt(mMisspelledUnderlineColor); 265 dest.writeFloat(mMisspelledUnderlineThickness); 266 dest.writeInt(mAutoCorrectionUnderlineColor); 267 dest.writeFloat(mAutoCorrectionUnderlineThickness); 268 } 269 270 @Override getSpanTypeId()271 public int getSpanTypeId() { 272 return getSpanTypeIdInternal(); 273 } 274 275 /** @hide */ getSpanTypeIdInternal()276 public int getSpanTypeIdInternal() { 277 return TextUtils.SUGGESTION_SPAN; 278 } 279 280 @Override equals(Object o)281 public boolean equals(Object o) { 282 if (o instanceof SuggestionSpan) { 283 return ((SuggestionSpan)o).hashCode() == mHashCode; 284 } 285 return false; 286 } 287 288 @Override hashCode()289 public int hashCode() { 290 return mHashCode; 291 } 292 hashCodeInternal(String[] suggestions, String locale, String notificationTargetClassName)293 private static int hashCodeInternal(String[] suggestions, String locale, 294 String notificationTargetClassName) { 295 return Arrays.hashCode(new Object[] {Long.valueOf(SystemClock.uptimeMillis()), suggestions, 296 locale, notificationTargetClassName}); 297 } 298 299 public static final Parcelable.Creator<SuggestionSpan> CREATOR = 300 new Parcelable.Creator<SuggestionSpan>() { 301 @Override 302 public SuggestionSpan createFromParcel(Parcel source) { 303 return new SuggestionSpan(source); 304 } 305 306 @Override 307 public SuggestionSpan[] newArray(int size) { 308 return new SuggestionSpan[size]; 309 } 310 }; 311 312 @Override updateDrawState(TextPaint tp)313 public void updateDrawState(TextPaint tp) { 314 final boolean misspelled = (mFlags & FLAG_MISSPELLED) != 0; 315 final boolean easy = (mFlags & FLAG_EASY_CORRECT) != 0; 316 final boolean autoCorrection = (mFlags & FLAG_AUTO_CORRECTION) != 0; 317 if (easy) { 318 if (!misspelled) { 319 tp.setUnderlineText(mEasyCorrectUnderlineColor, mEasyCorrectUnderlineThickness); 320 } else if (tp.underlineColor == 0) { 321 // Spans are rendered in an arbitrary order. Since misspelled is less prioritary 322 // than just easy, do not apply misspelled if an easy (or a mispelled) has been set 323 tp.setUnderlineText(mMisspelledUnderlineColor, mMisspelledUnderlineThickness); 324 } 325 } else if (autoCorrection) { 326 tp.setUnderlineText(mAutoCorrectionUnderlineColor, mAutoCorrectionUnderlineThickness); 327 } 328 } 329 330 /** 331 * @return The color of the underline for that span, or 0 if there is no underline 332 * 333 * @hide 334 */ getUnderlineColor()335 public int getUnderlineColor() { 336 // The order here should match what is used in updateDrawState 337 final boolean misspelled = (mFlags & FLAG_MISSPELLED) != 0; 338 final boolean easy = (mFlags & FLAG_EASY_CORRECT) != 0; 339 final boolean autoCorrection = (mFlags & FLAG_AUTO_CORRECTION) != 0; 340 if (easy) { 341 if (!misspelled) { 342 return mEasyCorrectUnderlineColor; 343 } else { 344 return mMisspelledUnderlineColor; 345 } 346 } else if (autoCorrection) { 347 return mAutoCorrectionUnderlineColor; 348 } 349 return 0; 350 } 351 352 /** 353 * Notifies a suggestion selection. 354 * 355 * @hide 356 */ notifySelection(Context context, String original, int index)357 public void notifySelection(Context context, String original, int index) { 358 final Intent intent = new Intent(); 359 360 if (context == null || mNotificationTargetClassName == null) { 361 return; 362 } 363 // Ensures that only a class in the original IME package will receive the 364 // notification. 365 if (mSuggestions == null || index < 0 || index >= mSuggestions.length) { 366 Log.w(TAG, "Unable to notify the suggestion as the index is out of range index=" + index 367 + " length=" + mSuggestions.length); 368 return; 369 } 370 371 // The package name is not mandatory (legacy from JB), and if the package name 372 // is missing, we try to notify the suggestion through the input method manager. 373 if (mNotificationTargetPackageName != null) { 374 intent.setClassName(mNotificationTargetPackageName, mNotificationTargetClassName); 375 intent.setAction(SuggestionSpan.ACTION_SUGGESTION_PICKED); 376 intent.putExtra(SuggestionSpan.SUGGESTION_SPAN_PICKED_BEFORE, original); 377 intent.putExtra(SuggestionSpan.SUGGESTION_SPAN_PICKED_AFTER, mSuggestions[index]); 378 intent.putExtra(SuggestionSpan.SUGGESTION_SPAN_PICKED_HASHCODE, hashCode()); 379 context.sendBroadcast(intent); 380 } else { 381 InputMethodManager imm = InputMethodManager.peekInstance(); 382 if (imm != null) { 383 imm.notifySuggestionPicked(this, original, index); 384 } 385 } 386 } 387 } 388