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.clocks.AlarmData
29 import com.android.systemui.plugins.clocks.ClockAnimations
30 import com.android.systemui.plugins.clocks.ClockConfig
31 import com.android.systemui.plugins.clocks.ClockController
32 import com.android.systemui.plugins.clocks.ClockEvents
33 import com.android.systemui.plugins.clocks.ClockFaceConfig
34 import com.android.systemui.plugins.clocks.ClockFaceController
35 import com.android.systemui.plugins.clocks.ClockFaceEvents
36 import com.android.systemui.plugins.clocks.ClockMessageBuffers
37 import com.android.systemui.plugins.clocks.ClockSettings
38 import com.android.systemui.plugins.clocks.DefaultClockFaceLayout
39 import com.android.systemui.plugins.clocks.WeatherData
40 import com.android.systemui.plugins.clocks.ZenData
41 import java.io.PrintWriter
42 import java.util.Locale
43 import java.util.TimeZone
44 
45 /**
46  * Controls the default clock visuals.
47  *
48  * This serves as an adapter between the clock interface and the AnimatableClockView used by the
49  * existing lockscreen clock.
50  */
51 class DefaultClockController(
52     private val ctx: Context,
53     private val layoutInflater: LayoutInflater,
54     private val resources: Resources,
55     private val settings: ClockSettings?,
56     private val hasStepClockAnimation: Boolean = false,
57     private val migratedClocks: Boolean = false,
58     messageBuffers: ClockMessageBuffers? = null,
59 ) : ClockController {
60     override val smallClock: DefaultClockFaceController
61     override val largeClock: LargeClockFaceController
62     private val clocks: List<AnimatableClockView>
63 
64     private val burmeseNf = NumberFormat.getInstance(Locale.forLanguageTag("my"))
65     private val burmeseNumerals = burmeseNf.format(FORMAT_NUMBER.toLong())
66     private val burmeseLineSpacing =
67         resources.getFloat(R.dimen.keyguard_clock_line_spacing_scale_burmese)
68     private val defaultLineSpacing = resources.getFloat(R.dimen.keyguard_clock_line_spacing_scale)
69     protected var onSecondaryDisplay: Boolean = false
70 
71     override val events: DefaultClockEvents
<lambda>null72     override val config: ClockConfig by lazy {
73         ClockConfig(
74             DEFAULT_CLOCK_ID,
75             resources.getString(R.string.clock_default_name),
76             resources.getString(R.string.clock_default_description)
77         )
78     }
79 
80     init {
81         val parent = FrameLayout(ctx)
82         smallClock =
83             DefaultClockFaceController(
84                 layoutInflater.inflate(R.layout.clock_default_small, parent, false)
85                     as AnimatableClockView,
86                 settings?.seedColor,
87                 messageBuffers?.smallClockMessageBuffer
88             )
89         largeClock =
90             LargeClockFaceController(
91                 layoutInflater.inflate(R.layout.clock_default_large, parent, false)
92                     as AnimatableClockView,
93                 settings?.seedColor,
94                 messageBuffers?.largeClockMessageBuffer
95             )
96         clocks = listOf(smallClock.view, largeClock.view)
97 
98         events = DefaultClockEvents()
99         events.onLocaleChanged(Locale.getDefault())
100     }
101 
initializenull102     override fun initialize(resources: Resources, dozeFraction: Float, foldFraction: Float) {
103         largeClock.recomputePadding(null)
104         largeClock.animations = LargeClockAnimations(largeClock.view, dozeFraction, foldFraction)
105         smallClock.animations = DefaultClockAnimations(smallClock.view, dozeFraction, foldFraction)
106         events.onColorPaletteChanged(resources)
107         events.onTimeZoneChanged(TimeZone.getDefault())
108         smallClock.events.onTimeTick()
109         largeClock.events.onTimeTick()
110     }
111 
112     open inner class DefaultClockFaceController(
113         override val view: AnimatableClockView,
114         var seedColor: Int?,
115         messageBuffer: MessageBuffer?,
116     ) : ClockFaceController {
117 
118         // MAGENTA is a placeholder, and will be assigned correctly in initialize
119         private var currentColor = Color.MAGENTA
120         private var isRegionDark = false
121         protected var targetRegion: Rect? = null
122 
123         override val config = ClockFaceConfig()
124         override val layout =
<lambda>null125             DefaultClockFaceLayout(view).apply {
126                 views[0].id =
127                     resources.getIdentifier("lockscreen_clock_view", "id", ctx.packageName)
128             }
129 
130         override var animations: DefaultClockAnimations = DefaultClockAnimations(view, 0f, 0f)
131             internal set
132 
133         init {
134             if (seedColor != null) {
135                 currentColor = seedColor!!
136             }
137             view.setColors(DOZE_COLOR, currentColor)
<lambda>null138             messageBuffer?.let { view.messageBuffer = it }
139         }
140 
141         override val events =
142             object : ClockFaceEvents {
onTimeTicknull143                 override fun onTimeTick() = view.refreshTime()
144 
145                 override fun onRegionDarknessChanged(isRegionDark: Boolean) {
146                     this@DefaultClockFaceController.isRegionDark = isRegionDark
147                     updateColor()
148                 }
149 
onTargetRegionChangednull150                 override fun onTargetRegionChanged(targetRegion: Rect?) {
151                     this@DefaultClockFaceController.targetRegion = targetRegion
152                     recomputePadding(targetRegion)
153                 }
154 
onFontSettingChangednull155                 override fun onFontSettingChanged(fontSizePx: Float) {
156                     view.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSizePx)
157                     recomputePadding(targetRegion)
158                 }
159 
onSecondaryDisplayChangednull160                 override fun onSecondaryDisplayChanged(onSecondaryDisplay: Boolean) {
161                     this@DefaultClockController.onSecondaryDisplay = onSecondaryDisplay
162                     recomputePadding(null)
163                 }
164             }
165 
recomputePaddingnull166         open fun recomputePadding(targetRegion: Rect?) {}
167 
updateColornull168         fun updateColor() {
169             val color =
170                 if (seedColor != null) {
171                     seedColor!!
172                 } else if (isRegionDark) {
173                     resources.getColor(android.R.color.system_accent1_100)
174                 } else {
175                     resources.getColor(android.R.color.system_accent2_600)
176                 }
177 
178             if (currentColor == color) {
179                 return
180             }
181 
182             currentColor = color
183             view.setColors(DOZE_COLOR, color)
184             if (!animations.dozeState.isActive) {
185                 view.animateColorChange()
186             }
187         }
188     }
189 
190     inner class LargeClockFaceController(
191         view: AnimatableClockView,
192         seedColor: Int?,
193         messageBuffer: MessageBuffer?,
194     ) : DefaultClockFaceController(view, seedColor, messageBuffer) {
195         override val layout =
<lambda>null196             DefaultClockFaceLayout(view).apply {
197                 views[0].id =
198                     resources.getIdentifier("lockscreen_clock_view_large", "id", ctx.packageName)
199             }
200         override val config =
201             ClockFaceConfig(hasCustomPositionUpdatedAnimation = hasStepClockAnimation)
202 
203         init {
204             view.migratedClocks = migratedClocks
205             view.hasCustomPositionUpdatedAnimation = hasStepClockAnimation
206             animations = LargeClockAnimations(view, 0f, 0f)
207         }
208 
recomputePaddingnull209         override fun recomputePadding(targetRegion: Rect?) {
210             if (migratedClocks) {
211                 return
212             }
213             // We center the view within the targetRegion instead of within the parent
214             // view by computing the difference and adding that to the padding.
215             val lp = view.getLayoutParams() as FrameLayout.LayoutParams
216             lp.topMargin =
217                 if (onSecondaryDisplay) {
218                     // On the secondary display we don't want any additional top/bottom margin.
219                     0
220                 } else {
221                     val parent = view.parent
222                     val yDiff =
223                         if (targetRegion != null && parent is View && parent.isLaidOut())
224                             targetRegion.centerY() - parent.height / 2f
225                         else 0f
226                     (-0.5f * view.bottom + yDiff).toInt()
227                 }
228             view.setLayoutParams(lp)
229         }
230 
231         /** See documentation at [AnimatableClockView.offsetGlyphsForStepClockAnimation]. */
offsetGlyphsForStepClockAnimationnull232         fun offsetGlyphsForStepClockAnimation(fromLeft: Int, direction: Int, fraction: Float) {
233             view.offsetGlyphsForStepClockAnimation(fromLeft, direction, fraction)
234         }
235 
offsetGlyphsForStepClockAnimationnull236         fun offsetGlyphsForStepClockAnimation(distance: Float, fraction: Float) {
237             view.offsetGlyphsForStepClockAnimation(distance, fraction)
238         }
239     }
240 
241     inner class DefaultClockEvents : ClockEvents {
242         override var isReactiveTouchInteractionEnabled: Boolean = false
243 
onTimeFormatChangednull244         override fun onTimeFormatChanged(is24Hr: Boolean) =
245             clocks.forEach { it.refreshFormat(is24Hr) }
246 
onTimeZoneChangednull247         override fun onTimeZoneChanged(timeZone: TimeZone) =
248             clocks.forEach { it.onTimeZoneChanged(timeZone) }
249 
onColorPaletteChangednull250         override fun onColorPaletteChanged(resources: Resources) {
251             largeClock.updateColor()
252             smallClock.updateColor()
253         }
254 
onSeedColorChangednull255         override fun onSeedColorChanged(seedColor: Int?) {
256             largeClock.seedColor = seedColor
257             smallClock.seedColor = seedColor
258 
259             largeClock.updateColor()
260             smallClock.updateColor()
261         }
262 
onLocaleChangednull263         override fun onLocaleChanged(locale: Locale) {
264             val nf = NumberFormat.getInstance(locale)
265             if (nf.format(FORMAT_NUMBER.toLong()) == burmeseNumerals) {
266                 clocks.forEach { it.setLineSpacingScale(burmeseLineSpacing) }
267             } else {
268                 clocks.forEach { it.setLineSpacingScale(defaultLineSpacing) }
269             }
270 
271             clocks.forEach { it.refreshFormat() }
272         }
273 
onWeatherDataChangednull274         override fun onWeatherDataChanged(data: WeatherData) {}
onAlarmDataChangednull275         override fun onAlarmDataChanged(data: AlarmData) {}
onZenDataChangednull276         override fun onZenDataChanged(data: ZenData) {}
277     }
278 
279     open inner class DefaultClockAnimations(
280         val view: AnimatableClockView,
281         dozeFraction: Float,
282         foldFraction: Float,
283     ) : ClockAnimations {
284         internal val dozeState = AnimationState(dozeFraction)
285         private val foldState = AnimationState(foldFraction)
286 
287         init {
288             if (foldState.isActive) {
289                 view.animateFoldAppear(false)
290             } else {
291                 view.animateDoze(dozeState.isActive, false)
292             }
293         }
294 
enternull295         override fun enter() {
296             if (!dozeState.isActive) {
297                 view.animateAppearOnLockscreen()
298             }
299         }
300 
<lambda>null301         override fun charge() = view.animateCharge { dozeState.isActive }
302 
foldnull303         override fun fold(fraction: Float) {
304             val (hasChanged, hasJumped) = foldState.update(fraction)
305             if (hasChanged) {
306                 view.animateFoldAppear(!hasJumped)
307             }
308         }
309 
dozenull310         override fun doze(fraction: Float) {
311             val (hasChanged, hasJumped) = dozeState.update(fraction)
312             if (hasChanged) {
313                 view.animateDoze(dozeState.isActive, !hasJumped)
314             }
315         }
316 
onPickerCarouselSwipingnull317         override fun onPickerCarouselSwiping(swipingFraction: Float) {
318             // TODO(b/278936436): refactor this part when we change recomputePadding
319             // when on the side, swipingFraction = 0, translationY should offset
320             // the top margin change in recomputePadding to make clock be centered
321             view.translationY = 0.5f * view.bottom * (1 - swipingFraction)
322         }
323 
onPositionUpdatednull324         override fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) {}
325 
onPositionUpdatednull326         override fun onPositionUpdated(distance: Float, fraction: Float) {}
327     }
328 
329     inner class LargeClockAnimations(
330         view: AnimatableClockView,
331         dozeFraction: Float,
332         foldFraction: Float,
333     ) : DefaultClockAnimations(view, dozeFraction, foldFraction) {
onPositionUpdatednull334         override fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) {
335             largeClock.offsetGlyphsForStepClockAnimation(fromLeft, direction, fraction)
336         }
337 
onPositionUpdatednull338         override fun onPositionUpdated(distance: Float, fraction: Float) {
339             largeClock.offsetGlyphsForStepClockAnimation(distance, fraction)
340         }
341     }
342 
343     class AnimationState(
344         var fraction: Float,
345     ) {
346         var isActive: Boolean = fraction > 0.5f
updatenull347         fun update(newFraction: Float): Pair<Boolean, Boolean> {
348             if (newFraction == fraction) {
349                 return Pair(isActive, false)
350             }
351             val wasActive = isActive
352             val hasJumped =
353                 (fraction == 0f && newFraction == 1f) || (fraction == 1f && newFraction == 0f)
354             isActive = newFraction > fraction
355             fraction = newFraction
356             return Pair(wasActive != isActive, hasJumped)
357         }
358     }
359 
dumpnull360     override fun dump(pw: PrintWriter) {
361         pw.print("smallClock=")
362         smallClock.view.dump(pw)
363 
364         pw.print("largeClock=")
365         largeClock.view.dump(pw)
366     }
367 
368     companion object {
369         @VisibleForTesting const val DOZE_COLOR = Color.WHITE
370         private const val FORMAT_NUMBER = 1234567890
371     }
372 }
373