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