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.Context
20 import android.content.SharedPreferences
21 import android.graphics.Typeface
22 import androidx.annotation.DrawableRes
23 import androidx.annotation.StringRes
24 
25 import com.android.deskclock.AlarmClockFragment
26 import com.android.deskclock.ClockFragment
27 import com.android.deskclock.R
28 import com.android.deskclock.Utils
29 import com.android.deskclock.stopwatch.StopwatchFragment
30 import com.android.deskclock.timer.TimerFragment
31 
32 /**
33  * All application-wide user interface data is accessible through this singleton.
34  */
35 class UiDataModel private constructor() {
36     /** Identifies each of the primary tabs within the application.  */
37     enum class Tab(
38         fragmentClass: Class<*>,
39         @DrawableRes val iconResId: Int,
40         @StringRes val labelResId: Int
41     ) {
42         ALARMS(AlarmClockFragment::class.java, R.drawable.ic_tab_alarm, R.string.menu_alarm),
43         CLOCKS(ClockFragment::class.java, R.drawable.ic_tab_clock, R.string.menu_clock),
44         TIMERS(TimerFragment::class.java, R.drawable.ic_tab_timer, R.string.menu_timer),
45         STOPWATCH(StopwatchFragment::class.java,
46                 R.drawable.ic_tab_stopwatch, R.string.menu_stopwatch);
47 
48         val fragmentClassName: String = fragmentClass.name
49     }
50 
51     private var mContext: Context? = null
52 
53     /** The model from which tab data are fetched.  */
54     private lateinit var mTabModel: TabModel
55 
56     /** The model from which formatted strings are fetched.  */
57     private lateinit var mFormattedStringModel: FormattedStringModel
58 
59     /** The model from which timed callbacks originate.  */
60     private lateinit var mPeriodicCallbackModel: PeriodicCallbackModel
61 
62     /**
63      * The context may be set precisely once during the application life.
64      */
initnull65     fun init(context: Context, prefs: SharedPreferences) {
66         if (mContext !== context) {
67             mContext = context.applicationContext
68 
69             mPeriodicCallbackModel = PeriodicCallbackModel(mContext!!)
70             mFormattedStringModel = FormattedStringModel(mContext!!)
71             mTabModel = TabModel(prefs)
72         }
73     }
74 
75     /**
76      * To display the alarm clock in this font, use the character [R.string.clock_emoji].
77      *
78      * @return a special font containing a glyph that draws an alarm clock
79      */
80     val alarmIconTypeface: Typeface
81         get() = Typeface.createFromAsset(mContext!!.assets, "fonts/clock.ttf")
82 
83     //
84     // Formatted Strings
85     //
86 
87     /**
88      * This method is intended to be used when formatting numbers occurs in a hotspot such as the
89      * update loop of a timer or stopwatch. It returns cached results when possible in order to
90      * provide speed and limit garbage to be collected by the virtual machine.
91      *
92      * @param value a positive integer to format as a String
93      * @return the `value` formatted as a String in the current locale
94      * @throws IllegalArgumentException if `value` is negative
95      */
getFormattedNumbernull96     fun getFormattedNumber(value: Int): String {
97         Utils.enforceMainLooper()
98         return mFormattedStringModel.getFormattedNumber(value)
99     }
100 
101     /**
102      * This method is intended to be used when formatting numbers occurs in a hotspot such as the
103      * update loop of a timer or stopwatch. It returns cached results when possible in order to
104      * provide speed and limit garbage to be collected by the virtual machine.
105      *
106      * @param value a positive integer to format as a String
107      * @param length the length of the String; zeroes are padded to match this length
108      * @return the `value` formatted as a String in the current locale and padded to the
109      * requested `length`
110      * @throws IllegalArgumentException if `value` is negative
111      */
getFormattedNumbernull112     fun getFormattedNumber(value: Int, length: Int): String {
113         Utils.enforceMainLooper()
114         return mFormattedStringModel.getFormattedNumber(value, length)
115     }
116 
117     /**
118      * This method is intended to be used when formatting numbers occurs in a hotspot such as the
119      * update loop of a timer or stopwatch. It returns cached results when possible in order to
120      * provide speed and limit garbage to be collected by the virtual machine.
121      *
122      * @param negative force a minus sign (-) onto the display, even if `value` is `0`
123      * @param value a positive integer to format as a String
124      * @param length the length of the String; zeroes are padded to match this length. If
125      * `negative` is `true` the return value will contain a minus sign and a total
126      * length of `length + 1`.
127      * @return the `value` formatted as a String in the current locale and padded to the
128      * requested `length`
129      * @throws IllegalArgumentException if `value` is negative
130      */
getFormattedNumbernull131     fun getFormattedNumber(negative: Boolean, value: Int, length: Int): String {
132         Utils.enforceMainLooper()
133         return mFormattedStringModel.getFormattedNumber(negative, value, length)
134     }
135 
136     /**
137      * @param calendarDay any of the following values
138      *
139      *  * [Calendar.SUNDAY]
140      *  * [Calendar.MONDAY]
141      *  * [Calendar.TUESDAY]
142      *  * [Calendar.WEDNESDAY]
143      *  * [Calendar.THURSDAY]
144      *  * [Calendar.FRIDAY]
145      *  * [Calendar.SATURDAY]
146      *
147      * @return single-character version of weekday name; e.g.: 'S', 'M', 'T', 'W', 'T', 'F', 'S'
148      */
getShortWeekdaynull149     fun getShortWeekday(calendarDay: Int): String? {
150         Utils.enforceMainLooper()
151         return mFormattedStringModel.getShortWeekday(calendarDay)
152     }
153 
154     /**
155      * @param calendarDay any of the following values
156      *
157      *  * [Calendar.SUNDAY]
158      *  * [Calendar.MONDAY]
159      *  * [Calendar.TUESDAY]
160      *  * [Calendar.WEDNESDAY]
161      *  * [Calendar.THURSDAY]
162      *  * [Calendar.FRIDAY]
163      *  * [Calendar.SATURDAY]
164      *
165      * @return full weekday name; e.g.: 'Sunday', 'Monday', 'Tuesday', etc.
166      */
getLongWeekdaynull167     fun getLongWeekday(calendarDay: Int): String? {
168         Utils.enforceMainLooper()
169         return mFormattedStringModel.getLongWeekday(calendarDay)
170     }
171 
172     //
173     // Animations
174     //
175 
176     /**
177      * @return the duration in milliseconds of short animations
178      */
179     val shortAnimationDuration: Long
180         get() {
181             Utils.enforceMainLooper()
182             return mContext!!.resources.getInteger(android.R.integer.config_shortAnimTime).toLong()
183         }
184 
185     /**
186      * @return the duration in milliseconds of long animations
187      */
188     val longAnimationDuration: Long
189         get() {
190             Utils.enforceMainLooper()
191             return mContext!!.resources.getInteger(android.R.integer.config_longAnimTime).toLong()
192         }
193 
194     //
195     // Tabs
196     //
197 
198     /**
199      * @param tabListener to be notified when the selected tab changes
200      */
addTabListenernull201     fun addTabListener(tabListener: TabListener) {
202         Utils.enforceMainLooper()
203         mTabModel.addTabListener(tabListener)
204     }
205 
206     /**
207      * @param tabListener to no longer be notified when the selected tab changes
208      */
removeTabListenernull209     fun removeTabListener(tabListener: TabListener) {
210         Utils.enforceMainLooper()
211         mTabModel.removeTabListener(tabListener)
212     }
213 
214     /**
215      * @return the number of tabs
216      */
217     val tabCount: Int
218         get() {
219             Utils.enforceMainLooper()
220             return mTabModel.tabCount
221         }
222 
223     /**
224      * @param ordinal the ordinal of the tab
225      * @return the tab at the given `ordinal`
226      */
getTabnull227     fun getTab(ordinal: Int): Tab {
228         Utils.enforceMainLooper()
229         return mTabModel.getTab(ordinal)
230     }
231 
232     /**
233      * @param position the position of the tab in the user interface
234      * @return the tab at the given `ordinal`
235      */
getTabAtnull236     fun getTabAt(position: Int): Tab {
237         Utils.enforceMainLooper()
238         return mTabModel.getTabAt(position)
239     }
240 
241     var selectedTab: Tab
242         /**
243          * @return an enumerated value indicating the currently selected primary tab
244          */
245         get() {
246             Utils.enforceMainLooper()
247             return mTabModel.selectedTab
248         }
249         /**
250          * @param tab an enumerated value indicating the newly selected primary tab
251          */
252         set(tab) {
253             Utils.enforceMainLooper()
254             mTabModel.setSelectedTab(tab)
255         }
256 
257     /**
258      * @param tabScrollListener to be notified when the scroll position of the selected tab changes
259      */
addTabScrollListenernull260     fun addTabScrollListener(tabScrollListener: TabScrollListener) {
261         Utils.enforceMainLooper()
262         mTabModel.addTabScrollListener(tabScrollListener)
263     }
264 
265     /**
266      * @param tabScrollListener to be notified when the scroll position of the selected tab changes
267      */
removeTabScrollListenernull268     fun removeTabScrollListener(tabScrollListener: TabScrollListener) {
269         Utils.enforceMainLooper()
270         mTabModel.removeTabScrollListener(tabScrollListener)
271     }
272 
273     /**
274      * Updates the scrolling state in the [UiDataModel] for this tab.
275      *
276      * @param tab an enumerated value indicating the tab reporting its vertical scroll position
277      * @param scrolledToTop `true` iff the vertical scroll position of the tab is at the top
278      */
setTabScrolledToTopnull279     fun setTabScrolledToTop(tab: Tab, scrolledToTop: Boolean) {
280         Utils.enforceMainLooper()
281         mTabModel.setTabScrolledToTop(tab, scrolledToTop)
282     }
283 
284     /**
285      * @return `true` iff the content in the selected tab is currently scrolled to the top
286      */
287     val isSelectedTabScrolledToTop: Boolean
288         get() {
289             Utils.enforceMainLooper()
290             return mTabModel.isTabScrolledToTop(selectedTab)
291         }
292 
293     //
294     // Shortcut Ids
295     //
296 
297     /**
298      * @param category which category of shortcut of which to get the id
299      * @param action the desired action to perform
300      * @return the id of the shortcut
301      */
getShortcutIdnull302     fun getShortcutId(@StringRes category: Int, @StringRes action: Int): String {
303         return if (category == R.string.category_stopwatch) {
304             mContext!!.getString(category)
305         } else {
306             mContext!!.getString(category) + "_" + mContext!!.getString(action)
307         }
308     }
309 
310     //
311     // Timed Callbacks
312     //
313 
314     /**
315      * @param runnable to be called every minute
316      * @param offset an offset applied to the minute to control when the callback occurs
317      */
addMinuteCallbacknull318     fun addMinuteCallback(runnable: Runnable, offset: Long) {
319         Utils.enforceMainLooper()
320         mPeriodicCallbackModel.addMinuteCallback(runnable, offset)
321     }
322 
323     /**
324      * @param runnable to be called every quarter-hour
325      */
addQuarterHourCallbacknull326     fun addQuarterHourCallback(runnable: Runnable) {
327         Utils.enforceMainLooper()
328         mPeriodicCallbackModel.addQuarterHourCallback(runnable)
329     }
330 
331     /**
332      * @param runnable to be called every hour
333      */
addHourCallbacknull334     fun addHourCallback(runnable: Runnable) {
335         Utils.enforceMainLooper()
336         mPeriodicCallbackModel.addHourCallback(runnable)
337     }
338 
339     /**
340      * @param runnable to be called every midnight
341      */
addMidnightCallbacknull342     fun addMidnightCallback(runnable: Runnable) {
343         Utils.enforceMainLooper()
344         mPeriodicCallbackModel.addMidnightCallback(runnable)
345     }
346 
347     /**
348      * @param runnable to no longer be called periodically
349      */
removePeriodicCallbacknull350     fun removePeriodicCallback(runnable: Runnable) {
351         Utils.enforceMainLooper()
352         mPeriodicCallbackModel.removePeriodicCallback(runnable)
353     }
354 
355     companion object {
356         /** The single instance of this data model that exists for the life of the application.  */
357         val sUiDataModel = UiDataModel()
358 
359         @get:JvmStatic
360         val uiDataModel
361             get() = sUiDataModel
362     }
363 }