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 }