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 */ copyNonParagraphSuggestionSpansFrom(Spanned source, int start, int end, Spannable dest, int destoff)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 */ concatWithNonParagraphSuggestionSpansOnly(CharSequence... text)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 hasUrlSpans(final CharSequence text, final int startIndex, final int endIndex)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 split(final CharSequence charSequence, final String regex, final boolean preserveTrailingEmptySegments)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