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.Gravity
21 import android.view.View
22 import android.view.ViewGroup.LayoutParams.MATCH_PARENT
23 import android.widget.FrameLayout
24 import com.android.systemui.customization.R
25 import com.android.systemui.plugins.clocks.AlarmData
26 import com.android.systemui.plugins.clocks.ClockAnimations
27 import com.android.systemui.plugins.clocks.ClockEvents
28 import com.android.systemui.plugins.clocks.ClockFaceConfig
29 import com.android.systemui.plugins.clocks.ClockFaceController
30 import com.android.systemui.plugins.clocks.ClockFaceEvents
31 import com.android.systemui.plugins.clocks.ClockFaceLayout
32 import com.android.systemui.plugins.clocks.ClockFontAxisSetting
33 import com.android.systemui.plugins.clocks.DefaultClockFaceLayout
34 import com.android.systemui.plugins.clocks.ThemeConfig
35 import com.android.systemui.plugins.clocks.WeatherData
36 import com.android.systemui.plugins.clocks.ZenData
37 import com.android.systemui.shared.clocks.view.FlexClockView
38 import com.android.systemui.shared.clocks.view.SimpleDigitalClockTextView
39 import java.util.Locale
40 import java.util.TimeZone
41 import kotlin.math.max
42 
43 // TODO(b/364680879): Merge w/ ComposedDigitalLayerController
44 class FlexClockFaceController(
45     clockCtx: ClockContext,
46     face: ClockFace,
47     private val isLargeClock: Boolean,
48 ) : ClockFaceController {
49     override val view: View
50         get() = layerController.view
51 
52     override val config = ClockFaceConfig(hasCustomPositionUpdatedAnimation = true)
53 
54     override var theme = ThemeConfig(true, clockCtx.settings.seedColor)
55 
56     private val keyguardLargeClockTopMargin =
57         clockCtx.resources.getDimensionPixelSize(R.dimen.keyguard_large_clock_top_margin)
58     val layerController: SimpleClockLayerController
59     val timespecHandler = DigitalTimespecHandler(DigitalTimespec.TIME_FULL_FORMAT, "hh:mm")
60 
61     init {
62         val lp = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
63         lp.gravity = Gravity.CENTER
64 
65         val layer = face.layers[0]
66 
67         layerController =
68             if (isLargeClock) {
69                 ComposedDigitalLayerController(clockCtx, layer as ComposedDigitalHandLayer)
70             } else {
71                 val childView = SimpleDigitalClockTextView(clockCtx)
72                 SimpleDigitalHandLayerController(clockCtx, layer as DigitalHandLayer, childView)
73             }
74         layerController.view.layoutParams = lp
75     }
76 
77     /** See documentation at [FlexClockView.offsetGlyphsForStepClockAnimation]. */
offsetGlyphsForStepClockAnimationnull78     private fun offsetGlyphsForStepClockAnimation(
79         clockStartLeft: Int,
80         direction: Int,
81         fraction: Float,
82     ) {
83         (view as? FlexClockView)?.offsetGlyphsForStepClockAnimation(
84             clockStartLeft,
85             direction,
86             fraction,
87         )
88     }
89 
90     override val layout: ClockFaceLayout =
<lambda>null91         DefaultClockFaceLayout(view).apply {
92             views[0].id =
93                 if (isLargeClock) R.id.lockscreen_clock_view_large else R.id.lockscreen_clock_view
94         }
95 
96     override val events = FlexClockFaceEvents()
97 
98     // TODO(b/364680879): Remove ClockEvents
99     inner class FlexClockFaceEvents : ClockEvents, ClockFaceEvents {
100         override var isReactiveTouchInteractionEnabled = false
101             get() = field
102             set(value) {
103                 field = value
104                 layerController.events.isReactiveTouchInteractionEnabled = value
105             }
106 
onTimeTicknull107         override fun onTimeTick() {
108             timespecHandler.updateTime()
109             view.contentDescription = timespecHandler.getContentDescription()
110             layerController.faceEvents.onTimeTick()
111         }
112 
onTimeZoneChangednull113         override fun onTimeZoneChanged(timeZone: TimeZone) {
114             timespecHandler.timeZone = timeZone
115             layerController.events.onTimeZoneChanged(timeZone)
116         }
117 
onTimeFormatChangednull118         override fun onTimeFormatChanged(is24Hr: Boolean) {
119             timespecHandler.is24Hr = is24Hr
120             layerController.events.onTimeFormatChanged(is24Hr)
121         }
122 
onLocaleChangednull123         override fun onLocaleChanged(locale: Locale) {
124             timespecHandler.updateLocale(locale)
125             layerController.events.onLocaleChanged(locale)
126         }
127 
onFontSettingChangednull128         override fun onFontSettingChanged(fontSizePx: Float) {
129             layerController.faceEvents.onFontSettingChanged(fontSizePx)
130             view.requestLayout()
131         }
132 
onThemeChangednull133         override fun onThemeChanged(theme: ThemeConfig) {
134             this@FlexClockFaceController.theme = theme
135             layerController.faceEvents.onThemeChanged(theme)
136         }
137 
onFontAxesChangednull138         override fun onFontAxesChanged(axes: List<ClockFontAxisSetting>) {
139             layerController.events.onFontAxesChanged(axes)
140         }
141 
142         /**
143          * targetRegion passed to all customized clock applies counter translationY of
144          * KeyguardStatusView and keyguard_large_clock_top_margin from default clock
145          */
onTargetRegionChangednull146         override fun onTargetRegionChanged(targetRegion: Rect?) {
147             // When a clock needs to be aligned with screen, like weather clock
148             // it needs to offset back the translation of keyguard_large_clock_top_margin
149             if (isLargeClock && (view as FlexClockView).isAlignedWithScreen()) {
150                 val topMargin = keyguardLargeClockTopMargin
151                 targetRegion?.let {
152                     val (_, yDiff) = computeLayoutDiff(view, it, isLargeClock)
153                     // In LS, we use yDiff to counter translate
154                     // the translation of KeyguardLargeClockTopMargin
155                     // With the targetRegion passed from picker,
156                     // we will have yDiff = 0, no translation is needed for weather clock
157                     if (yDiff.toInt() != 0) view.translationY = yDiff - topMargin / 2
158                 }
159                 return
160             }
161 
162             var maxWidth = 0f
163             var maxHeight = 0f
164 
165             layerController.faceEvents.onTargetRegionChanged(targetRegion)
166             maxWidth = max(maxWidth, view.layoutParams.width.toFloat())
167             maxHeight = max(maxHeight, view.layoutParams.height.toFloat())
168 
169             val lp =
170                 if (maxHeight <= 0 || maxWidth <= 0 || targetRegion == null) {
171                     // No specified width/height. Just match parent size.
172                     FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
173                 } else {
174                     // Scale to fit in targetRegion based on largest child elements.
175                     val ratio = maxWidth / maxHeight
176                     val targetRatio = targetRegion.width() / targetRegion.height().toFloat()
177                     val scale =
178                         if (ratio > targetRatio) targetRegion.width() / maxWidth
179                         else targetRegion.height() / maxHeight
180 
181                     FrameLayout.LayoutParams(
182                         (maxWidth * scale).toInt(),
183                         (maxHeight * scale).toInt(),
184                     )
185                 }
186 
187             lp.gravity = Gravity.CENTER
188             view.layoutParams = lp
189             targetRegion?.let {
190                 val (xDiff, yDiff) = computeLayoutDiff(view, it, isLargeClock)
191                 view.translationX = xDiff
192                 view.translationY = yDiff
193             }
194         }
195 
onSecondaryDisplayChangednull196         override fun onSecondaryDisplayChanged(onSecondaryDisplay: Boolean) {}
197 
onWeatherDataChangednull198         override fun onWeatherDataChanged(data: WeatherData) {
199             layerController.events.onWeatherDataChanged(data)
200         }
201 
onAlarmDataChangednull202         override fun onAlarmDataChanged(data: AlarmData) {
203             layerController.events.onAlarmDataChanged(data)
204         }
205 
onZenDataChangednull206         override fun onZenDataChanged(data: ZenData) {
207             layerController.events.onZenDataChanged(data)
208         }
209     }
210 
211     override val animations =
212         object : ClockAnimations {
enternull213             override fun enter() {
214                 layerController.animations.enter()
215             }
216 
dozenull217             override fun doze(fraction: Float) {
218                 layerController.animations.doze(fraction)
219             }
220 
foldnull221             override fun fold(fraction: Float) {
222                 layerController.animations.fold(fraction)
223             }
224 
chargenull225             override fun charge() {
226                 layerController.animations.charge()
227             }
228 
onPickerCarouselSwipingnull229             override fun onPickerCarouselSwiping(swipingFraction: Float) {
230                 face.pickerScale?.let {
231                     view.scaleX = swipingFraction * (1 - it.scaleX) + it.scaleX
232                     view.scaleY = swipingFraction * (1 - it.scaleY) + it.scaleY
233                 }
234                 if (isLargeClock && !(view as FlexClockView).isAlignedWithScreen()) {
235                     view.translationY = keyguardLargeClockTopMargin / 2F * swipingFraction
236                 }
237                 layerController.animations.onPickerCarouselSwiping(swipingFraction)
238                 view.invalidate()
239             }
240 
onPositionUpdatednull241             override fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) {
242                 layerController.animations.onPositionUpdated(fromLeft, direction, fraction)
243                 if (isLargeClock) offsetGlyphsForStepClockAnimation(fromLeft, direction, fraction)
244             }
245 
onPositionUpdatednull246             override fun onPositionUpdated(distance: Float, fraction: Float) {
247                 layerController.animations.onPositionUpdated(distance, fraction)
248                 // TODO(b/378128811) port stepping animation
249             }
250         }
251 }
252