1 /*
2  * Copyright (C) 2024 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.systemui.shared.clocks
18 
19 import android.icu.text.DateFormat
20 import android.icu.text.SimpleDateFormat
21 import android.icu.util.TimeZone as IcuTimeZone
22 import android.icu.util.ULocale
23 import androidx.annotation.VisibleForTesting
24 import java.util.Calendar
25 import java.util.Locale
26 import java.util.TimeZone
27 
28 open class TimespecHandler(
29     val cal: Calendar,
30 ) {
31     var timeZone: TimeZone
32         get() = cal.timeZone
33         set(value) {
34             cal.timeZone = value
35             onTimeZoneChanged()
36         }
37 
38     @VisibleForTesting var fakeTimeMills: Long? = null
39 
updateTimenull40     fun updateTime() {
41         var timeMs = fakeTimeMills ?: System.currentTimeMillis()
42         cal.timeInMillis = (timeMs * TIME_TRAVEL_SCALE).toLong()
43     }
44 
onTimeZoneChangednull45     protected open fun onTimeZoneChanged() {}
46 
47     companion object {
48         // Modifying this will cause the clock to run faster or slower. This is a useful way of
49         // manually checking that clocks are correctly animating through time.
50         private const val TIME_TRAVEL_SCALE = 1.0
51     }
52 }
53 
54 class DigitalTimespecHandler(
55     val timespec: DigitalTimespec,
56     private val timeFormat: String,
57     cal: Calendar = Calendar.getInstance(),
58 ) : TimespecHandler(cal) {
59     var is24Hr = false
60         set(value) {
61             field = value
62             applyPattern()
63         }
64 
65     private var dateFormat = updateSimpleDateFormat(Locale.getDefault())
66     private var contentDescriptionFormat = getContentDescriptionFormat(Locale.getDefault())
67 
68     init {
69         applyPattern()
70     }
71 
onTimeZoneChangednull72     override fun onTimeZoneChanged() {
73         dateFormat.timeZone = IcuTimeZone.getTimeZone(cal.timeZone.id)
74         contentDescriptionFormat?.timeZone = IcuTimeZone.getTimeZone(cal.timeZone.id)
75         applyPattern()
76     }
77 
updateLocalenull78     fun updateLocale(locale: Locale) {
79         dateFormat = updateSimpleDateFormat(locale)
80         contentDescriptionFormat = getContentDescriptionFormat(locale)
81         onTimeZoneChanged()
82     }
83 
updateSimpleDateFormatnull84     private fun updateSimpleDateFormat(locale: Locale): DateFormat {
85         if (
86             locale.language.equals(Locale.ENGLISH.language) ||
87                 timespec != DigitalTimespec.DATE_FORMAT
88         ) {
89             // force date format in English, and time format to use format defined in json
90             return SimpleDateFormat(timeFormat, timeFormat, ULocale.forLocale(locale))
91         } else {
92             return SimpleDateFormat.getInstanceForSkeleton(timeFormat, locale)
93         }
94     }
95 
getContentDescriptionFormatnull96     private fun getContentDescriptionFormat(locale: Locale): DateFormat? {
97         return when (timespec) {
98             DigitalTimespec.TIME_FULL_FORMAT ->
99                 SimpleDateFormat.getInstanceForSkeleton("hh:mm", locale)
100             DigitalTimespec.DATE_FORMAT ->
101                 SimpleDateFormat.getInstanceForSkeleton("EEEE MMMM d", locale)
102             else -> {
103                 null
104             }
105         }
106     }
107 
applyPatternnull108     private fun applyPattern() {
109         val timeFormat24Hour = timeFormat.replace("hh", "h").replace("h", "HH")
110         val format = if (is24Hr) timeFormat24Hour else timeFormat
111         if (timespec != DigitalTimespec.DATE_FORMAT) {
112             (dateFormat as SimpleDateFormat).applyPattern(format)
113             (contentDescriptionFormat as? SimpleDateFormat)?.applyPattern(
114                 if (is24Hr) CONTENT_DESCRIPTION_TIME_FORMAT_24_HOUR
115                 else CONTENT_DESCRIPTION_TIME_FORMAT_12_HOUR
116             )
117         }
118     }
119 
getSingleDigitnull120     private fun getSingleDigit(): String {
121         val isFirstDigit = timespec == DigitalTimespec.FIRST_DIGIT
122         val text = dateFormat.format(cal.time).toString()
123         return text.substring(
124             if (isFirstDigit) 0 else text.length - 1,
125             if (isFirstDigit) text.length - 1 else text.length
126         )
127     }
128 
getDigitStringnull129     fun getDigitString(): String {
130         return when (timespec) {
131             DigitalTimespec.FIRST_DIGIT,
132             DigitalTimespec.SECOND_DIGIT -> getSingleDigit()
133             DigitalTimespec.DIGIT_PAIR -> {
134                 dateFormat.format(cal.time).toString()
135             }
136             DigitalTimespec.TIME_FULL_FORMAT -> {
137                 dateFormat.format(cal.time).toString()
138             }
139             DigitalTimespec.DATE_FORMAT -> {
140                 dateFormat.format(cal.time).toString().uppercase()
141             }
142         }
143     }
144 
getContentDescriptionnull145     fun getContentDescription(): String? {
146         return when (timespec) {
147             DigitalTimespec.TIME_FULL_FORMAT,
148             DigitalTimespec.DATE_FORMAT -> {
149                 contentDescriptionFormat?.format(cal.time).toString()
150             }
151             else -> {
152                 return null
153             }
154         }
155     }
156 
157     companion object {
158         const val CONTENT_DESCRIPTION_TIME_FORMAT_12_HOUR = "hh:mm"
159         const val CONTENT_DESCRIPTION_TIME_FORMAT_24_HOUR = "HH:mm"
160     }
161 }
162