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