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