• 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.view.View
24 import android.widget.FrameLayout
25 import androidx.annotation.VisibleForTesting
26 import com.android.systemui.customization.R
27 import com.android.systemui.plugins.ClockAnimations
28 import com.android.systemui.plugins.ClockController
29 import com.android.systemui.plugins.ClockEvents
30 import com.android.systemui.plugins.ClockFaceController
31 import com.android.systemui.plugins.ClockFaceEvents
32 import com.android.systemui.plugins.ClockSettings
33 import com.android.systemui.plugins.log.LogBuffer
34 import java.io.PrintWriter
35 import java.util.Locale
36 import java.util.TimeZone
37 
38 private val TAG = DefaultClockController::class.simpleName
39 
40 /**
41  * Controls the default clock visuals.
42  *
43  * This serves as an adapter between the clock interface and the AnimatableClockView used by the
44  * existing lockscreen clock.
45  */
46 class DefaultClockController(
47     ctx: Context,
48     private val layoutInflater: LayoutInflater,
49     private val resources: Resources,
50     private val settings: ClockSettings?,
51 ) : ClockController {
52     override val smallClock: DefaultClockFaceController
53     override val largeClock: LargeClockFaceController
54     private val clocks: List<AnimatableClockView>
55 
56     private val burmeseNf = NumberFormat.getInstance(Locale.forLanguageTag("my"))
57     private val burmeseNumerals = burmeseNf.format(FORMAT_NUMBER.toLong())
58     private val burmeseLineSpacing =
59         resources.getFloat(R.dimen.keyguard_clock_line_spacing_scale_burmese)
60     private val defaultLineSpacing = resources.getFloat(R.dimen.keyguard_clock_line_spacing_scale)
61 
62     override val events: DefaultClockEvents
63     override lateinit var animations: DefaultClockAnimations
64         private set
65 
66     init {
67         val parent = FrameLayout(ctx)
68         smallClock =
69             DefaultClockFaceController(
70                 layoutInflater.inflate(R.layout.clock_default_small, parent, false)
71                     as AnimatableClockView,
72                 settings?.seedColor
73             )
74         largeClock =
75             LargeClockFaceController(
76                 layoutInflater.inflate(R.layout.clock_default_large, parent, false)
77                     as AnimatableClockView,
78                 settings?.seedColor
79             )
80         clocks = listOf(smallClock.view, largeClock.view)
81 
82         events = DefaultClockEvents()
83         animations = DefaultClockAnimations(0f, 0f)
84         events.onLocaleChanged(Locale.getDefault())
85     }
86 
initializenull87     override fun initialize(resources: Resources, dozeFraction: Float, foldFraction: Float) {
88         largeClock.recomputePadding(null)
89         animations = DefaultClockAnimations(dozeFraction, foldFraction)
90         events.onColorPaletteChanged(resources)
91         events.onTimeZoneChanged(TimeZone.getDefault())
92         smallClock.events.onTimeTick()
93         largeClock.events.onTimeTick()
94     }
95 
96     open inner class DefaultClockFaceController(
97         override val view: AnimatableClockView,
98         var seedColor: Int?,
99     ) : ClockFaceController {
100 
101         // MAGENTA is a placeholder, and will be assigned correctly in initialize
102         private var currentColor = Color.MAGENTA
103         private var isRegionDark = false
104         protected var targetRegion: Rect? = null
105 
106         override var logBuffer: LogBuffer?
107             get() = view.logBuffer
108             set(value) {
109                 view.logBuffer = value
110             }
111 
112         init {
113             if (seedColor != null) {
114                 currentColor = seedColor!!
115             }
116             view.setColors(DOZE_COLOR, currentColor)
117         }
118 
119         override val events =
120             object : ClockFaceEvents {
onTimeTicknull121                 override fun onTimeTick() = view.refreshTime()
122 
123                 override fun onRegionDarknessChanged(isRegionDark: Boolean) {
124                     this@DefaultClockFaceController.isRegionDark = isRegionDark
125                     updateColor()
126                 }
127 
onTargetRegionChangednull128                 override fun onTargetRegionChanged(targetRegion: Rect?) {
129                     this@DefaultClockFaceController.targetRegion = targetRegion
130                     recomputePadding(targetRegion)
131                 }
132 
onFontSettingChangednull133                 override fun onFontSettingChanged(fontSizePx: Float) {
134                     view.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSizePx)
135                     recomputePadding(targetRegion)
136                 }
137             }
138 
recomputePaddingnull139         open fun recomputePadding(targetRegion: Rect?) {}
140 
updateColornull141         fun updateColor() {
142             val color =
143                 if (seedColor != null) {
144                     seedColor!!
145                 } else if (isRegionDark) {
146                     resources.getColor(android.R.color.system_accent1_100)
147                 } else {
148                     resources.getColor(android.R.color.system_accent2_600)
149                 }
150 
151             if (currentColor == color) {
152                 return
153             }
154 
155             currentColor = color
156             view.setColors(DOZE_COLOR, color)
157             if (!animations.dozeState.isActive) {
158                 view.animateColorChange()
159             }
160         }
161     }
162 
163     inner class LargeClockFaceController(
164         view: AnimatableClockView,
165         seedColor: Int?,
166     ) : DefaultClockFaceController(view, seedColor) {
recomputePaddingnull167         override fun recomputePadding(targetRegion: Rect?) {
168             // We center the view within the targetRegion instead of within the parent
169             // view by computing the difference and adding that to the padding.
170             val parent = view.parent
171             val yDiff =
172                 if (targetRegion != null && parent is View && parent.isLaidOut())
173                     targetRegion.centerY() - parent.height / 2f
174                 else 0f
175             val lp = view.getLayoutParams() as FrameLayout.LayoutParams
176             lp.topMargin = (-0.5f * view.bottom + yDiff).toInt()
177             view.setLayoutParams(lp)
178         }
179 
moveForSplitShadenull180         fun moveForSplitShade(fromRect: Rect, toRect: Rect, fraction: Float) {
181             view.moveForSplitShade(fromRect, toRect, fraction)
182         }
183     }
184 
185     inner class DefaultClockEvents : ClockEvents {
onTimeFormatChangednull186         override fun onTimeFormatChanged(is24Hr: Boolean) =
187             clocks.forEach { it.refreshFormat(is24Hr) }
188 
onTimeZoneChangednull189         override fun onTimeZoneChanged(timeZone: TimeZone) =
190             clocks.forEach { it.onTimeZoneChanged(timeZone) }
191 
onColorPaletteChangednull192         override fun onColorPaletteChanged(resources: Resources) {
193             largeClock.updateColor()
194             smallClock.updateColor()
195         }
196 
onSeedColorChangednull197         override fun onSeedColorChanged(seedColor: Int?) {
198             largeClock.seedColor = seedColor
199             smallClock.seedColor = seedColor
200 
201             largeClock.updateColor()
202             smallClock.updateColor()
203         }
204 
onLocaleChangednull205         override fun onLocaleChanged(locale: Locale) {
206             val nf = NumberFormat.getInstance(locale)
207             if (nf.format(FORMAT_NUMBER.toLong()) == burmeseNumerals) {
208                 clocks.forEach { it.setLineSpacingScale(burmeseLineSpacing) }
209             } else {
210                 clocks.forEach { it.setLineSpacingScale(defaultLineSpacing) }
211             }
212 
213             clocks.forEach { it.refreshFormat() }
214         }
215     }
216 
217     inner class DefaultClockAnimations(
218         dozeFraction: Float,
219         foldFraction: Float,
220     ) : ClockAnimations {
221         internal val dozeState = AnimationState(dozeFraction)
222         private val foldState = AnimationState(foldFraction)
223 
224         init {
225             if (foldState.isActive) {
<lambda>null226                 clocks.forEach { it.animateFoldAppear(false) }
227             } else {
<lambda>null228                 clocks.forEach { it.animateDoze(dozeState.isActive, false) }
229             }
230         }
231 
enternull232         override fun enter() {
233             if (!dozeState.isActive) {
234                 clocks.forEach { it.animateAppearOnLockscreen() }
235             }
236         }
237 
<lambda>null238         override fun charge() = clocks.forEach { it.animateCharge { dozeState.isActive } }
239 
foldnull240         override fun fold(fraction: Float) {
241             val (hasChanged, hasJumped) = foldState.update(fraction)
242             if (hasChanged) {
243                 clocks.forEach { it.animateFoldAppear(!hasJumped) }
244             }
245         }
246 
dozenull247         override fun doze(fraction: Float) {
248             val (hasChanged, hasJumped) = dozeState.update(fraction)
249             if (hasChanged) {
250                 clocks.forEach { it.animateDoze(dozeState.isActive, !hasJumped) }
251             }
252         }
253 
onPositionUpdatednull254         override fun onPositionUpdated(fromRect: Rect, toRect: Rect, fraction: Float) {
255             largeClock.moveForSplitShade(fromRect, toRect, fraction)
256         }
257 
258         override val hasCustomPositionUpdatedAnimation: Boolean
259             get() = true
260     }
261 
262     class AnimationState(
263         var fraction: Float,
264     ) {
265         var isActive: Boolean = fraction > 0.5f
updatenull266         fun update(newFraction: Float): Pair<Boolean, Boolean> {
267             if (newFraction == fraction) {
268                 return Pair(isActive, false)
269             }
270             val wasActive = isActive
271             val hasJumped =
272                 (fraction == 0f && newFraction == 1f) || (fraction == 1f && newFraction == 0f)
273             isActive = newFraction > fraction
274             fraction = newFraction
275             return Pair(wasActive != isActive, hasJumped)
276         }
277     }
278 
dumpnull279     override fun dump(pw: PrintWriter) {
280         pw.print("smallClock=")
281         smallClock.view.dump(pw)
282 
283         pw.print("largeClock=")
284         largeClock.view.dump(pw)
285     }
286 
287     companion object {
288         @VisibleForTesting const val DOZE_COLOR = Color.WHITE
289         private const val FORMAT_NUMBER = 1234567890
290     }
291 }
292