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 */ 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 */ 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 */ 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 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 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 */ 212 public String[] getSuggestions() { 213 return mSuggestions; 214 } 215 216 /** 217 * @return the locale of the suggestions 218 */ 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 */ 232 public String getNotificationTargetClassName() { 233 return mNotificationTargetClassName; 234 } 235 236 public int getFlags() { 237 return mFlags; 238 } 239 240 public void setFlags(int flags) { 241 mFlags = flags; 242 } 243 244 @Override 245 public int describeContents() { 246 return 0; 247 } 248 249 @Override 250 public void writeToParcel(Parcel dest, int flags) { 251 dest.writeStringArray(mSuggestions); 252 dest.writeInt(mFlags); 253 dest.writeString(mLocaleString); 254 dest.writeString(mNotificationTargetClassName); 255 dest.writeString(mNotificationTargetPackageName); 256 dest.writeInt(mHashCode); 257 dest.writeInt(mEasyCorrectUnderlineColor); 258 dest.writeFloat(mEasyCorrectUnderlineThickness); 259 dest.writeInt(mMisspelledUnderlineColor); 260 dest.writeFloat(mMisspelledUnderlineThickness); 261 dest.writeInt(mAutoCorrectionUnderlineColor); 262 dest.writeFloat(mAutoCorrectionUnderlineThickness); 263 } 264 265 @Override 266 public int getSpanTypeId() { 267 return TextUtils.SUGGESTION_SPAN; 268 } 269 270 @Override 271 public boolean equals(Object o) { 272 if (o instanceof SuggestionSpan) { 273 return ((SuggestionSpan)o).hashCode() == mHashCode; 274 } 275 return false; 276 } 277 278 @Override 279 public int hashCode() { 280 return mHashCode; 281 } 282 283 private static int hashCodeInternal(String[] suggestions, String locale, 284 String notificationTargetClassName) { 285 return Arrays.hashCode(new Object[] {Long.valueOf(SystemClock.uptimeMillis()), suggestions, 286 locale, notificationTargetClassName}); 287 } 288 289 public static final Parcelable.Creator<SuggestionSpan> CREATOR = 290 new Parcelable.Creator<SuggestionSpan>() { 291 @Override 292 public SuggestionSpan createFromParcel(Parcel source) { 293 return new SuggestionSpan(source); 294 } 295 296 @Override 297 public SuggestionSpan[] newArray(int size) { 298 return new SuggestionSpan[size]; 299 } 300 }; 301 302 @Override 303 public void updateDrawState(TextPaint tp) { 304 final boolean misspelled = (mFlags & FLAG_MISSPELLED) != 0; 305 final boolean easy = (mFlags & FLAG_EASY_CORRECT) != 0; 306 final boolean autoCorrection = (mFlags & FLAG_AUTO_CORRECTION) != 0; 307 if (easy) { 308 if (!misspelled) { 309 tp.setUnderlineText(mEasyCorrectUnderlineColor, mEasyCorrectUnderlineThickness); 310 } else if (tp.underlineColor == 0) { 311 // Spans are rendered in an arbitrary order. Since misspelled is less prioritary 312 // than just easy, do not apply misspelled if an easy (or a mispelled) has been set 313 tp.setUnderlineText(mMisspelledUnderlineColor, mMisspelledUnderlineThickness); 314 } 315 } else if (autoCorrection) { 316 tp.setUnderlineText(mAutoCorrectionUnderlineColor, mAutoCorrectionUnderlineThickness); 317 } 318 } 319 320 /** 321 * @return The color of the underline for that span, or 0 if there is no underline 322 * 323 * @hide 324 */ 325 public int getUnderlineColor() { 326 // The order here should match what is used in updateDrawState 327 final boolean misspelled = (mFlags & FLAG_MISSPELLED) != 0; 328 final boolean easy = (mFlags & FLAG_EASY_CORRECT) != 0; 329 final boolean autoCorrection = (mFlags & FLAG_AUTO_CORRECTION) != 0; 330 if (easy) { 331 if (!misspelled) { 332 return mEasyCorrectUnderlineColor; 333 } else { 334 return mMisspelledUnderlineColor; 335 } 336 } else if (autoCorrection) { 337 return mAutoCorrectionUnderlineColor; 338 } 339 return 0; 340 } 341 342 /** 343 * Notifies a suggestion selection. 344 * 345 * @hide 346 */ 347 public void notifySelection(Context context, String original, int index) { 348 final Intent intent = new Intent(); 349 350 if (context == null || mNotificationTargetClassName == null) { 351 return; 352 } 353 // Ensures that only a class in the original IME package will receive the 354 // notification. 355 if (mSuggestions == null || index < 0 || index >= mSuggestions.length) { 356 Log.w(TAG, "Unable to notify the suggestion as the index is out of range index=" + index 357 + " length=" + mSuggestions.length); 358 return; 359 } 360 361 // The package name is not mandatory (legacy from JB), and if the package name 362 // is missing, we try to notify the suggestion through the input method manager. 363 if (mNotificationTargetPackageName != null) { 364 intent.setClassName(mNotificationTargetPackageName, mNotificationTargetClassName); 365 intent.setAction(SuggestionSpan.ACTION_SUGGESTION_PICKED); 366 intent.putExtra(SuggestionSpan.SUGGESTION_SPAN_PICKED_BEFORE, original); 367 intent.putExtra(SuggestionSpan.SUGGESTION_SPAN_PICKED_AFTER, mSuggestions[index]); 368 intent.putExtra(SuggestionSpan.SUGGESTION_SPAN_PICKED_HASHCODE, hashCode()); 369 context.sendBroadcast(intent); 370 } else { 371 InputMethodManager imm = InputMethodManager.peekInstance(); 372 if (imm != null) { 373 imm.notifySuggestionPicked(this, original, index); 374 } 375 } 376 } 377 } 378