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