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.ClockFontAxisSetting
37 import com.android.systemui.plugins.clocks.ClockMessageBuffers
38 import com.android.systemui.plugins.clocks.ClockSettings
39 import com.android.systemui.plugins.clocks.DefaultClockFaceLayout
40 import com.android.systemui.plugins.clocks.ThemeConfig
41 import com.android.systemui.plugins.clocks.WeatherData
42 import com.android.systemui.plugins.clocks.ZenData
43 import java.io.PrintWriter
44 import java.util.Locale
45 import java.util.TimeZone
46 
47 /**
48  * Controls the default clock visuals.
49  *
50  * This serves as an adapter between the clock interface and the AnimatableClockView used by the
51  * existing lockscreen clock.
52  */
53 class DefaultClockController(
54     private val ctx: Context,
55     private val layoutInflater: LayoutInflater,
56     private val resources: Resources,
57     private val settings: ClockSettings?,
58     private val migratedClocks: Boolean = false,
59     messageBuffers: ClockMessageBuffers? = null,
60 ) : ClockController {
61     override val smallClock: DefaultClockFaceController
62     override val largeClock: LargeClockFaceController
63     private val clocks: List<AnimatableClockView>
64 
65     private val burmeseNf = NumberFormat.getInstance(Locale.forLanguageTag("my"))
66     private val burmeseNumerals = burmeseNf.format(FORMAT_NUMBER.toLong())
67     private val burmeseLineSpacing =
68         resources.getFloat(R.dimen.keyguard_clock_line_spacing_scale_burmese)
69     private val defaultLineSpacing = resources.getFloat(R.dimen.keyguard_clock_line_spacing_scale)
70     protected var onSecondaryDisplay: Boolean = false
71 
72     override val events: DefaultClockEvents
<lambda>null73     override val config: ClockConfig by lazy {
74         ClockConfig(
75             DEFAULT_CLOCK_ID,
76             resources.getString(R.string.clock_default_name),
77             resources.getString(R.string.clock_default_description),
78         )
79     }
80 
81     init {
82         val parent = FrameLayout(ctx)
83         smallClock =
84             DefaultClockFaceController(
85                 layoutInflater.inflate(R.layout.clock_default_small, parent, false)
86                     as AnimatableClockView,
87                 settings?.seedColor,
88                 messageBuffers?.smallClockMessageBuffer,
89             )
90         largeClock =
91             LargeClockFaceController(
92                 layoutInflater.inflate(R.layout.clock_default_large, parent, false)
93                     as AnimatableClockView,
94                 settings?.seedColor,
95                 messageBuffers?.largeClockMessageBuffer,
96             )
97         clocks = listOf(smallClock.view, largeClock.view)
98 
99         events = DefaultClockEvents()
100         events.onLocaleChanged(Locale.getDefault())
101     }
102 
initializenull103     override fun initialize(isDarkTheme: Boolean, dozeFraction: Float, foldFraction: Float) {
104         largeClock.recomputePadding(null)
105 
106         largeClock.animations = LargeClockAnimations(largeClock.view, dozeFraction, foldFraction)
107         smallClock.animations = DefaultClockAnimations(smallClock.view, dozeFraction, foldFraction)
108 
109         largeClock.events.onThemeChanged(largeClock.theme.copy(isDarkTheme = isDarkTheme))
110         smallClock.events.onThemeChanged(smallClock.theme.copy(isDarkTheme = isDarkTheme))
111         events.onTimeZoneChanged(TimeZone.getDefault())
112 
113         smallClock.events.onTimeTick()
114         largeClock.events.onTimeTick()
115     }
116 
117     open inner class DefaultClockFaceController(
118         override val view: AnimatableClockView,
119         seedColor: Int?,
120         messageBuffer: MessageBuffer?,
121     ) : ClockFaceController {
122         // MAGENTA is a placeholder, and will be assigned correctly in initialize
123         private var currentColor = seedColor ?: Color.MAGENTA
124         protected var targetRegion: Rect? = null
125 
126         override val config = ClockFaceConfig()
127         override var theme = ThemeConfig(true, seedColor)
128         override val layout =
<lambda>null129             DefaultClockFaceLayout(view).apply {
130                 views[0].id =
131                     resources.getIdentifier("lockscreen_clock_view", "id", ctx.packageName)
132             }
133 
134         override var animations: DefaultClockAnimations = DefaultClockAnimations(view, 0f, 0f)
135             internal set
136 
137         init {
138             view.setColors(DOZE_COLOR, currentColor)
<lambda>null139             messageBuffer?.let { view.messageBuffer = it }
140         }
141 
142         override val events =
143             object : ClockFaceEvents {
onTimeTicknull144                 override fun onTimeTick() = view.refreshTime()
145 
146                 override fun onThemeChanged(theme: ThemeConfig) {
147                     this@DefaultClockFaceController.theme = theme
148 
149                     val color =
150                         when {
151                             theme.seedColor != null -> theme.seedColor!!
152                             theme.isDarkTheme ->
153                                 resources.getColor(android.R.color.system_accent1_100)
154                             else -> resources.getColor(android.R.color.system_accent2_600)
155                         }
156 
157                     if (currentColor == color) {
158                         return
159                     }
160 
161                     currentColor = color
162                     view.setColors(DOZE_COLOR, color)
163                     if (!animations.dozeState.isActive) {
164                         view.animateColorChange()
165                     }
166                 }
167 
onTargetRegionChangednull168                 override fun onTargetRegionChanged(targetRegion: Rect?) {
169                     this@DefaultClockFaceController.targetRegion = targetRegion
170                     recomputePadding(targetRegion)
171                 }
172 
onFontSettingChangednull173                 override fun onFontSettingChanged(fontSizePx: Float) {
174                     view.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSizePx)
175                     recomputePadding(targetRegion)
176                 }
177 
onSecondaryDisplayChangednull178                 override fun onSecondaryDisplayChanged(onSecondaryDisplay: Boolean) {
179                     this@DefaultClockController.onSecondaryDisplay = onSecondaryDisplay
180                     recomputePadding(null)
181                 }
182             }
183 
recomputePaddingnull184         open fun recomputePadding(targetRegion: Rect?) {}
185     }
186 
187     inner class LargeClockFaceController(
188         view: AnimatableClockView,
189         seedColor: Int?,
190         messageBuffer: MessageBuffer?,
191     ) : DefaultClockFaceController(view, seedColor, messageBuffer) {
192         override val layout =
<lambda>null193             DefaultClockFaceLayout(view).apply {
194                 views[0].id =
195                     resources.getIdentifier("lockscreen_clock_view_large", "id", ctx.packageName)
196             }
197         override val config = ClockFaceConfig(hasCustomPositionUpdatedAnimation = true)
198 
199         init {
200             view.migratedClocks = migratedClocks
201             view.hasCustomPositionUpdatedAnimation = true
202             animations = LargeClockAnimations(view, 0f, 0f)
203         }
204 
recomputePaddingnull205         override fun recomputePadding(targetRegion: Rect?) {
206             if (migratedClocks) {
207                 return
208             }
209             // We center the view within the targetRegion instead of within the parent
210             // view by computing the difference and adding that to the padding.
211             val lp = view.getLayoutParams() as FrameLayout.LayoutParams
212             lp.topMargin =
213                 if (onSecondaryDisplay) {
214                     // On the secondary display we don't want any additional top/bottom margin.
215                     0
216                 } else {
217                     val parent = view.parent
218                     val yDiff =
219                         if (targetRegion != null && parent is View && parent.isLaidOut())
220                             targetRegion.centerY() - parent.height / 2f
221                         else 0f
222                     (-0.5f * view.bottom + yDiff).toInt()
223                 }
224             view.setLayoutParams(lp)
225         }
226 
227         /** See documentation at [AnimatableClockView.offsetGlyphsForStepClockAnimation]. */
offsetGlyphsForStepClockAnimationnull228         fun offsetGlyphsForStepClockAnimation(fromLeft: Int, direction: Int, fraction: Float) {
229             view.offsetGlyphsForStepClockAnimation(fromLeft, direction, fraction)
230         }
231 
offsetGlyphsForStepClockAnimationnull232         fun offsetGlyphsForStepClockAnimation(distance: Float, fraction: Float) {
233             view.offsetGlyphsForStepClockAnimation(distance, fraction)
234         }
235     }
236 
237     inner class DefaultClockEvents : ClockEvents {
238         override var isReactiveTouchInteractionEnabled: Boolean = false
239 
onTimeFormatChangednull240         override fun onTimeFormatChanged(is24Hr: Boolean) =
241             clocks.forEach { it.refreshFormat(is24Hr) }
242 
onTimeZoneChangednull243         override fun onTimeZoneChanged(timeZone: TimeZone) =
244             clocks.forEach { it.onTimeZoneChanged(timeZone) }
245 
onLocaleChangednull246         override fun onLocaleChanged(locale: Locale) {
247             val nf = NumberFormat.getInstance(locale)
248             if (nf.format(FORMAT_NUMBER.toLong()) == burmeseNumerals) {
249                 clocks.forEach { it.setLineSpacingScale(burmeseLineSpacing) }
250             } else {
251                 clocks.forEach { it.setLineSpacingScale(defaultLineSpacing) }
252             }
253 
254             clocks.forEach { it.refreshFormat() }
255         }
256 
onWeatherDataChangednull257         override fun onWeatherDataChanged(data: WeatherData) {}
258 
onAlarmDataChangednull259         override fun onAlarmDataChanged(data: AlarmData) {}
260 
onZenDataChangednull261         override fun onZenDataChanged(data: ZenData) {}
262 
onFontAxesChangednull263         override fun onFontAxesChanged(axes: List<ClockFontAxisSetting>) {}
264     }
265 
266     open inner class DefaultClockAnimations(
267         val view: AnimatableClockView,
268         dozeFraction: Float,
269         foldFraction: Float,
270     ) : ClockAnimations {
271         internal val dozeState = AnimationState(dozeFraction)
272         private val foldState = AnimationState(foldFraction)
273 
274         init {
275             if (foldState.isActive) {
276                 view.animateFoldAppear(false)
277             } else {
278                 view.animateDoze(dozeState.isActive, false)
279             }
280         }
281 
enternull282         override fun enter() {
283             if (!dozeState.isActive) {
284                 view.animateAppearOnLockscreen()
285             }
286         }
287 
<lambda>null288         override fun charge() = view.animateCharge { dozeState.isActive }
289 
foldnull290         override fun fold(fraction: Float) {
291             val (hasChanged, hasJumped) = foldState.update(fraction)
292             if (hasChanged) {
293                 view.animateFoldAppear(!hasJumped)
294             }
295         }
296 
dozenull297         override fun doze(fraction: Float) {
298             val (hasChanged, hasJumped) = dozeState.update(fraction)
299             if (hasChanged) {
300                 view.animateDoze(dozeState.isActive, !hasJumped)
301             }
302         }
303 
onPickerCarouselSwipingnull304         override fun onPickerCarouselSwiping(swipingFraction: Float) {
305             // TODO(b/278936436): refactor this part when we change recomputePadding
306             // when on the side, swipingFraction = 0, translationY should offset
307             // the top margin change in recomputePadding to make clock be centered
308             view.translationY = 0.5f * view.bottom * (1 - swipingFraction)
309         }
310 
onPositionUpdatednull311         override fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) {}
312 
onPositionUpdatednull313         override fun onPositionUpdated(distance: Float, fraction: Float) {}
314     }
315 
316     inner class LargeClockAnimations(
317         view: AnimatableClockView,
318         dozeFraction: Float,
319         foldFraction: Float,
320     ) : DefaultClockAnimations(view, dozeFraction, foldFraction) {
onPositionUpdatednull321         override fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) {
322             largeClock.offsetGlyphsForStepClockAnimation(fromLeft, direction, fraction)
323         }
324 
onPositionUpdatednull325         override fun onPositionUpdated(distance: Float, fraction: Float) {
326             largeClock.offsetGlyphsForStepClockAnimation(distance, fraction)
327         }
328     }
329 
330     class AnimationState(var fraction: Float) {
331         var isActive: Boolean = fraction > 0.5f
332 
updatenull333         fun update(newFraction: Float): Pair<Boolean, Boolean> {
334             if (newFraction == fraction) {
335                 return Pair(isActive, false)
336             }
337             val wasActive = isActive
338             val hasJumped =
339                 (fraction == 0f && newFraction == 1f) || (fraction == 1f && newFraction == 0f)
340             isActive = newFraction > fraction
341             fraction = newFraction
342             return Pair(wasActive != isActive, hasJumped)
343         }
344     }
345 
dumpnull346     override fun dump(pw: PrintWriter) {
347         pw.print("smallClock=")
348         smallClock.view.dump(pw)
349 
350         pw.print("largeClock=")
351         largeClock.view.dump(pw)
352     }
353 
354     companion object {
355         @VisibleForTesting const val DOZE_COLOR = Color.WHITE
356         private const val FORMAT_NUMBER = 1234567890
357     }
358 }
359