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