Home | History | Annotate | Download | only in utils
      1 /*
      2  * Copyright (C) 2013 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 com.android.inputmethod.latin.utils;
     18 
     19 import android.text.Spannable;
     20 import android.text.SpannableString;
     21 import android.text.Spanned;
     22 import android.text.SpannedString;
     23 import android.text.TextUtils;
     24 import android.text.style.SuggestionSpan;
     25 import android.text.style.URLSpan;
     26 
     27 import com.android.inputmethod.annotations.UsedForTesting;
     28 
     29 import java.util.ArrayList;
     30 import java.util.regex.Matcher;
     31 import java.util.regex.Pattern;
     32 
     33 public final class SpannableStringUtils {
     34     /**
     35      * Copies the spans from the region <code>start...end</code> in
     36      * <code>source</code> to the region
     37      * <code>destoff...destoff+end-start</code> in <code>dest</code>.
     38      * Spans in <code>source</code> that begin before <code>start</code>
     39      * or end after <code>end</code> but overlap this range are trimmed
     40      * as if they began at <code>start</code> or ended at <code>end</code>.
     41      * Only SuggestionSpans that don't have the SPAN_PARAGRAPH span are copied.
     42      *
     43      * This code is almost entirely taken from {@link TextUtils#copySpansFrom}, except for the
     44      * kind of span that is copied.
     45      *
     46      * @throws IndexOutOfBoundsException if any of the copied spans
     47      * are out of range in <code>dest</code>.
     48      */
     49     public static void copyNonParagraphSuggestionSpansFrom(Spanned source, int start, int end,
     50             Spannable dest, int destoff) {
     51         Object[] spans = source.getSpans(start, end, SuggestionSpan.class);
     52 
     53         for (int i = 0; i < spans.length; i++) {
     54             int fl = source.getSpanFlags(spans[i]);
     55             // We don't care about the PARAGRAPH flag in LatinIME code. However, if this flag
     56             // is set, Spannable#setSpan will throw an exception unless the span is on the edge
     57             // of a word. But the spans have been split into two by the getText{Before,After}Cursor
     58             // methods, so after concatenation they may end in the middle of a word.
     59             // Since we don't use them, we can just remove them and avoid crashing.
     60             fl &= ~Spanned.SPAN_PARAGRAPH;
     61 
     62             int st = source.getSpanStart(spans[i]);
     63             int en = source.getSpanEnd(spans[i]);
     64 
     65             if (st < start)
     66                 st = start;
     67             if (en > end)
     68                 en = end;
     69 
     70             dest.setSpan(spans[i], st - start + destoff, en - start + destoff,
     71                          fl);
     72         }
     73     }
     74 
     75     /**
     76      * Returns a CharSequence concatenating the specified CharSequences, retaining their
     77      * SuggestionSpans that don't have the PARAGRAPH flag, but not other spans.
     78      *
     79      * This code is almost entirely taken from {@link TextUtils#concat(CharSequence...)}, except
     80      * it calls copyNonParagraphSuggestionSpansFrom instead of {@link TextUtils#copySpansFrom}.
     81      */
     82     public static CharSequence concatWithNonParagraphSuggestionSpansOnly(CharSequence... text) {
     83         if (text.length == 0) {
     84             return "";
     85         }
     86 
     87         if (text.length == 1) {
     88             return text[0];
     89         }
     90 
     91         boolean spanned = false;
     92         for (int i = 0; i < text.length; i++) {
     93             if (text[i] instanceof Spanned) {
     94                 spanned = true;
     95                 break;
     96             }
     97         }
     98 
     99         StringBuilder sb = new StringBuilder();
    100         for (int i = 0; i < text.length; i++) {
    101             sb.append(text[i]);
    102         }
    103 
    104         if (!spanned) {
    105             return sb.toString();
    106         }
    107 
    108         SpannableString ss = new SpannableString(sb);
    109         int off = 0;
    110         for (int i = 0; i < text.length; i++) {
    111             int len = text[i].length();
    112 
    113             if (text[i] instanceof Spanned) {
    114                 copyNonParagraphSuggestionSpansFrom((Spanned) text[i], 0, len, ss, off);
    115             }
    116 
    117             off += len;
    118         }
    119 
    120         return new SpannedString(ss);
    121     }
    122 
    123     public static boolean hasUrlSpans(final CharSequence text,
    124             final int startIndex, final int endIndex) {
    125         if (!(text instanceof Spanned)) {
    126             return false; // Not spanned, so no link
    127         }
    128         final Spanned spanned = (Spanned)text;
    129         // getSpans(x, y) does not return spans that start on x or end on y. x-1, y+1 does the
    130         // trick, and works in all cases even if startIndex <= 0 or endIndex >= text.length().
    131         final URLSpan[] spans = spanned.getSpans(startIndex - 1, endIndex + 1, URLSpan.class);
    132         return null != spans && spans.length > 0;
    133     }
    134 
    135     /**
    136      * Splits the given {@code charSequence} with at occurrences of the given {@code regex}.
    137      * <p>
    138      * This is equivalent to
    139      * {@code charSequence.toString().split(regex, preserveTrailingEmptySegments ? -1 : 0)}
    140      * except that the spans are preserved in the result array.
    141      * </p>
    142      * @param charSequence the character sequence to be split.
    143      * @param regex the regex pattern to be used as the separator.
    144      * @param preserveTrailingEmptySegments {@code true} to preserve the trailing empty
    145      * segments. Otherwise, trailing empty segments will be removed before being returned.
    146      * @return the array which contains the result. All the spans in the <code>charSequence</code>
    147      * is preserved.
    148      */
    149     @UsedForTesting
    150     public static CharSequence[] split(final CharSequence charSequence, final String regex,
    151             final boolean preserveTrailingEmptySegments) {
    152         // A short-cut for non-spanned strings.
    153         if (!(charSequence instanceof Spanned)) {
    154             // -1 means that trailing empty segments will be preserved.
    155             return charSequence.toString().split(regex, preserveTrailingEmptySegments ? -1 : 0);
    156         }
    157 
    158         // Hereafter, emulate String.split for CharSequence.
    159         final ArrayList<CharSequence> sequences = new ArrayList<>();
    160         final Matcher matcher = Pattern.compile(regex).matcher(charSequence);
    161         int nextStart = 0;
    162         boolean matched = false;
    163         while (matcher.find()) {
    164             sequences.add(charSequence.subSequence(nextStart, matcher.start()));
    165             nextStart = matcher.end();
    166             matched = true;
    167         }
    168         if (!matched) {
    169             // never matched. preserveTrailingEmptySegments is ignored in this case.
    170             return new CharSequence[] { charSequence };
    171         }
    172         sequences.add(charSequence.subSequence(nextStart, charSequence.length()));
    173         if (!preserveTrailingEmptySegments) {
    174             for (int i = sequences.size() - 1; i >= 0; --i) {
    175                 if (!TextUtils.isEmpty(sequences.get(i))) {
    176                     break;
    177                 }
    178                 sequences.remove(i);
    179             }
    180         }
    181         return sequences.toArray(new CharSequence[sequences.size()]);
    182     }
    183 }
    184