blob: 8b4095367d7dafd6478fbc7bc915cc72a60d3611 [file] [log] [blame]
satokadb43582011-03-09 10:08:47 +09001/*
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.content.Context;
Luca Zanolin0c96b81f2012-08-29 11:33:12 +010020import android.content.Intent;
Luca Zanolin7d1c55f2011-08-16 14:59:26 +010021import android.content.res.TypedArray;
22import android.graphics.Color;
satokadb43582011-03-09 10:08:47 +090023import android.os.Parcel;
24import android.os.Parcelable;
satokf9f01002011-05-19 21:31:50 +090025import android.os.SystemClock;
satokadb43582011-03-09 10:08:47 +090026import android.text.ParcelableSpan;
Luca Zanolin7d1c55f2011-08-16 14:59:26 +010027import android.text.TextPaint;
satokadb43582011-03-09 10:08:47 +090028import android.text.TextUtils;
Gilles Debunne6e0b22b2012-01-25 14:57:40 -080029import android.util.Log;
Luca Zanolin0c96b81f2012-08-29 11:33:12 +010030import android.view.inputmethod.InputMethodManager;
Gilles Debunne4dacef22011-07-08 11:45:39 -070031import android.widget.TextView;
satokadb43582011-03-09 10:08:47 +090032
satoke3797a12011-03-22 06:34:48 +090033import java.util.Arrays;
satokadb43582011-03-09 10:08:47 +090034import java.util.Locale;
35
satoke3797a12011-03-22 06:34:48 +090036/**
Gilles Debunne4dacef22011-07-08 11:45:39 -070037 * 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 *
Luca Zanolin7d1c55f2011-08-16 14:59:26 +010043 * These spans should typically be created by the input method to provide correction and alternates
Gilles Debunne4dacef22011-07-08 11:45:39 -070044 * for the text.
45 *
Gilles Debunne6435a562011-08-04 21:22:30 -070046 * @see TextView#isSuggestionsEnabled()
satoke3797a12011-03-22 06:34:48 +090047 */
Luca Zanolin7d1c55f2011-08-16 14:59:26 +010048public class SuggestionSpan extends CharacterStyle implements ParcelableSpan {
49
Luca Zanolin0c96b81f2012-08-29 11:33:12 +010050 private static final String TAG = "SuggestionSpan";
51
satokadb43582011-03-09 10:08:47 +090052 /**
Luca Zanolin7d1c55f2011-08-16 14:59:26 +010053 * 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.
satokadb43582011-03-09 10:08:47 +090055 */
Luca Zanolin7d1c55f2011-08-16 14:59:26 +010056 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;
satokadb43582011-03-09 10:08:47 +090063
satok9ca4b432011-10-07 15:08:19 +090064 /**
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.
satok9ca4b432011-10-07 15:08:19 +090068 */
69 public static final int FLAG_AUTO_CORRECTION = 0x0004;
70
satokf9f01002011-05-19 21:31:50 +090071 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;
satokadb43582011-03-09 10:08:47 +090077
78 /*
79 * TODO: Needs to check the validity and add a feature that TextView will change
satokb3fc1a52011-04-06 18:28:55 +090080 * 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.
satokadb43582011-03-09 10:08:47 +090082 * And the current IME might want to specify any IME as the target IME including other IMEs.
83 */
84
Gilles Debunne6435a562011-08-04 21:22:30 -070085 private int mFlags;
satoke3797a12011-03-22 06:34:48 +090086 private final String[] mSuggestions;
satokadb43582011-03-09 10:08:47 +090087 private final String mLocaleString;
satok42c5a162011-05-26 16:46:14 +090088 private final String mNotificationTargetClassName;
Luca Zanolin0c96b81f2012-08-29 11:33:12 +010089 private final String mNotificationTargetPackageName;
satokf9f01002011-05-19 21:31:50 +090090 private final int mHashCode;
91
Luca Zanolinfe5e9832011-09-02 19:41:42 +010092 private float mEasyCorrectUnderlineThickness;
93 private int mEasyCorrectUnderlineColor;
94
95 private float mMisspelledUnderlineThickness;
96 private int mMisspelledUnderlineColor;
Luca Zanolin7d1c55f2011-08-16 14:59:26 +010097
satok9ca4b432011-10-07 15:08:19 +090098 private float mAutoCorrectionUnderlineThickness;
99 private int mAutoCorrectionUnderlineColor;
100
satokadb43582011-03-09 10:08:47 +0900101 /**
102 * @param context Context for the application
satoke3797a12011-03-22 06:34:48 +0900103 * @param suggestions Suggestions for the string under the span
satokadb43582011-03-09 10:08:47 +0900104 * @param flags Additional flags indicating how this span is handled in TextView
105 */
satokb3fc1a52011-04-06 18:28:55 +0900106 public SuggestionSpan(Context context, String[] suggestions, int flags) {
satoke3797a12011-03-22 06:34:48 +0900107 this(context, null, suggestions, flags, null);
satokadb43582011-03-09 10:08:47 +0900108 }
109
110 /**
111 * @param locale Locale of the suggestions
satoke3797a12011-03-22 06:34:48 +0900112 * @param suggestions Suggestions for the string under the span
satokadb43582011-03-09 10:08:47 +0900113 * @param flags Additional flags indicating how this span is handled in TextView
114 */
satokb3fc1a52011-04-06 18:28:55 +0900115 public SuggestionSpan(Locale locale, String[] suggestions, int flags) {
satoke3797a12011-03-22 06:34:48 +0900116 this(null, locale, suggestions, flags, null);
satokadb43582011-03-09 10:08:47 +0900117 }
118
119 /**
120 * @param context Context for the application
121 * @param locale locale Locale of the suggestions
Gilles Debunnec9fd9782011-09-09 09:20:12 -0700122 * @param suggestions Suggestions for the string under the span. Only the first up to
Gilles Debunne6e0b22b2012-01-25 14:57:40 -0800123 * {@link SuggestionSpan#SUGGESTIONS_MAX_SIZE} will be considered. Null values not permitted.
satokadb43582011-03-09 10:08:47 +0900124 * @param flags Additional flags indicating how this span is handled in TextView
satokf9f01002011-05-19 21:31:50 +0900125 * @param notificationTargetClass if not null, this class will get notified when the user
126 * selects one of the suggestions.
satokadb43582011-03-09 10:08:47 +0900127 */
satokb3fc1a52011-04-06 18:28:55 +0900128 public SuggestionSpan(Context context, Locale locale, String[] suggestions, int flags,
satokf9f01002011-05-19 21:31:50 +0900129 Class<?> notificationTargetClass) {
satoke3797a12011-03-22 06:34:48 +0900130 final int N = Math.min(SUGGESTIONS_MAX_SIZE, suggestions.length);
131 mSuggestions = Arrays.copyOf(suggestions, N);
satokadb43582011-03-09 10:08:47 +0900132 mFlags = flags;
Gilles Debunne6e0b22b2012-01-25 14:57:40 -0800133 if (locale != null) {
134 mLocaleString = locale.toString();
135 } else if (context != null) {
satokadb43582011-03-09 10:08:47 +0900136 mLocaleString = context.getResources().getConfiguration().locale.toString();
137 } else {
Gilles Debunne6e0b22b2012-01-25 14:57:40 -0800138 Log.e("SuggestionSpan", "No locale or context specified in SuggestionSpan constructor");
139 mLocaleString = "";
satokadb43582011-03-09 10:08:47 +0900140 }
Luca Zanolin7d1c55f2011-08-16 14:59:26 +0100141
Luca Zanolin0c96b81f2012-08-29 11:33:12 +0100142 if (context != null) {
143 mNotificationTargetPackageName = context.getPackageName();
144 } else {
145 mNotificationTargetPackageName = null;
146 }
147
satok42c5a162011-05-26 16:46:14 +0900148 if (notificationTargetClass != null) {
149 mNotificationTargetClassName = notificationTargetClass.getCanonicalName();
150 } else {
151 mNotificationTargetClassName = "";
152 }
Gilles Debunne6435a562011-08-04 21:22:30 -0700153 mHashCode = hashCodeInternal(mSuggestions, mLocaleString, mNotificationTargetClassName);
Luca Zanolin7d1c55f2011-08-16 14:59:26 +0100154
155 initStyle(context);
156 }
157
158 private void initStyle(Context context) {
Gilles Debunnec5436f22011-12-06 10:27:00 -0800159 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
Alan Viverette617feb92013-09-09 18:09:13 -0700169 int defStyleAttr = com.android.internal.R.attr.textAppearanceMisspelledSuggestion;
Luca Zanolinfe5e9832011-09-02 19:41:42 +0100170 TypedArray typedArray = context.obtainStyledAttributes(
Alan Viverette617feb92013-09-09 18:09:13 -0700171 null, com.android.internal.R.styleable.SuggestionSpan, defStyleAttr, 0);
Luca Zanolinfe5e9832011-09-02 19:41:42 +0100172 mMisspelledUnderlineThickness = typedArray.getDimension(
Luca Zanoline6d36822011-08-30 18:04:34 +0100173 com.android.internal.R.styleable.SuggestionSpan_textUnderlineThickness, 0);
Luca Zanolinfe5e9832011-09-02 19:41:42 +0100174 mMisspelledUnderlineColor = typedArray.getColor(
175 com.android.internal.R.styleable.SuggestionSpan_textUnderlineColor, Color.BLACK);
176
Alan Viverette617feb92013-09-09 18:09:13 -0700177 defStyleAttr = com.android.internal.R.attr.textAppearanceEasyCorrectSuggestion;
Luca Zanolinfe5e9832011-09-02 19:41:42 +0100178 typedArray = context.obtainStyledAttributes(
Alan Viverette617feb92013-09-09 18:09:13 -0700179 null, com.android.internal.R.styleable.SuggestionSpan, defStyleAttr, 0);
Luca Zanolinfe5e9832011-09-02 19:41:42 +0100180 mEasyCorrectUnderlineThickness = typedArray.getDimension(
181 com.android.internal.R.styleable.SuggestionSpan_textUnderlineThickness, 0);
182 mEasyCorrectUnderlineColor = typedArray.getColor(
Luca Zanolin7d1c55f2011-08-16 14:59:26 +0100183 com.android.internal.R.styleable.SuggestionSpan_textUnderlineColor, Color.BLACK);
satok9ca4b432011-10-07 15:08:19 +0900184
Alan Viverette617feb92013-09-09 18:09:13 -0700185 defStyleAttr = com.android.internal.R.attr.textAppearanceAutoCorrectionSuggestion;
satok9ca4b432011-10-07 15:08:19 +0900186 typedArray = context.obtainStyledAttributes(
Alan Viverette617feb92013-09-09 18:09:13 -0700187 null, com.android.internal.R.styleable.SuggestionSpan, defStyleAttr, 0);
satok9ca4b432011-10-07 15:08:19 +0900188 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);
satokadb43582011-03-09 10:08:47 +0900192 }
193
satokb3fc1a52011-04-06 18:28:55 +0900194 public SuggestionSpan(Parcel src) {
satoke3797a12011-03-22 06:34:48 +0900195 mSuggestions = src.readStringArray();
satokadb43582011-03-09 10:08:47 +0900196 mFlags = src.readInt();
197 mLocaleString = src.readString();
satok42c5a162011-05-26 16:46:14 +0900198 mNotificationTargetClassName = src.readString();
Luca Zanolin0c96b81f2012-08-29 11:33:12 +0100199 mNotificationTargetPackageName = src.readString();
satokf9f01002011-05-19 21:31:50 +0900200 mHashCode = src.readInt();
Luca Zanolinfe5e9832011-09-02 19:41:42 +0100201 mEasyCorrectUnderlineColor = src.readInt();
202 mEasyCorrectUnderlineThickness = src.readFloat();
203 mMisspelledUnderlineColor = src.readInt();
204 mMisspelledUnderlineThickness = src.readFloat();
satok9ca4b432011-10-07 15:08:19 +0900205 mAutoCorrectionUnderlineColor = src.readInt();
206 mAutoCorrectionUnderlineThickness = src.readFloat();
satokadb43582011-03-09 10:08:47 +0900207 }
208
209 /**
Gilles Debunne4dacef22011-07-08 11:45:39 -0700210 * @return an array of suggestion texts for this span
satokadb43582011-03-09 10:08:47 +0900211 */
satoke3797a12011-03-22 06:34:48 +0900212 public String[] getSuggestions() {
Gilles Debunneee511cc2011-05-05 14:57:50 -0700213 return mSuggestions;
satokadb43582011-03-09 10:08:47 +0900214 }
215
216 /**
Gilles Debunne4dacef22011-07-08 11:45:39 -0700217 * @return the locale of the suggestions
satokadb43582011-03-09 10:08:47 +0900218 */
219 public String getLocale() {
220 return mLocaleString;
221 }
222
223 /**
satok42c5a162011-05-26 16:46:14 +0900224 * @return The name of the class to notify. The class of the original IME package will receive
satokf9f01002011-05-19 21:31:50 +0900225 * 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.
satok42c5a162011-05-26 16:46:14 +0900228 * This is an internal API because only the framework should know the class name.
229 *
230 * @hide
satokadb43582011-03-09 10:08:47 +0900231 */
satok42c5a162011-05-26 16:46:14 +0900232 public String getNotificationTargetClassName() {
233 return mNotificationTargetClassName;
satokadb43582011-03-09 10:08:47 +0900234 }
235
236 public int getFlags() {
237 return mFlags;
238 }
239
Gilles Debunne6435a562011-08-04 21:22:30 -0700240 public void setFlags(int flags) {
241 mFlags = flags;
242 }
243
satokadb43582011-03-09 10:08:47 +0900244 @Override
245 public int describeContents() {
246 return 0;
247 }
248
249 @Override
250 public void writeToParcel(Parcel dest, int flags) {
satoke3797a12011-03-22 06:34:48 +0900251 dest.writeStringArray(mSuggestions);
satokadb43582011-03-09 10:08:47 +0900252 dest.writeInt(mFlags);
253 dest.writeString(mLocaleString);
satok42c5a162011-05-26 16:46:14 +0900254 dest.writeString(mNotificationTargetClassName);
Luca Zanolin0c96b81f2012-08-29 11:33:12 +0100255 dest.writeString(mNotificationTargetPackageName);
satokf9f01002011-05-19 21:31:50 +0900256 dest.writeInt(mHashCode);
Luca Zanolinfe5e9832011-09-02 19:41:42 +0100257 dest.writeInt(mEasyCorrectUnderlineColor);
258 dest.writeFloat(mEasyCorrectUnderlineThickness);
259 dest.writeInt(mMisspelledUnderlineColor);
260 dest.writeFloat(mMisspelledUnderlineThickness);
satok9ca4b432011-10-07 15:08:19 +0900261 dest.writeInt(mAutoCorrectionUnderlineColor);
262 dest.writeFloat(mAutoCorrectionUnderlineThickness);
satokadb43582011-03-09 10:08:47 +0900263 }
264
265 @Override
266 public int getSpanTypeId() {
Gilles Debunnea00972a2011-04-13 16:07:31 -0700267 return TextUtils.SUGGESTION_SPAN;
satokadb43582011-03-09 10:08:47 +0900268 }
269
satokf9f01002011-05-19 21:31:50 +0900270 @Override
satok42c5a162011-05-26 16:46:14 +0900271 public boolean equals(Object o) {
272 if (o instanceof SuggestionSpan) {
273 return ((SuggestionSpan)o).hashCode() == mHashCode;
274 }
275 return false;
276 }
277
278 @Override
satokf9f01002011-05-19 21:31:50 +0900279 public int hashCode() {
280 return mHashCode;
281 }
282
Gilles Debunne6435a562011-08-04 21:22:30 -0700283 private static int hashCodeInternal(String[] suggestions, String locale,
satok42c5a162011-05-26 16:46:14 +0900284 String notificationTargetClassName) {
Gilles Debunne6435a562011-08-04 21:22:30 -0700285 return Arrays.hashCode(new Object[] {Long.valueOf(SystemClock.uptimeMillis()), suggestions,
286 locale, notificationTargetClassName});
satokf9f01002011-05-19 21:31:50 +0900287 }
288
satokb3fc1a52011-04-06 18:28:55 +0900289 public static final Parcelable.Creator<SuggestionSpan> CREATOR =
290 new Parcelable.Creator<SuggestionSpan>() {
satokadb43582011-03-09 10:08:47 +0900291 @Override
satokb3fc1a52011-04-06 18:28:55 +0900292 public SuggestionSpan createFromParcel(Parcel source) {
293 return new SuggestionSpan(source);
satokadb43582011-03-09 10:08:47 +0900294 }
295
296 @Override
satokb3fc1a52011-04-06 18:28:55 +0900297 public SuggestionSpan[] newArray(int size) {
298 return new SuggestionSpan[size];
satokadb43582011-03-09 10:08:47 +0900299 }
300 };
Luca Zanolin7d1c55f2011-08-16 14:59:26 +0100301
302 @Override
303 public void updateDrawState(TextPaint tp) {
Gilles Debunnec9fd9782011-09-09 09:20:12 -0700304 final boolean misspelled = (mFlags & FLAG_MISSPELLED) != 0;
305 final boolean easy = (mFlags & FLAG_EASY_CORRECT) != 0;
satok9ca4b432011-10-07 15:08:19 +0900306 final boolean autoCorrection = (mFlags & FLAG_AUTO_CORRECTION) != 0;
Gilles Debunnec9fd9782011-09-09 09:20:12 -0700307 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 }
satok9ca4b432011-10-07 15:08:19 +0900315 } else if (autoCorrection) {
316 tp.setUnderlineText(mAutoCorrectionUnderlineColor, mAutoCorrectionUnderlineThickness);
Luca Zanolinfe5e9832011-09-02 19:41:42 +0100317 }
Luca Zanolin7d1c55f2011-08-16 14:59:26 +0100318 }
Gilles Debunnefa4e2d92011-09-08 18:34:22 -0700319
320 /**
321 * @return The color of the underline for that span, or 0 if there is no underline
Gilles Debunne51322732011-09-09 16:46:42 -0700322 *
323 * @hide
Gilles Debunnefa4e2d92011-09-08 18:34:22 -0700324 */
325 public int getUnderlineColor() {
326 // The order here should match what is used in updateDrawState
Gilles Debunnec9fd9782011-09-09 09:20:12 -0700327 final boolean misspelled = (mFlags & FLAG_MISSPELLED) != 0;
328 final boolean easy = (mFlags & FLAG_EASY_CORRECT) != 0;
satok9ca4b432011-10-07 15:08:19 +0900329 final boolean autoCorrection = (mFlags & FLAG_AUTO_CORRECTION) != 0;
Gilles Debunnec9fd9782011-09-09 09:20:12 -0700330 if (easy) {
331 if (!misspelled) {
332 return mEasyCorrectUnderlineColor;
333 } else {
334 return mMisspelledUnderlineColor;
335 }
satok9ca4b432011-10-07 15:08:19 +0900336 } else if (autoCorrection) {
337 return mAutoCorrectionUnderlineColor;
Gilles Debunnec9fd9782011-09-09 09:20:12 -0700338 }
Gilles Debunnefa4e2d92011-09-08 18:34:22 -0700339 return 0;
340 }
Luca Zanolin0c96b81f2012-08-29 11:33:12 +0100341
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 }
satokadb43582011-03-09 10:08:47 +0900377}