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