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