/* * Copyright (C) 2009 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 com.android.utils; import android.text.Spannable; import android.text.SpannableString; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.TextUtils; import android.util.Log; import java.util.List; public class StringBuilderUtils { /** * Breaking separator inserted between text, intended to make TTS pause an * appropriate amount. Using a period breaks pronunciation of street * abbreviations, and using a new line doesn't work in eSpeak. */ public static final String DEFAULT_BREAKING_SEPARATOR = ", "; /** * Non-breaking separator inserted between text. Used when text already ends * with some sort of breaking separator or non-alphanumeric character. */ public static final String DEFAULT_SEPARATOR = " "; /** * The hex alphabet. */ private static final char[] HEX_ALPHABET = "0123456789abcdef".toCharArray(); /** * Generates the aggregate text from a list of {@link CharSequence}s, * separating as necessary. * * @param textList The list of text to process. * @return The separated aggregate text, or null if no text was appended. */ public static CharSequence getAggregateText(List<CharSequence> textList) { final CharSequence aggregateText; if (textList == null || textList.isEmpty()) { aggregateText = null; } else { final SpannableStringBuilder builder = new SpannableStringBuilder(); for (CharSequence text : textList) { appendWithSeparator(builder, text); } aggregateText = builder; } return aggregateText; } /** * Appends CharSequence representations of the specified arguments to a * {@link SpannableStringBuilder}, creating one if the supplied builder is * {@code null}. A separator will be inserted between each of the arguments. * * @param builder An existing {@link SpannableStringBuilder}, or {@code null} to create one. * @param args The objects to append to the builder. * @return A builder with the specified objects appended. */ public static SpannableStringBuilder appendWithSeparator( SpannableStringBuilder builder, CharSequence... args) { if (builder == null) { builder = new SpannableStringBuilder(); } for (CharSequence arg : args) { if (arg == null) { continue; } if (arg.toString().length() == 0) { continue; } if (builder.length() > 0) { if (needsBreakingSeparator(builder)) { builder.append(DEFAULT_BREAKING_SEPARATOR); } else { builder.append(DEFAULT_SEPARATOR); } } builder.append(arg); } return builder; } /** * Appends CharSequence representations of the specified arguments to a * {@link SpannableStringBuilder}, creating one if the supplied builder is * {@code null}. A separator will be inserted before the first non-{@code null} argument, * but additional separators will not be inserted between the following elements. * * @param builder An existing {@link SpannableStringBuilder}, or {@code null} to create one. * @param args The objects to append to the builder. * @return A builder with the specified objects appended. */ public static SpannableStringBuilder append( SpannableStringBuilder builder, CharSequence... args) { if (builder == null) { builder = new SpannableStringBuilder(); } boolean didAppend = false; for (CharSequence arg : args) { if (arg == null) { continue; } if (arg.toString().length() == 0) { continue; } if (builder.length() > 0) { if (!didAppend && needsBreakingSeparator(builder)) { builder.append(DEFAULT_BREAKING_SEPARATOR); } else { builder.append(DEFAULT_SEPARATOR); } } builder.append(arg); didAppend = true; } return builder; } /** * Create spannable from text that includes some CharSequence. If the CharSequence has any spans * they would be copied to result spannable * @param text - some text that potentially contains CharSequence template * @param innerTemplate - CharSequence that is supposed but not necessary to be Spanned. * If it is Spanned the spans are copied to result Spannable * @return Spannable object that contains incoming text and spans from innerTemplate */ public static Spannable createSpannableFromTextWithTemplate(String text, CharSequence innerTemplate) { SpannableString result = new SpannableString(text); if(innerTemplate instanceof Spanned) { int index = text.indexOf(innerTemplate.toString()); if(index >= 0) { copySpans(result, (Spanned) innerTemplate, index); } } return result; } /** * Utility that copies spans from fromSpan to toSpan * @param toSpan - Spannable that is supposed to contain fromSpan. * @param fromSpan - Spannable that could contain spans that would be copied to toSpan * @param startIndex - Starting index of occurrence fromSpan in toSpan */ private static void copySpans(Spannable toSpan, Spanned fromSpan, int startIndex) { if (startIndex < 0 || startIndex >= toSpan.length()) { LogUtils.log(StringBuilderUtils.class, Log.ERROR, "startIndex parameter (" + startIndex + ") is out of toSpan length " + toSpan.length()); return; } Object[] spans = fromSpan.getSpans(0, fromSpan.length(), Object.class); if (spans != null && spans.length > 0) { for (Object span : spans) { int spanStartIndex = fromSpan.getSpanStart(span); int spanEndIndex = fromSpan.getSpanEnd(span); if(spanStartIndex >= spanEndIndex) { continue; } int spanFlags = fromSpan.getSpanFlags(span); toSpan.setSpan(span, startIndex + spanStartIndex, startIndex + spanEndIndex, spanFlags); } } } /** * Returns whether the text needs a breaking separator (e.g. a period * followed by a space) appended before more text is appended. * <p> * If text ends with a letter or digit (according to the current locale) * then this method will return {@code true}. */ private static boolean needsBreakingSeparator(CharSequence text) { return !TextUtils.isEmpty(text) && Character.isLetterOrDigit(text.charAt(text.length() - 1)); } /** * Convert a byte array to a hex-encoded string. * * @param bytes The byte array of data to convert * @return The hex encoding of {@code bytes}, or null if {@code bytes} was null */ public static String bytesToHexString(byte[] bytes) { if (bytes == null) { return null; } final StringBuilder hex = new StringBuilder(bytes.length * 2); int nibble1, nibble2; for (byte b : bytes) { nibble1 = (b >>> 4) & 0xf; nibble2 = b & 0xf; hex.append(HEX_ALPHABET[nibble1]); hex.append(HEX_ALPHABET[nibble2]); } return hex.toString(); } }