1/*
2 * Copyright (C) 2016 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
17package com.android.deskclock.uidata;
18
19import android.content.BroadcastReceiver;
20import android.content.Context;
21import android.content.Intent;
22import android.content.IntentFilter;
23import android.util.ArrayMap;
24import android.util.SparseArray;
25
26import java.text.SimpleDateFormat;
27import java.util.Calendar;
28import java.util.GregorianCalendar;
29import java.util.Locale;
30import java.util.Map;
31
32import static java.util.Calendar.JULY;
33
34/**
35 * All formatted strings that are cached for performance are accessed via this model.
36 */
37final class FormattedStringModel {
38
39    /** Clears data structures containing data that is locale-sensitive. */
40    @SuppressWarnings("FieldCanBeLocal")
41    private final BroadcastReceiver mLocaleChangedReceiver = new LocaleChangedReceiver();
42
43    /**
44     * Caches formatted numbers in the current locale padded with zeroes to requested lengths.
45     * The first level of the cache maps length to the second level of the cache.
46     * The second level of the cache maps an integer to a formatted String in the current locale.
47     */
48    private final SparseArray<SparseArray<String>> mNumberFormatCache = new SparseArray<>(3);
49
50    /** Single-character version of weekday names; e.g.: 'S', 'M', 'T', 'W', 'T', 'F', 'S' */
51    private Map<Integer, String> mShortWeekdayNames;
52
53    /** Full weekday names; e.g.: 'Sunday', 'Monday', 'Tuesday', etc. */
54    private Map<Integer, String> mLongWeekdayNames;
55
56    FormattedStringModel(Context context) {
57        // Clear caches affected by locale when locale changes.
58        final IntentFilter localeBroadcastFilter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED);
59        context.registerReceiver(mLocaleChangedReceiver, localeBroadcastFilter);
60    }
61
62    /**
63     * This method is intended to be used when formatting numbers occurs in a hotspot such as the
64     * update loop of a timer or stopwatch. It returns cached results when possible in order to
65     * provide speed and limit garbage to be collected by the virtual machine.
66     *
67     * @param value a positive integer to format as a String
68     * @return the {@code value} formatted as a String in the current locale
69     * @throws IllegalArgumentException if {@code value} is negative
70     */
71    String getFormattedNumber(int value) {
72        final int length = value == 0 ? 1 : ((int) Math.log10(value) + 1);
73        return getFormattedNumber(false, value, length);
74    }
75
76    /**
77     * This method is intended to be used when formatting numbers occurs in a hotspot such as the
78     * update loop of a timer or stopwatch. It returns cached results when possible in order to
79     * provide speed and limit garbage to be collected by the virtual machine.
80     *
81     * @param value a positive integer to format as a String
82     * @param length the length of the String; zeroes are padded to match this length
83     * @return the {@code value} formatted as a String in the current locale and padded to the
84     *      requested {@code length}
85     * @throws IllegalArgumentException if {@code value} is negative
86     */
87    String getFormattedNumber(int value, int length) {
88        return getFormattedNumber(false, value, length);
89    }
90
91    /**
92     * This method is intended to be used when formatting numbers occurs in a hotspot such as the
93     * update loop of a timer or stopwatch. It returns cached results when possible in order to
94     * provide speed and limit garbage to be collected by the virtual machine.
95     *
96     * @param negative force a minus sign (-) onto the display, even if {@code value} is {@code 0}
97     * @param value a positive integer to format as a String
98     * @param length the length of the String; zeroes are padded to match this length. If
99     *      {@code negative} is {@code true} the return value will contain a minus sign and a total
100     *      length of {@code length + 1}.
101     * @return the {@code value} formatted as a String in the current locale and padded to the
102     *      requested {@code length}
103     * @throws IllegalArgumentException if {@code value} is negative
104     */
105    String getFormattedNumber(boolean negative, int value, int length) {
106        if (value < 0) {
107            throw new IllegalArgumentException("value may not be negative: " + value);
108        }
109
110        // Look up the value cache using the length; -ve and +ve values are cached separately.
111        final int lengthCacheKey = negative ? -length : length;
112        SparseArray<String> valueCache = mNumberFormatCache.get(lengthCacheKey);
113        if (valueCache == null) {
114            valueCache = new SparseArray<>((int) Math.pow(10, length));
115            mNumberFormatCache.put(lengthCacheKey, valueCache);
116        }
117
118        // Look up the cached formatted value using the value.
119        String formatted = valueCache.get(value);
120        if (formatted == null) {
121            final String sign = negative ? "−" : "";
122            formatted = String.format(Locale.getDefault(), sign + "%0" + length + "d", value);
123            valueCache.put(value, formatted);
124        }
125
126        return formatted;
127    }
128
129    /**
130     * @param calendarDay any of the following values
131     *                     <ul>
132     *                     <li>{@link Calendar#SUNDAY}</li>
133     *                     <li>{@link Calendar#MONDAY}</li>
134     *                     <li>{@link Calendar#TUESDAY}</li>
135     *                     <li>{@link Calendar#WEDNESDAY}</li>
136     *                     <li>{@link Calendar#THURSDAY}</li>
137     *                     <li>{@link Calendar#FRIDAY}</li>
138     *                     <li>{@link Calendar#SATURDAY}</li>
139     *                     </ul>
140     * @return single-character weekday name; e.g.: 'S', 'M', 'T', 'W', 'T', 'F', 'S'
141     */
142    String getShortWeekday(int calendarDay) {
143        if (mShortWeekdayNames == null) {
144            mShortWeekdayNames = new ArrayMap<>(7);
145
146            final SimpleDateFormat format = new SimpleDateFormat("ccccc", Locale.getDefault());
147            for (int i = Calendar.SUNDAY; i <= Calendar.SATURDAY; i++) {
148                final Calendar calendar = new GregorianCalendar(2014, JULY, 20 + i - 1);
149                final String weekday = format.format(calendar.getTime());
150                mShortWeekdayNames.put(i, weekday);
151            }
152        }
153
154        return mShortWeekdayNames.get(calendarDay);
155    }
156
157    /**
158     * @param calendarDay any of the following values
159     *                     <ul>
160     *                     <li>{@link Calendar#SUNDAY}</li>
161     *                     <li>{@link Calendar#MONDAY}</li>
162     *                     <li>{@link Calendar#TUESDAY}</li>
163     *                     <li>{@link Calendar#WEDNESDAY}</li>
164     *                     <li>{@link Calendar#THURSDAY}</li>
165     *                     <li>{@link Calendar#FRIDAY}</li>
166     *                     <li>{@link Calendar#SATURDAY}</li>
167     *                     </ul>
168     * @return full weekday name; e.g.: 'Sunday', 'Monday', 'Tuesday', etc.
169     */
170    String getLongWeekday(int calendarDay) {
171        if (mLongWeekdayNames == null) {
172            mLongWeekdayNames = new ArrayMap<>(7);
173
174            final Calendar calendar = new GregorianCalendar(2014, JULY, 20);
175            final SimpleDateFormat format = new SimpleDateFormat("EEEE", Locale.getDefault());
176            for (int i = Calendar.SUNDAY; i <= Calendar.SATURDAY; i++) {
177                final String weekday = format.format(calendar.getTime());
178                mLongWeekdayNames.put(i, weekday);
179                calendar.add(Calendar.DAY_OF_YEAR, 1);
180            }
181        }
182
183        return mLongWeekdayNames.get(calendarDay);
184    }
185
186    /**
187     * Cached information that is locale-sensitive must be cleared in response to locale changes.
188     */
189    private final class LocaleChangedReceiver extends BroadcastReceiver {
190        @Override
191        public void onReceive(Context context, Intent intent) {
192            mNumberFormatCache.clear();
193            mShortWeekdayNames = null;
194            mLongWeekdayNames = null;
195        }
196    }
197}