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.graphics.Rect
20 import android.view.View
21 import android.view.ViewGroup
22 import android.widget.RelativeLayout
23 import androidx.annotation.VisibleForTesting
24 import com.android.systemui.customization.R
25 import com.android.systemui.log.core.Logger
26 import com.android.systemui.plugins.clocks.AlarmData
27 import com.android.systemui.plugins.clocks.ClockAnimations
28 import com.android.systemui.plugins.clocks.ClockEvents
29 import com.android.systemui.plugins.clocks.ClockFaceConfig
30 import com.android.systemui.plugins.clocks.ClockFaceEvents
31 import com.android.systemui.plugins.clocks.ClockFontAxisSetting
32 import com.android.systemui.plugins.clocks.ThemeConfig
33 import com.android.systemui.plugins.clocks.WeatherData
34 import com.android.systemui.plugins.clocks.ZenData
35 import com.android.systemui.shared.clocks.view.SimpleDigitalClockView
36 import java.util.Locale
37 import java.util.TimeZone
38 
39 private val TAG = SimpleDigitalHandLayerController::class.simpleName!!
40 
41 open class SimpleDigitalHandLayerController<T>(
42     private val clockCtx: ClockContext,
43     private val layer: DigitalHandLayer,
44     override val view: T,
45 ) : SimpleClockLayerController where T : View, T : SimpleDigitalClockView {
46     private val logger = Logger(clockCtx.messageBuffer, TAG)
47     val timespec = DigitalTimespecHandler(layer.timespec, layer.dateTimeFormat)
48 
49     @VisibleForTesting
hasLeadingZeronull50     fun hasLeadingZero() = layer.dateTimeFormat.startsWith("hh") || timespec.is24Hr
51 
52     @VisibleForTesting
53     override var fakeTimeMills: Long?
54         get() = timespec.fakeTimeMills
55         set(value) {
56             timespec.fakeTimeMills = value
57         }
58 
59     override val config = ClockFaceConfig()
60     var dozeState: DefaultClockController.AnimationState? = null
61 
62     init {
63         view.layoutParams =
64             RelativeLayout.LayoutParams(
65                 ViewGroup.LayoutParams.WRAP_CONTENT,
66                 ViewGroup.LayoutParams.WRAP_CONTENT,
67             )
68         if (layer.alignment != null) {
<lambda>null69             layer.alignment.verticalAlignment?.let { view.verticalAlignment = it }
<lambda>null70             layer.alignment.horizontalAlignment?.let { view.horizontalAlignment = it }
71         }
72         view.applyStyles(layer.style, layer.aodStyle)
73         view.id =
74             clockCtx.resources.getIdentifier(
75                 generateDigitalLayerIdString(layer),
76                 "id",
77                 clockCtx.context.getPackageName(),
78             )
79     }
80 
applyLayoutnull81     fun applyLayout(layout: DigitalFaceLayout?) {
82         when (layout) {
83             DigitalFaceLayout.FOUR_DIGITS_ALIGN_CENTER,
84             DigitalFaceLayout.FOUR_DIGITS_HORIZONTAL -> applyFourDigitsLayout(layout)
85             DigitalFaceLayout.TWO_PAIRS_HORIZONTAL,
86             DigitalFaceLayout.TWO_PAIRS_VERTICAL -> applyTwoPairsLayout(layout)
87             else -> {
88                 // one view always use FrameLayout
89                 // no need to change here
90             }
91         }
92         applyMargin()
93     }
94 
applyMarginnull95     private fun applyMargin() {
96         if (view.layoutParams is RelativeLayout.LayoutParams) {
97             val lp = view.layoutParams as RelativeLayout.LayoutParams
98             layer.marginRatio?.let {
99                 lp.setMargins(
100                     /* left = */ (it.left * view.measuredWidth).toInt(),
101                     /* top = */ (it.top * view.measuredHeight).toInt(),
102                     /* right = */ (it.right * view.measuredWidth).toInt(),
103                     /* bottom = */ (it.bottom * view.measuredHeight).toInt(),
104                 )
105             }
106             view.layoutParams = lp
107         }
108     }
109 
applyTwoPairsLayoutnull110     private fun applyTwoPairsLayout(twoPairsLayout: DigitalFaceLayout) {
111         val lp = view.layoutParams as RelativeLayout.LayoutParams
112         lp.addRule(RelativeLayout.TEXT_ALIGNMENT_CENTER)
113         if (twoPairsLayout == DigitalFaceLayout.TWO_PAIRS_HORIZONTAL) {
114             when (view.id) {
115                 R.id.HOUR_DIGIT_PAIR -> {
116                     lp.addRule(RelativeLayout.CENTER_VERTICAL)
117                     lp.addRule(RelativeLayout.ALIGN_PARENT_START)
118                 }
119                 R.id.MINUTE_DIGIT_PAIR -> {
120                     lp.addRule(RelativeLayout.CENTER_VERTICAL)
121                     lp.addRule(RelativeLayout.END_OF, R.id.HOUR_DIGIT_PAIR)
122                 }
123                 else -> {
124                     throw Exception("cannot apply two pairs layout to view ${view.id}")
125                 }
126             }
127         } else {
128             when (view.id) {
129                 R.id.HOUR_DIGIT_PAIR -> {
130                     lp.addRule(RelativeLayout.CENTER_HORIZONTAL)
131                     lp.addRule(RelativeLayout.ALIGN_PARENT_TOP)
132                 }
133                 R.id.MINUTE_DIGIT_PAIR -> {
134                     lp.addRule(RelativeLayout.CENTER_HORIZONTAL)
135                     lp.addRule(RelativeLayout.BELOW, R.id.HOUR_DIGIT_PAIR)
136                 }
137                 else -> {
138                     throw Exception("cannot apply two pairs layout to view ${view.id}")
139                 }
140             }
141         }
142         view.layoutParams = lp
143     }
144 
applyFourDigitsLayoutnull145     private fun applyFourDigitsLayout(fourDigitsfaceLayout: DigitalFaceLayout) {
146         val lp = view.layoutParams as RelativeLayout.LayoutParams
147         when (fourDigitsfaceLayout) {
148             DigitalFaceLayout.FOUR_DIGITS_ALIGN_CENTER -> {
149                 when (view.id) {
150                     R.id.HOUR_FIRST_DIGIT -> {
151                         lp.addRule(RelativeLayout.ALIGN_PARENT_START)
152                         lp.addRule(RelativeLayout.ALIGN_PARENT_TOP)
153                     }
154                     R.id.HOUR_SECOND_DIGIT -> {
155                         lp.addRule(RelativeLayout.END_OF, R.id.HOUR_FIRST_DIGIT)
156                         lp.addRule(RelativeLayout.ALIGN_TOP, R.id.HOUR_FIRST_DIGIT)
157                     }
158                     R.id.MINUTE_FIRST_DIGIT -> {
159                         lp.addRule(RelativeLayout.ALIGN_START, R.id.HOUR_FIRST_DIGIT)
160                         lp.addRule(RelativeLayout.BELOW, R.id.HOUR_FIRST_DIGIT)
161                     }
162                     R.id.MINUTE_SECOND_DIGIT -> {
163                         lp.addRule(RelativeLayout.ALIGN_START, R.id.HOUR_SECOND_DIGIT)
164                         lp.addRule(RelativeLayout.BELOW, R.id.HOUR_SECOND_DIGIT)
165                     }
166                     else -> {
167                         throw Exception("cannot apply four digits layout to view ${view.id}")
168                     }
169                 }
170             }
171             DigitalFaceLayout.FOUR_DIGITS_HORIZONTAL -> {
172                 when (view.id) {
173                     R.id.HOUR_FIRST_DIGIT -> {
174                         lp.addRule(RelativeLayout.CENTER_VERTICAL)
175                         lp.addRule(RelativeLayout.ALIGN_PARENT_START)
176                     }
177                     R.id.HOUR_SECOND_DIGIT -> {
178                         lp.addRule(RelativeLayout.CENTER_VERTICAL)
179                         lp.addRule(RelativeLayout.END_OF, R.id.HOUR_FIRST_DIGIT)
180                     }
181                     R.id.MINUTE_FIRST_DIGIT -> {
182                         lp.addRule(RelativeLayout.CENTER_VERTICAL)
183                         lp.addRule(RelativeLayout.END_OF, R.id.HOUR_SECOND_DIGIT)
184                     }
185                     R.id.MINUTE_SECOND_DIGIT -> {
186                         lp.addRule(RelativeLayout.CENTER_VERTICAL)
187                         lp.addRule(RelativeLayout.END_OF, R.id.MINUTE_FIRST_DIGIT)
188                     }
189                     else -> {
190                         throw Exception("cannot apply FOUR_DIGITS_HORIZONTAL to view ${view.id}")
191                     }
192                 }
193             }
194             else -> {
195                 throw IllegalArgumentException(
196                     "applyFourDigitsLayout function should not " +
197                         "have parameters as ${layer.faceLayout}"
198                 )
199             }
200         }
201         if (lp == view.layoutParams) {
202             return
203         }
204         view.layoutParams = lp
205     }
206 
refreshTimenull207     fun refreshTime() {
208         timespec.updateTime()
209         val text = timespec.getDigitString()
210         if (view.text != text) {
211             view.text = text
212             view.refreshTime()
213             logger.d({ "refreshTime: new text=$str1" }) { str1 = text }
214         }
215     }
216 
217     override val events =
218         object : ClockEvents {
219             override var isReactiveTouchInteractionEnabled = false
220 
onLocaleChangednull221             override fun onLocaleChanged(locale: Locale) {
222                 timespec.updateLocale(locale)
223                 refreshTime()
224             }
225 
226             /** Call whenever the text time format changes (12hr vs 24hr) */
onTimeFormatChangednull227             override fun onTimeFormatChanged(is24Hr: Boolean) {
228                 timespec.is24Hr = is24Hr
229                 refreshTime()
230             }
231 
onTimeZoneChangednull232             override fun onTimeZoneChanged(timeZone: TimeZone) {
233                 timespec.timeZone = timeZone
234                 refreshTime()
235             }
236 
onWeatherDataChangednull237             override fun onWeatherDataChanged(data: WeatherData) {}
238 
onAlarmDataChangednull239             override fun onAlarmDataChanged(data: AlarmData) {}
240 
onZenDataChangednull241             override fun onZenDataChanged(data: ZenData) {}
242 
onFontAxesChangednull243             override fun onFontAxesChanged(axes: List<ClockFontAxisSetting>) {
244                 view.updateAxes(axes)
245             }
246         }
247 
248     override val animations =
249         object : ClockAnimations {
enternull250             override fun enter() {
251                 applyLayout(layer.faceLayout)
252                 refreshTime()
253             }
254 
dozenull255             override fun doze(fraction: Float) {
256                 if (dozeState == null) {
257                     dozeState = DefaultClockController.AnimationState(fraction)
258                     view.animateDoze(dozeState!!.isActive, false)
259                 } else {
260                     val (hasChanged, hasJumped) = dozeState!!.update(fraction)
261                     if (hasChanged) view.animateDoze(dozeState!!.isActive, !hasJumped)
262                 }
263                 view.dozeFraction = fraction
264             }
265 
foldnull266             override fun fold(fraction: Float) {
267                 applyLayout(layer.faceLayout)
268                 refreshTime()
269             }
270 
chargenull271             override fun charge() {
272                 view.animateCharge()
273             }
274 
onPickerCarouselSwipingnull275             override fun onPickerCarouselSwiping(swipingFraction: Float) {}
276 
onPositionUpdatednull277             override fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) {}
278 
onPositionUpdatednull279             override fun onPositionUpdated(distance: Float, fraction: Float) {}
280         }
281 
282     override val faceEvents =
283         object : ClockFaceEvents {
onTimeTicknull284             override fun onTimeTick() {
285                 refreshTime()
286                 if (
287                     layer.timespec == DigitalTimespec.TIME_FULL_FORMAT ||
288                         layer.timespec == DigitalTimespec.DATE_FORMAT
289                 ) {
290                     view.contentDescription = timespec.getContentDescription()
291                 }
292             }
293 
onFontSettingChangednull294             override fun onFontSettingChanged(fontSizePx: Float) {
295                 view.applyTextSize(fontSizePx)
296                 applyMargin()
297             }
298 
onThemeChangednull299             override fun onThemeChanged(theme: ThemeConfig) {
300                 val color =
301                     when {
302                         theme.seedColor != null -> theme.seedColor!!
303                         theme.isDarkTheme ->
304                             clockCtx.resources.getColor(android.R.color.system_accent1_100)
305                         else -> clockCtx.resources.getColor(android.R.color.system_accent2_600)
306                     }
307 
308                 view.updateColor(color)
309                 refreshTime()
310             }
311 
onTargetRegionChangednull312             override fun onTargetRegionChanged(targetRegion: Rect?) {}
313 
onSecondaryDisplayChangednull314             override fun onSecondaryDisplayChanged(onSecondaryDisplay: Boolean) {}
315         }
316 }
317