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