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 17 package com.android.deskclock.uidata; 18 19 import android.content.BroadcastReceiver; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.content.IntentFilter; 23 import android.util.ArrayMap; 24 import android.util.SparseArray; 25 26 import java.text.SimpleDateFormat; 27 import java.util.Calendar; 28 import java.util.GregorianCalendar; 29 import java.util.Locale; 30 import java.util.Map; 31 32 import static java.util.Calendar.JULY; 33 34 /** 35 * All formatted strings that are cached for performance are accessed via this model. 36 */ 37 final 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 FormattedStringModel(Context context)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 */ getFormattedNumber(int value)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 */ getFormattedNumber(int value, int length)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 */ getFormattedNumber(boolean negative, int value, int length)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 */ getShortWeekday(int calendarDay)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 */ getLongWeekday(int calendarDay)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 onReceive(Context context, Intent intent)191 public void onReceive(Context context, Intent intent) { 192 mNumberFormatCache.clear(); 193 mShortWeekdayNames = null; 194 mLongWeekdayNames = null; 195 } 196 } 197 }