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 }