• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.widget.FrameLayout
24 import androidx.annotation.VisibleForTesting
25 import com.android.systemui.customization.R
26 import com.android.systemui.log.core.MessageBuffer
27 import com.android.systemui.plugins.clocks.AlarmData
28 import com.android.systemui.plugins.clocks.ClockAnimations
29 import com.android.systemui.plugins.clocks.ClockAxisStyle
30 import com.android.systemui.plugins.clocks.ClockConfig
31 import com.android.systemui.plugins.clocks.ClockController
32 import com.android.systemui.plugins.clocks.ClockEventListener
33 import com.android.systemui.plugins.clocks.ClockEvents
34 import com.android.systemui.plugins.clocks.ClockFaceConfig
35 import com.android.systemui.plugins.clocks.ClockFaceController
36 import com.android.systemui.plugins.clocks.ClockFaceEvents
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     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 
70     override val events: DefaultClockEvents
<lambda>null71     override val config: ClockConfig by lazy {
72         ClockConfig(
73             DEFAULT_CLOCK_ID,
74             resources.getString(R.string.clock_default_name),
75             resources.getString(R.string.clock_default_description),
76         )
77     }
78 
79     init {
80         val parent = FrameLayout(ctx)
81         smallClock =
82             DefaultClockFaceController(
83                 layoutInflater.inflate(R.layout.clock_default_small, parent, false)
84                     as AnimatableClockView,
85                 settings?.seedColor,
86                 messageBuffers?.smallClockMessageBuffer,
87             )
88         largeClock =
89             LargeClockFaceController(
90                 layoutInflater.inflate(R.layout.clock_default_large, parent, false)
91                     as AnimatableClockView,
92                 settings?.seedColor,
93                 messageBuffers?.largeClockMessageBuffer,
94             )
95         clocks = listOf(smallClock.view, largeClock.view)
96 
97         events = DefaultClockEvents()
98         events.onLocaleChanged(Locale.getDefault())
99     }
100 
initializenull101     override fun initialize(
102         isDarkTheme: Boolean,
103         dozeFraction: Float,
104         foldFraction: Float,
105         clockListener: ClockEventListener?,
106     ) {
107         largeClock.recomputePadding(null)
108 
109         largeClock.animations = LargeClockAnimations(largeClock.view, dozeFraction, foldFraction)
110         smallClock.animations = DefaultClockAnimations(smallClock.view, dozeFraction, foldFraction)
111 
112         largeClock.events.onThemeChanged(largeClock.theme.copy(isDarkTheme = isDarkTheme))
113         smallClock.events.onThemeChanged(smallClock.theme.copy(isDarkTheme = isDarkTheme))
114         events.onTimeZoneChanged(TimeZone.getDefault())
115 
116         smallClock.events.onTimeTick()
117         largeClock.events.onTimeTick()
118     }
119 
120     open inner class DefaultClockFaceController(
121         override val view: AnimatableClockView,
122         seedColor: Int?,
123         messageBuffer: MessageBuffer?,
124     ) : ClockFaceController {
125         // MAGENTA is a placeholder, and will be assigned correctly in initialize
126         private var currentColor = seedColor ?: Color.MAGENTA
127         protected var targetRegion: Rect? = null
128 
129         override val config = ClockFaceConfig()
130         override var theme = ThemeConfig(true, seedColor)
131         override val layout =
<lambda>null132             DefaultClockFaceLayout(view).apply {
133                 views[0].id =
134                     resources.getIdentifier("lockscreen_clock_view", "id", ctx.packageName)
135             }
136 
137         override var animations: DefaultClockAnimations = DefaultClockAnimations(view, 0f, 0f)
138             internal set
139 
140         init {
141             view.setColors(DOZE_COLOR, currentColor)
<lambda>null142             messageBuffer?.let { view.messageBuffer = it }
143         }
144 
145         override val events =
146             object : ClockFaceEvents {
onTimeTicknull147                 override fun onTimeTick() = view.refreshTime()
148 
149                 override fun onThemeChanged(theme: ThemeConfig) {
150                     this@DefaultClockFaceController.theme = theme
151 
152                     val color = theme.getDefaultColor(ctx)
153                     if (currentColor == color) {
154                         return
155                     }
156 
157                     currentColor = color
158                     view.setColors(DOZE_COLOR, color)
159                     if (!animations.dozeState.isActive) {
160                         view.animateColorChange()
161                     }
162                 }
163 
onTargetRegionChangednull164                 override fun onTargetRegionChanged(targetRegion: Rect?) {
165                     this@DefaultClockFaceController.targetRegion = targetRegion
166                     recomputePadding(targetRegion)
167                 }
168 
onFontSettingChangednull169                 override fun onFontSettingChanged(fontSizePx: Float) {
170                     view.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSizePx)
171                     recomputePadding(targetRegion)
172                 }
173 
onSecondaryDisplayChangednull174                 override fun onSecondaryDisplayChanged(onSecondaryDisplay: Boolean) {}
175             }
176 
recomputePaddingnull177         open fun recomputePadding(targetRegion: Rect?) {}
178     }
179 
180     inner class LargeClockFaceController(
181         view: AnimatableClockView,
182         seedColor: Int?,
183         messageBuffer: MessageBuffer?,
184     ) : DefaultClockFaceController(view, seedColor, messageBuffer) {
185         override val layout =
<lambda>null186             DefaultClockFaceLayout(view).apply {
187                 views[0].id =
188                     resources.getIdentifier("lockscreen_clock_view_large", "id", ctx.packageName)
189             }
190         override val config = ClockFaceConfig(hasCustomPositionUpdatedAnimation = true)
191 
192         init {
193             view.hasCustomPositionUpdatedAnimation = true
194             animations = LargeClockAnimations(view, 0f, 0f)
195         }
196 
recomputePaddingnull197         override fun recomputePadding(targetRegion: Rect?) {}
198 
199         /** See documentation at [AnimatableClockView.offsetGlyphsForStepClockAnimation]. */
offsetGlyphsForStepClockAnimationnull200         fun offsetGlyphsForStepClockAnimation(fromLeft: Int, direction: Int, fraction: Float) {
201             view.offsetGlyphsForStepClockAnimation(fromLeft, direction, fraction)
202         }
203 
offsetGlyphsForStepClockAnimationnull204         fun offsetGlyphsForStepClockAnimation(distance: Float, fraction: Float) {
205             view.offsetGlyphsForStepClockAnimation(distance, fraction)
206         }
207     }
208 
209     inner class DefaultClockEvents : ClockEvents {
210         override var isReactiveTouchInteractionEnabled: Boolean = false
211 
onTimeFormatChangednull212         override fun onTimeFormatChanged(is24Hr: Boolean) =
213             clocks.forEach { it.refreshFormat(is24Hr) }
214 
onTimeZoneChangednull215         override fun onTimeZoneChanged(timeZone: TimeZone) =
216             clocks.forEach { it.onTimeZoneChanged(timeZone) }
217 
onLocaleChangednull218         override fun onLocaleChanged(locale: Locale) {
219             val nf = NumberFormat.getInstance(locale)
220             if (nf.format(FORMAT_NUMBER.toLong()) == burmeseNumerals) {
221                 clocks.forEach { it.setLineSpacingScale(burmeseLineSpacing) }
222             } else {
223                 clocks.forEach { it.setLineSpacingScale(defaultLineSpacing) }
224             }
225 
226             clocks.forEach { it.refreshFormat() }
227         }
228 
onWeatherDataChangednull229         override fun onWeatherDataChanged(data: WeatherData) {}
230 
onAlarmDataChangednull231         override fun onAlarmDataChanged(data: AlarmData) {}
232 
onZenDataChangednull233         override fun onZenDataChanged(data: ZenData) {}
234     }
235 
236     open inner class DefaultClockAnimations(
237         val view: AnimatableClockView,
238         dozeFraction: Float,
239         foldFraction: Float,
240     ) : ClockAnimations {
241         internal val dozeState = AnimationState(dozeFraction)
242         private val foldState = AnimationState(foldFraction)
243 
244         init {
245             if (foldState.isActive) {
246                 view.animateFoldAppear(false)
247             } else {
248                 view.animateDoze(dozeState.isActive, false)
249             }
250         }
251 
enternull252         override fun enter() {
253             if (!dozeState.isActive) {
254                 view.animateAppearOnLockscreen()
255             }
256         }
257 
<lambda>null258         override fun charge() = view.animateCharge { dozeState.isActive }
259 
foldnull260         override fun fold(fraction: Float) {
261             val (hasChanged, hasJumped) = foldState.update(fraction)
262             if (hasChanged) {
263                 view.animateFoldAppear(!hasJumped)
264             }
265         }
266 
dozenull267         override fun doze(fraction: Float) {
268             val (hasChanged, hasJumped) = dozeState.update(fraction)
269             if (hasChanged) {
270                 view.animateDoze(dozeState.isActive, !hasJumped)
271             }
272         }
273 
onPickerCarouselSwipingnull274         override fun onPickerCarouselSwiping(swipingFraction: Float) {
275             // TODO(b/278936436): refactor this part when we change recomputePadding
276             // when on the side, swipingFraction = 0, translationY should offset
277             // the top margin change in recomputePadding to make clock be centered
278             view.translationY = 0.5f * view.bottom * (1 - swipingFraction)
279         }
280 
onPositionUpdatednull281         override fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) {}
282 
onPositionUpdatednull283         override fun onPositionUpdated(distance: Float, fraction: Float) {}
284 
onFidgetTapnull285         override fun onFidgetTap(x: Float, y: Float) {}
286 
onFontAxesChangednull287         override fun onFontAxesChanged(style: ClockAxisStyle) {}
288     }
289 
290     inner class LargeClockAnimations(
291         view: AnimatableClockView,
292         dozeFraction: Float,
293         foldFraction: Float,
294     ) : DefaultClockAnimations(view, dozeFraction, foldFraction) {
onPositionUpdatednull295         override fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) {
296             largeClock.offsetGlyphsForStepClockAnimation(fromLeft, direction, fraction)
297         }
298 
onPositionUpdatednull299         override fun onPositionUpdated(distance: Float, fraction: Float) {
300             largeClock.offsetGlyphsForStepClockAnimation(distance, fraction)
301         }
302     }
303 
304     class AnimationState(var fraction: Float) {
305         var isActive: Boolean = fraction > 0.5f
306 
updatenull307         fun update(newFraction: Float): Pair<Boolean, Boolean> {
308             if (newFraction == fraction) {
309                 return Pair(isActive, false)
310             }
311             val wasActive = isActive
312             val hasJumped =
313                 (fraction == 0f && newFraction == 1f) || (fraction == 1f && newFraction == 0f)
314             isActive = newFraction > fraction
315             fraction = newFraction
316             return Pair(wasActive != isActive, hasJumped)
317         }
318     }
319 
dumpnull320     override fun dump(pw: PrintWriter) {
321         pw.print("smallClock=")
322         smallClock.view.dump(pw)
323 
324         pw.print("largeClock=")
325         largeClock.view.dump(pw)
326     }
327 
328     companion object {
329         @VisibleForTesting const val DOZE_COLOR = Color.WHITE
330         private const val FORMAT_NUMBER = 1234567890
331     }
332 }
333