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