1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5  * except in compliance with the License. You may obtain a copy of the License at
6  *
7  *      http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software distributed under the
10  * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
11  * KIND, either express or implied. See the License for the specific language governing
12  * permissions and limitations under the License.
13  */
14 package com.android.systemui.shared.clocks
15 
16 import android.content.Context
17 import android.content.res.Resources
18 import android.graphics.Color
19 import android.graphics.Rect
20 import android.icu.text.NumberFormat
21 import android.util.TypedValue
22 import android.view.LayoutInflater
23 import android.view.View
24 import android.widget.FrameLayout
25 import androidx.annotation.VisibleForTesting
26 import com.android.systemui.customization.R
27 import com.android.systemui.log.core.MessageBuffer
28 import com.android.systemui.plugins.ClockAnimations
29 import com.android.systemui.plugins.ClockConfig
30 import com.android.systemui.plugins.ClockController
31 import com.android.systemui.plugins.ClockEvents
32 import com.android.systemui.plugins.ClockFaceConfig
33 import com.android.systemui.plugins.ClockFaceController
34 import com.android.systemui.plugins.ClockFaceEvents
35 import com.android.systemui.plugins.ClockSettings
36 import com.android.systemui.plugins.WeatherData
37 import java.io.PrintWriter
38 import java.util.Locale
39 import java.util.TimeZone
40 
41 private val TAG = DefaultClockController::class.simpleName
42 
43 /**
44  * Controls the default clock visuals.
45  *
46  * This serves as an adapter between the clock interface and the AnimatableClockView used by the
47  * existing lockscreen clock.
48  */
49 class DefaultClockController(
50     ctx: Context,
51     private val layoutInflater: LayoutInflater,
52     private val resources: Resources,
53     private val settings: ClockSettings?,
54     private val hasStepClockAnimation: Boolean = false,
55 ) : ClockController {
56     override val smallClock: DefaultClockFaceController
57     override val largeClock: LargeClockFaceController
58     private val clocks: List<AnimatableClockView>
59 
60     private val burmeseNf = NumberFormat.getInstance(Locale.forLanguageTag("my"))
61     private val burmeseNumerals = burmeseNf.format(FORMAT_NUMBER.toLong())
62     private val burmeseLineSpacing =
63         resources.getFloat(R.dimen.keyguard_clock_line_spacing_scale_burmese)
64     private val defaultLineSpacing = resources.getFloat(R.dimen.keyguard_clock_line_spacing_scale)
65     protected var onSecondaryDisplay: Boolean = false
66 
67     override val events: DefaultClockEvents
68     override val config = ClockConfig(DEFAULT_CLOCK_ID)
69 
70     init {
71         val parent = FrameLayout(ctx)
72         smallClock =
73             DefaultClockFaceController(
74                 layoutInflater.inflate(R.layout.clock_default_small, parent, false)
75                     as AnimatableClockView,
76                 settings?.seedColor
77             )
78         largeClock =
79             LargeClockFaceController(
80                 layoutInflater.inflate(R.layout.clock_default_large, parent, false)
81                     as AnimatableClockView,
82                 settings?.seedColor
83             )
84         clocks = listOf(smallClock.view, largeClock.view)
85 
86         events = DefaultClockEvents()
87         events.onLocaleChanged(Locale.getDefault())
88     }
89 
90     override fun initialize(resources: Resources, dozeFraction: Float, foldFraction: Float) {
91         largeClock.recomputePadding(null)
92         largeClock.animations = LargeClockAnimations(largeClock.view, dozeFraction, foldFraction)
93         smallClock.animations = DefaultClockAnimations(smallClock.view, dozeFraction, foldFraction)
94         events.onColorPaletteChanged(resources)
95         events.onTimeZoneChanged(TimeZone.getDefault())
96         smallClock.events.onTimeTick()
97         largeClock.events.onTimeTick()
98     }
99 
100     open inner class DefaultClockFaceController(
101         override val view: AnimatableClockView,
102         var seedColor: Int?,
103     ) : ClockFaceController {
104 
105         // MAGENTA is a placeholder, and will be assigned correctly in initialize
106         private var currentColor = Color.MAGENTA
107         private var isRegionDark = false
108         protected var targetRegion: Rect? = null
109 
110         override val config = ClockFaceConfig()
111 
112         override var messageBuffer: MessageBuffer?
113             get() = view.messageBuffer
114             set(value) {
115                 view.messageBuffer = value
116             }
117 
118         override var animations: DefaultClockAnimations = DefaultClockAnimations(view, 0f, 0f)
119             internal set
120 
121         init {
122             if (seedColor != null) {
123                 currentColor = seedColor!!
124             }
125             view.setColors(DOZE_COLOR, currentColor)
126         }
127 
128         override val events =
129             object : ClockFaceEvents {
130                 override fun onTimeTick() = view.refreshTime()
131 
132                 override fun onRegionDarknessChanged(isRegionDark: Boolean) {
133                     this@DefaultClockFaceController.isRegionDark = isRegionDark
134                     updateColor()
135                 }
136 
137                 override fun onTargetRegionChanged(targetRegion: Rect?) {
138                     this@DefaultClockFaceController.targetRegion = targetRegion
139                     recomputePadding(targetRegion)
140                 }
141 
142                 override fun onFontSettingChanged(fontSizePx: Float) {
143                     view.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSizePx)
144                     recomputePadding(targetRegion)
145                 }
146 
147                 override fun onSecondaryDisplayChanged(onSecondaryDisplay: Boolean) {
148                     this@DefaultClockController.onSecondaryDisplay = onSecondaryDisplay
149                     recomputePadding(null)
150                 }
151             }
152 
153         open fun recomputePadding(targetRegion: Rect?) {}
154 
155         fun updateColor() {
156             val color =
157                 if (seedColor != null) {
158                     seedColor!!
159                 } else if (isRegionDark) {
160                     resources.getColor(android.R.color.system_accent1_100)
161                 } else {
162                     resources.getColor(android.R.color.system_accent2_600)
163                 }
164 
165             if (currentColor == color) {
166                 return
167             }
168 
169             currentColor = color
170             view.setColors(DOZE_COLOR, color)
171             if (!animations.dozeState.isActive) {
172                 view.animateColorChange()
173             }
174         }
175     }
176 
177     inner class LargeClockFaceController(
178         view: AnimatableClockView,
179         seedColor: Int?,
180     ) : DefaultClockFaceController(view, seedColor) {
181         override val config =
182             ClockFaceConfig(hasCustomPositionUpdatedAnimation = hasStepClockAnimation)
183 
184         init {
185             animations = LargeClockAnimations(view, 0f, 0f)
186         }
187 
188         override fun recomputePadding(targetRegion: Rect?) {
189             // We center the view within the targetRegion instead of within the parent
190             // view by computing the difference and adding that to the padding.
191             val lp = view.getLayoutParams() as FrameLayout.LayoutParams
192             lp.topMargin =
193                 if (onSecondaryDisplay) {
194                     // On the secondary display we don't want any additional top/bottom margin.
195                     0
196                 } else {
197                     val parent = view.parent
198                     val yDiff =
199                         if (targetRegion != null && parent is View && parent.isLaidOut())
200                             targetRegion.centerY() - parent.height / 2f
201                         else 0f
202                     (-0.5f * view.bottom + yDiff).toInt()
203                 }
204             view.setLayoutParams(lp)
205         }
206 
207         /** See documentation at [AnimatableClockView.offsetGlyphsForStepClockAnimation]. */
208         fun offsetGlyphsForStepClockAnimation(fromLeft: Int, direction: Int, fraction: Float) {
209             view.offsetGlyphsForStepClockAnimation(fromLeft, direction, fraction)
210         }
211     }
212 
213     inner class DefaultClockEvents : ClockEvents {
214         override fun onTimeFormatChanged(is24Hr: Boolean) =
215             clocks.forEach { it.refreshFormat(is24Hr) }
216 
217         override fun onTimeZoneChanged(timeZone: TimeZone) =
218             clocks.forEach { it.onTimeZoneChanged(timeZone) }
219 
220         override fun onColorPaletteChanged(resources: Resources) {
221             largeClock.updateColor()
222             smallClock.updateColor()
223         }
224 
225         override fun onSeedColorChanged(seedColor: Int?) {
226             largeClock.seedColor = seedColor
227             smallClock.seedColor = seedColor
228 
229             largeClock.updateColor()
230             smallClock.updateColor()
231         }
232 
233         override fun onLocaleChanged(locale: Locale) {
234             val nf = NumberFormat.getInstance(locale)
235             if (nf.format(FORMAT_NUMBER.toLong()) == burmeseNumerals) {
236                 clocks.forEach { it.setLineSpacingScale(burmeseLineSpacing) }
237             } else {
238                 clocks.forEach { it.setLineSpacingScale(defaultLineSpacing) }
239             }
240 
241             clocks.forEach { it.refreshFormat() }
242         }
243 
244         override fun onWeatherDataChanged(data: WeatherData) {}
245     }
246 
247     open inner class DefaultClockAnimations(
248         val view: AnimatableClockView,
249         dozeFraction: Float,
250         foldFraction: Float,
251     ) : ClockAnimations {
252         internal val dozeState = AnimationState(dozeFraction)
253         private val foldState = AnimationState(foldFraction)
254 
255         init {
256             if (foldState.isActive) {
257                 view.animateFoldAppear(false)
258             } else {
259                 view.animateDoze(dozeState.isActive, false)
260             }
261         }
262 
263         override fun enter() {
264             if (!dozeState.isActive) {
265                 view.animateAppearOnLockscreen()
266             }
267         }
268 
269         override fun charge() = view.animateCharge { dozeState.isActive }
270 
271         override fun fold(fraction: Float) {
272             val (hasChanged, hasJumped) = foldState.update(fraction)
273             if (hasChanged) {
274                 view.animateFoldAppear(!hasJumped)
275             }
276         }
277 
278         override fun doze(fraction: Float) {
279             val (hasChanged, hasJumped) = dozeState.update(fraction)
280             if (hasChanged) {
281                 view.animateDoze(dozeState.isActive, !hasJumped)
282             }
283         }
284 
285         override fun onPickerCarouselSwiping(swipingFraction: Float) {
286             // TODO(b/278936436): refactor this part when we change recomputePadding
287             // when on the side, swipingFraction = 0, translationY should offset
288             // the top margin change in recomputePadding to make clock be centered
289             view.translationY = 0.5f * view.bottom * (1 - swipingFraction)
290         }
291 
292         override fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) {}
293     }
294 
295     inner class LargeClockAnimations(
296         view: AnimatableClockView,
297         dozeFraction: Float,
298         foldFraction: Float,
299     ) : DefaultClockAnimations(view, dozeFraction, foldFraction) {
300         override fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) {
301             largeClock.offsetGlyphsForStepClockAnimation(fromLeft, direction, fraction)
302         }
303     }
304 
305     class AnimationState(
306         var fraction: Float,
307     ) {
308         var isActive: Boolean = fraction > 0.5f
309         fun update(newFraction: Float): Pair<Boolean, Boolean> {
310             if (newFraction == fraction) {
311                 return Pair(isActive, false)
312             }
313             val wasActive = isActive
314             val hasJumped =
315                 (fraction == 0f && newFraction == 1f) || (fraction == 1f && newFraction == 0f)
316             isActive = newFraction > fraction
317             fraction = newFraction
318             return Pair(wasActive != isActive, hasJumped)
319         }
320     }
321 
322     override fun dump(pw: PrintWriter) {
323         pw.print("smallClock=")
324         smallClock.view.dump(pw)
325 
326         pw.print("largeClock=")
327         largeClock.view.dump(pw)
328     }
329 
330     companion object {
331         @VisibleForTesting const val DOZE_COLOR = Color.WHITE
332         private const val FORMAT_NUMBER = 1234567890
333     }
334 }
335