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