blob: 1b00db2e4ad36855b3f95a9027da069640be213a [file] [log] [blame]
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.text.style;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.content.Intent;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.SystemClock;
import android.text.ParcelableSpan;
import android.text.TextPaint;
import android.text.TextUtils;
import android.util.Log;
import android.view.inputmethod.InputMethodManager;
import android.widget.TextView;
import java.util.Arrays;
import java.util.Locale;
/**
* Holds suggestion candidates for the text enclosed in this span.
*
* When such a span is edited in an EditText, double tapping on the text enclosed in this span will
* display a popup dialog listing suggestion replacement for that text. The user can then replace
* the original text by one of the suggestions.
*
* These spans should typically be created by the input method to provide correction and alternates
* for the text.
*
* @see TextView#isSuggestionsEnabled()
*/
public class SuggestionSpan extends CharacterStyle implements ParcelableSpan {
private static final String TAG = "SuggestionSpan";
/**
* Sets this flag if the suggestions should be easily accessible with few interactions.
* This flag should be set for every suggestions that the user is likely to use.
*/
public static final int FLAG_EASY_CORRECT = 0x0001;
/**
* Sets this flag if the suggestions apply to a misspelled word/text. This type of suggestion is
* rendered differently to highlight the error.
*/
public static final int FLAG_MISSPELLED = 0x0002;
/**
* Sets this flag if the auto correction is about to be applied to a word/text
* that the user is typing/composing. This type of suggestion is rendered differently
* to indicate the auto correction is happening.
*/
public static final int FLAG_AUTO_CORRECTION = 0x0004;
public static final String ACTION_SUGGESTION_PICKED = "android.text.style.SUGGESTION_PICKED";
public static final String SUGGESTION_SPAN_PICKED_AFTER = "after";
public static final String SUGGESTION_SPAN_PICKED_BEFORE = "before";
public static final String SUGGESTION_SPAN_PICKED_HASHCODE = "hashcode";
public static final int SUGGESTIONS_MAX_SIZE = 5;
/*
* TODO: Needs to check the validity and add a feature that TextView will change
* the current IME to the other IME which is specified in SuggestionSpan.
* An IME needs to set the span by specifying the target IME and Subtype of SuggestionSpan.
* And the current IME might want to specify any IME as the target IME including other IMEs.
*/
private int mFlags;
private final String[] mSuggestions;
/**
* Kept for compatibility for apps that rely on invalid locale strings e.g.
* {@code new Locale(" an ", " i n v a l i d ", "data")}, which cannot be handled by
* {@link #mLanguageTag}.
*/
@NonNull
private final String mLocaleStringForCompatibility;
@NonNull
private final String mLanguageTag;
private final String mNotificationTargetClassName;
private final String mNotificationTargetPackageName;
private final int mHashCode;
private float mEasyCorrectUnderlineThickness;
private int mEasyCorrectUnderlineColor;
private float mMisspelledUnderlineThickness;
private int mMisspelledUnderlineColor;
private float mAutoCorrectionUnderlineThickness;
private int mAutoCorrectionUnderlineColor;
/**
* @param context Context for the application
* @param suggestions Suggestions for the string under the span
* @param flags Additional flags indicating how this span is handled in TextView
*/
public SuggestionSpan(Context context, String[] suggestions, int flags) {
this(context, null, suggestions, flags, null);
}
/**
* @param locale Locale of the suggestions
* @param suggestions Suggestions for the string under the span
* @param flags Additional flags indicating how this span is handled in TextView
*/
public SuggestionSpan(Locale locale, String[] suggestions, int flags) {
this(null, locale, suggestions, flags, null);
}
/**
* @param context Context for the application
* @param locale locale Locale of the suggestions
* @param suggestions Suggestions for the string under the span. Only the first up to
* {@link SuggestionSpan#SUGGESTIONS_MAX_SIZE} will be considered. Null values not permitted.
* @param flags Additional flags indicating how this span is handled in TextView
* @param notificationTargetClass if not null, this class will get notified when the user
* selects one of the suggestions.
*/
public SuggestionSpan(Context context, Locale locale, String[] suggestions, int flags,
Class<?> notificationTargetClass) {
final int N = Math.min(SUGGESTIONS_MAX_SIZE, suggestions.length);
mSuggestions = Arrays.copyOf(suggestions, N);
mFlags = flags;
final Locale sourceLocale;
if (locale != null) {
sourceLocale = locale;
} else if (context != null) {
// TODO: Consider to context.getResources().getResolvedLocale() instead.
sourceLocale = context.getResources().getConfiguration().locale;
} else {
Log.e("SuggestionSpan", "No locale or context specified in SuggestionSpan constructor");
sourceLocale = null;
}
mLocaleStringForCompatibility = sourceLocale == null ? "" : sourceLocale.toString();
mLanguageTag = sourceLocale == null ? "" : sourceLocale.toLanguageTag();
if (context != null) {
mNotificationTargetPackageName = context.getPackageName();
} else {
mNotificationTargetPackageName = null;
}
if (notificationTargetClass != null) {
mNotificationTargetClassName = notificationTargetClass.getCanonicalName();
} else {
mNotificationTargetClassName = "";
}
mHashCode = hashCodeInternal(mSuggestions, mLanguageTag, mLocaleStringForCompatibility,
mNotificationTargetClassName);
initStyle(context);
}
private void initStyle(Context context) {
if (context == null) {
mMisspelledUnderlineThickness = 0;
mEasyCorrectUnderlineThickness = 0;
mAutoCorrectionUnderlineThickness = 0;
mMisspelledUnderlineColor = Color.BLACK;
mEasyCorrectUnderlineColor = Color.BLACK;
mAutoCorrectionUnderlineColor = Color.BLACK;
return;
}
int defStyleAttr = com.android.internal.R.attr.textAppearanceMisspelledSuggestion;
TypedArray typedArray = context.obtainStyledAttributes(
null, com.android.internal.R.styleable.SuggestionSpan, defStyleAttr, 0);
mMisspelledUnderlineThickness = typedArray.getDimension(
com.android.internal.R.styleable.SuggestionSpan_textUnderlineThickness, 0);
mMisspelledUnderlineColor = typedArray.getColor(
com.android.internal.R.styleable.SuggestionSpan_textUnderlineColor, Color.BLACK);
defStyleAttr = com.android.internal.R.attr.textAppearanceEasyCorrectSuggestion;
typedArray = context.obtainStyledAttributes(
null, com.android.internal.R.styleable.SuggestionSpan, defStyleAttr, 0);
mEasyCorrectUnderlineThickness = typedArray.getDimension(
com.android.internal.R.styleable.SuggestionSpan_textUnderlineThickness, 0);
mEasyCorrectUnderlineColor = typedArray.getColor(
com.android.internal.R.styleable.SuggestionSpan_textUnderlineColor, Color.BLACK);
defStyleAttr = com.android.internal.R.attr.textAppearanceAutoCorrectionSuggestion;
typedArray = context.obtainStyledAttributes(
null, com.android.internal.R.styleable.SuggestionSpan, defStyleAttr, 0);
mAutoCorrectionUnderlineThickness = typedArray.getDimension(
com.android.internal.R.styleable.SuggestionSpan_textUnderlineThickness, 0);
mAutoCorrectionUnderlineColor = typedArray.getColor(
com.android.internal.R.styleable.SuggestionSpan_textUnderlineColor, Color.BLACK);
}
public SuggestionSpan(Parcel src) {
mSuggestions = src.readStringArray();
mFlags = src.readInt();
mLocaleStringForCompatibility = src.readString();
mLanguageTag = src.readString();
mNotificationTargetClassName = src.readString();
mNotificationTargetPackageName = src.readString();
mHashCode = src.readInt();
mEasyCorrectUnderlineColor = src.readInt();
mEasyCorrectUnderlineThickness = src.readFloat();
mMisspelledUnderlineColor = src.readInt();
mMisspelledUnderlineThickness = src.readFloat();
mAutoCorrectionUnderlineColor = src.readInt();
mAutoCorrectionUnderlineThickness = src.readFloat();
}
/**
* @return an array of suggestion texts for this span
*/
public String[] getSuggestions() {
return mSuggestions;
}
/**
* @deprecated use {@link #getLocaleObject()} instead.
* @return the locale of the suggestions. An empty string is returned if no locale is specified.
*/
@NonNull
@Deprecated
public String getLocale() {
return mLocaleStringForCompatibility;
}
/**
* Returns a well-formed BCP 47 language tag representation of the suggestions, as a
* {@link Locale} object.
*
* <p><b>Caveat</b>: The returned object is guaranteed to be a a well-formed BCP 47 language tag
* representation. For example, this method can return an empty locale rather than returning a
* malformed data when this object is initialized with an malformed {@link Locale} object, e.g.
* {@code new Locale(" a ", " b c d ", " "}.</p>
*
* @return the locale of the suggestions. {@code null} is returned if no locale is specified.
*/
@Nullable
public Locale getLocaleObject() {
return mLanguageTag.isEmpty() ? null : Locale.forLanguageTag(mLanguageTag);
}
/**
* @return The name of the class to notify. The class of the original IME package will receive
* a notification when the user selects one of the suggestions. The notification will include
* the original string, the suggested replacement string as well as the hashCode of this span.
* The class will get notified by an intent that has those information.
* This is an internal API because only the framework should know the class name.
*
* @hide
*/
public String getNotificationTargetClassName() {
return mNotificationTargetClassName;
}
public int getFlags() {
return mFlags;
}
public void setFlags(int flags) {
mFlags = flags;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
writeToParcelInternal(dest, flags);
}
/** @hide */
public void writeToParcelInternal(Parcel dest, int flags) {
dest.writeStringArray(mSuggestions);
dest.writeInt(mFlags);
dest.writeString(mLocaleStringForCompatibility);
dest.writeString(mLanguageTag);
dest.writeString(mNotificationTargetClassName);
dest.writeString(mNotificationTargetPackageName);
dest.writeInt(mHashCode);
dest.writeInt(mEasyCorrectUnderlineColor);
dest.writeFloat(mEasyCorrectUnderlineThickness);
dest.writeInt(mMisspelledUnderlineColor);
dest.writeFloat(mMisspelledUnderlineThickness);
dest.writeInt(mAutoCorrectionUnderlineColor);
dest.writeFloat(mAutoCorrectionUnderlineThickness);
}
@Override
public int getSpanTypeId() {
return getSpanTypeIdInternal();
}
/** @hide */
public int getSpanTypeIdInternal() {
return TextUtils.SUGGESTION_SPAN;
}
@Override
public boolean equals(Object o) {
if (o instanceof SuggestionSpan) {
return ((SuggestionSpan)o).hashCode() == mHashCode;
}
return false;
}
@Override
public int hashCode() {
return mHashCode;
}
private static int hashCodeInternal(String[] suggestions, @NonNull String languageTag,
@NonNull String localeStringForCompatibility, String notificationTargetClassName) {
return Arrays.hashCode(new Object[] {Long.valueOf(SystemClock.uptimeMillis()), suggestions,
languageTag, localeStringForCompatibility, notificationTargetClassName});
}
public static final Parcelable.Creator<SuggestionSpan> CREATOR =
new Parcelable.Creator<SuggestionSpan>() {
@Override
public SuggestionSpan createFromParcel(Parcel source) {
return new SuggestionSpan(source);
}
@Override
public SuggestionSpan[] newArray(int size) {
return new SuggestionSpan[size];
}
};
@Override
public void updateDrawState(TextPaint tp) {
final boolean misspelled = (mFlags & FLAG_MISSPELLED) != 0;
final boolean easy = (mFlags & FLAG_EASY_CORRECT) != 0;
final boolean autoCorrection = (mFlags & FLAG_AUTO_CORRECTION) != 0;
if (easy) {
if (!misspelled) {
tp.setUnderlineText(mEasyCorrectUnderlineColor, mEasyCorrectUnderlineThickness);
} else if (tp.underlineColor == 0) {
// Spans are rendered in an arbitrary order. Since misspelled is less prioritary
// than just easy, do not apply misspelled if an easy (or a mispelled) has been set
tp.setUnderlineText(mMisspelledUnderlineColor, mMisspelledUnderlineThickness);
}
} else if (autoCorrection) {
tp.setUnderlineText(mAutoCorrectionUnderlineColor, mAutoCorrectionUnderlineThickness);
}
}
/**
* @return The color of the underline for that span, or 0 if there is no underline
*
* @hide
*/
public int getUnderlineColor() {
// The order here should match what is used in updateDrawState
final boolean misspelled = (mFlags & FLAG_MISSPELLED) != 0;
final boolean easy = (mFlags & FLAG_EASY_CORRECT) != 0;
final boolean autoCorrection = (mFlags & FLAG_AUTO_CORRECTION) != 0;
if (easy) {
if (!misspelled) {
return mEasyCorrectUnderlineColor;
} else {
return mMisspelledUnderlineColor;
}
} else if (autoCorrection) {
return mAutoCorrectionUnderlineColor;
}
return 0;
}
/**
* Notifies a suggestion selection.
*
* @hide
*/
public void notifySelection(Context context, String original, int index) {
final Intent intent = new Intent();
if (context == null || mNotificationTargetClassName == null) {
return;
}
// Ensures that only a class in the original IME package will receive the
// notification.
if (mSuggestions == null || index < 0 || index >= mSuggestions.length) {
Log.w(TAG, "Unable to notify the suggestion as the index is out of range index=" + index
+ " length=" + mSuggestions.length);
return;
}
// The package name is not mandatory (legacy from JB), and if the package name
// is missing, we try to notify the suggestion through the input method manager.
if (mNotificationTargetPackageName != null) {
intent.setClassName(mNotificationTargetPackageName, mNotificationTargetClassName);
intent.setAction(SuggestionSpan.ACTION_SUGGESTION_PICKED);
intent.putExtra(SuggestionSpan.SUGGESTION_SPAN_PICKED_BEFORE, original);
intent.putExtra(SuggestionSpan.SUGGESTION_SPAN_PICKED_AFTER, mSuggestions[index]);
intent.putExtra(SuggestionSpan.SUGGESTION_SPAN_PICKED_HASHCODE, hashCode());
context.sendBroadcast(intent);
} else {
InputMethodManager imm = InputMethodManager.peekInstance();
if (imm != null) {
imm.notifySuggestionPicked(this, original, index);
}
}
}
}