1 /* 2 * Copyright (C) 2018 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.settings.datetime.timezone; 18 19 import android.annotation.StringRes; 20 import android.content.res.Resources; 21 import android.icu.text.CaseMap; 22 import android.icu.text.Edits; 23 import android.text.Spannable; 24 import android.text.SpannableStringBuilder; 25 import android.util.Log; 26 27 import java.io.IOException; 28 import java.util.ArrayList; 29 import java.util.Collections; 30 import java.util.Formattable; 31 import java.util.FormattableFlags; 32 import java.util.Formatter; 33 import java.util.List; 34 import java.util.Locale; 35 36 37 public class SpannableUtil { 38 private static final String TAG = "SpannableUtil"; 39 40 private static class SpannableFormattable implements Formattable { 41 42 private final Spannable mSpannable; 43 SpannableFormattable(Spannable spannable)44 private SpannableFormattable(Spannable spannable) { 45 this.mSpannable = spannable; 46 } 47 48 @Override formatTo(Formatter formatter, int flags, int width, int precision)49 public void formatTo(Formatter formatter, int flags, int width, int precision) { 50 CharSequence s = handlePrecision(mSpannable, precision); 51 s = handleWidth(s, width, (flags & FormattableFlags.LEFT_JUSTIFY) != 0); 52 try { 53 formatter.out().append(s); 54 } catch (IOException e) { 55 // The error should never occur because formatter.out() returns 56 // SpannableStringBuilder which doesn't throw IOException. 57 Log.e(TAG, "error in SpannableFormattable", e); 58 } 59 } 60 handlePrecision(CharSequence s, int precision)61 private static CharSequence handlePrecision(CharSequence s, int precision) { 62 if (precision != -1 && precision < s.length()) { 63 return s.subSequence(0, precision); 64 } 65 return s; 66 } 67 handleWidth(CharSequence s, int width, boolean isLeftJustify)68 private static CharSequence handleWidth(CharSequence s, int width, boolean isLeftJustify) { 69 if (width == -1) { 70 return s; 71 } 72 int diff = width - s.length(); 73 if (diff <= 0) { 74 return s; 75 } 76 SpannableStringBuilder sb = new SpannableStringBuilder(); 77 if (!isLeftJustify) { 78 sb.append(" ".repeat(diff)); 79 } 80 sb.append(s); 81 if (isLeftJustify) { 82 sb.append(" ".repeat(diff)); 83 } 84 return sb; 85 } 86 } 87 88 /** 89 * {@class Resources} has no method to format string resource with {@class Spannable} a 90 * rguments. It's a helper method for this purpose. 91 */ getResourcesText(Resources res, @StringRes int resId, Object... args)92 public static Spannable getResourcesText(Resources res, @StringRes int resId, 93 Object... args) { 94 final Locale locale = res.getConfiguration().getLocales().get(0); 95 final SpannableStringBuilder builder = new SpannableStringBuilder(); 96 // Formatter converts CharSequence to String by calling toString() if an arg isn't 97 // Formattable. Wrap Spannable by SpannableFormattable to preserve Spannable objects. 98 for (int i = 0; i < args.length; i++) { 99 if (args[i] instanceof Spannable) { 100 args[i] = new SpannableFormattable((Spannable) args[i]); 101 } 102 } 103 new Formatter(builder, locale).format(res.getString(resId), args); 104 return builder; 105 } 106 107 private static final CaseMap.Title TITLE_CASE_MAP = 108 CaseMap.toTitle().sentences().noLowercase(); 109 110 /** 111 * Titlecasing {@link CharSequence} and {@link Spannable} by using {@link CaseMap.Title}. 112 */ titleCaseSentences(Locale locale, CharSequence src)113 public static CharSequence titleCaseSentences(Locale locale, CharSequence src) { 114 if (src instanceof Spannable) { 115 return applyCaseMapToSpannable(locale, TITLE_CASE_MAP, (Spannable) src); 116 } else { 117 return TITLE_CASE_MAP.apply(locale, null, src); 118 } 119 } 120 applyCaseMapToSpannable(Locale locale, CaseMap.Title caseMap, Spannable src)121 private static Spannable applyCaseMapToSpannable(Locale locale, CaseMap.Title caseMap, 122 Spannable src) { 123 Edits edits = new Edits(); 124 SpannableStringBuilder dest = new SpannableStringBuilder(); 125 caseMap.apply(locale, null, src, dest, edits); 126 if (!edits.hasChanges()) { 127 return src; 128 } 129 Edits.Iterator iterator = edits.getCoarseChangesIterator(); 130 List<int[]> changes = new ArrayList<>(); 131 while (iterator.next()) { 132 int[] change = new int[] { 133 iterator.sourceIndex(), // 0 134 iterator.oldLength(), // 1 135 iterator.destinationIndex(), // 2 136 iterator.newLength(), // 3 137 }; 138 changes.add(change); 139 } 140 // Replacement starts from the end to avoid shifting the source index during replacement 141 Collections.reverse(changes); 142 SpannableStringBuilder result = new SpannableStringBuilder(src); 143 for (int[] c : changes) { 144 result.replace(c[0], c[0] + c[1], dest, c[2], c[2] + c[3]); 145 } 146 return result; 147 } 148 } 149