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.clocks.AlarmData 29 import com.android.systemui.plugins.clocks.ClockAnimations 30 import com.android.systemui.plugins.clocks.ClockConfig 31 import com.android.systemui.plugins.clocks.ClockController 32 import com.android.systemui.plugins.clocks.ClockEvents 33 import com.android.systemui.plugins.clocks.ClockFaceConfig 34 import com.android.systemui.plugins.clocks.ClockFaceController 35 import com.android.systemui.plugins.clocks.ClockFaceEvents 36 import com.android.systemui.plugins.clocks.ClockMessageBuffers 37 import com.android.systemui.plugins.clocks.ClockSettings 38 import com.android.systemui.plugins.clocks.DefaultClockFaceLayout 39 import com.android.systemui.plugins.clocks.WeatherData 40 import com.android.systemui.plugins.clocks.ZenData 41 import java.io.PrintWriter 42 import java.util.Locale 43 import java.util.TimeZone 44 45 /** 46 * Controls the default clock visuals. 47 * 48 * This serves as an adapter between the clock interface and the AnimatableClockView used by the 49 * existing lockscreen clock. 50 */ 51 class DefaultClockController( 52 private val ctx: Context, 53 private val layoutInflater: LayoutInflater, 54 private val resources: Resources, 55 private val settings: ClockSettings?, 56 private val hasStepClockAnimation: Boolean = false, 57 private val migratedClocks: Boolean = false, 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 protected var onSecondaryDisplay: Boolean = false 70 71 override val events: DefaultClockEvents <lambda>null72 override val config: ClockConfig by lazy { 73 ClockConfig( 74 DEFAULT_CLOCK_ID, 75 resources.getString(R.string.clock_default_name), 76 resources.getString(R.string.clock_default_description) 77 ) 78 } 79 80 init { 81 val parent = FrameLayout(ctx) 82 smallClock = 83 DefaultClockFaceController( 84 layoutInflater.inflate(R.layout.clock_default_small, parent, false) 85 as AnimatableClockView, 86 settings?.seedColor, 87 messageBuffers?.smallClockMessageBuffer 88 ) 89 largeClock = 90 LargeClockFaceController( 91 layoutInflater.inflate(R.layout.clock_default_large, parent, false) 92 as AnimatableClockView, 93 settings?.seedColor, 94 messageBuffers?.largeClockMessageBuffer 95 ) 96 clocks = listOf(smallClock.view, largeClock.view) 97 98 events = DefaultClockEvents() 99 events.onLocaleChanged(Locale.getDefault()) 100 } 101 initializenull102 override fun initialize(resources: Resources, dozeFraction: Float, foldFraction: Float) { 103 largeClock.recomputePadding(null) 104 largeClock.animations = LargeClockAnimations(largeClock.view, dozeFraction, foldFraction) 105 smallClock.animations = DefaultClockAnimations(smallClock.view, dozeFraction, foldFraction) 106 events.onColorPaletteChanged(resources) 107 events.onTimeZoneChanged(TimeZone.getDefault()) 108 smallClock.events.onTimeTick() 109 largeClock.events.onTimeTick() 110 } 111 112 open inner class DefaultClockFaceController( 113 override val view: AnimatableClockView, 114 var seedColor: Int?, 115 messageBuffer: MessageBuffer?, 116 ) : ClockFaceController { 117 118 // MAGENTA is a placeholder, and will be assigned correctly in initialize 119 private var currentColor = Color.MAGENTA 120 private var isRegionDark = false 121 protected var targetRegion: Rect? = null 122 123 override val config = ClockFaceConfig() 124 override val layout = <lambda>null125 DefaultClockFaceLayout(view).apply { 126 views[0].id = 127 resources.getIdentifier("lockscreen_clock_view", "id", ctx.packageName) 128 } 129 130 override var animations: DefaultClockAnimations = DefaultClockAnimations(view, 0f, 0f) 131 internal set 132 133 init { 134 if (seedColor != null) { 135 currentColor = seedColor!! 136 } 137 view.setColors(DOZE_COLOR, currentColor) <lambda>null138 messageBuffer?.let { view.messageBuffer = it } 139 } 140 141 override val events = 142 object : ClockFaceEvents { onTimeTicknull143 override fun onTimeTick() = view.refreshTime() 144 145 override fun onRegionDarknessChanged(isRegionDark: Boolean) { 146 this@DefaultClockFaceController.isRegionDark = isRegionDark 147 updateColor() 148 } 149 onTargetRegionChangednull150 override fun onTargetRegionChanged(targetRegion: Rect?) { 151 this@DefaultClockFaceController.targetRegion = targetRegion 152 recomputePadding(targetRegion) 153 } 154 onFontSettingChangednull155 override fun onFontSettingChanged(fontSizePx: Float) { 156 view.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSizePx) 157 recomputePadding(targetRegion) 158 } 159 onSecondaryDisplayChangednull160 override fun onSecondaryDisplayChanged(onSecondaryDisplay: Boolean) { 161 this@DefaultClockController.onSecondaryDisplay = onSecondaryDisplay 162 recomputePadding(null) 163 } 164 } 165 recomputePaddingnull166 open fun recomputePadding(targetRegion: Rect?) {} 167 updateColornull168 fun updateColor() { 169 val color = 170 if (seedColor != null) { 171 seedColor!! 172 } else if (isRegionDark) { 173 resources.getColor(android.R.color.system_accent1_100) 174 } else { 175 resources.getColor(android.R.color.system_accent2_600) 176 } 177 178 if (currentColor == color) { 179 return 180 } 181 182 currentColor = color 183 view.setColors(DOZE_COLOR, color) 184 if (!animations.dozeState.isActive) { 185 view.animateColorChange() 186 } 187 } 188 } 189 190 inner class LargeClockFaceController( 191 view: AnimatableClockView, 192 seedColor: Int?, 193 messageBuffer: MessageBuffer?, 194 ) : DefaultClockFaceController(view, seedColor, messageBuffer) { 195 override val layout = <lambda>null196 DefaultClockFaceLayout(view).apply { 197 views[0].id = 198 resources.getIdentifier("lockscreen_clock_view_large", "id", ctx.packageName) 199 } 200 override val config = 201 ClockFaceConfig(hasCustomPositionUpdatedAnimation = hasStepClockAnimation) 202 203 init { 204 view.migratedClocks = migratedClocks 205 view.hasCustomPositionUpdatedAnimation = hasStepClockAnimation 206 animations = LargeClockAnimations(view, 0f, 0f) 207 } 208 recomputePaddingnull209 override fun recomputePadding(targetRegion: Rect?) { 210 if (migratedClocks) { 211 return 212 } 213 // We center the view within the targetRegion instead of within the parent 214 // view by computing the difference and adding that to the padding. 215 val lp = view.getLayoutParams() as FrameLayout.LayoutParams 216 lp.topMargin = 217 if (onSecondaryDisplay) { 218 // On the secondary display we don't want any additional top/bottom margin. 219 0 220 } else { 221 val parent = view.parent 222 val yDiff = 223 if (targetRegion != null && parent is View && parent.isLaidOut()) 224 targetRegion.centerY() - parent.height / 2f 225 else 0f 226 (-0.5f * view.bottom + yDiff).toInt() 227 } 228 view.setLayoutParams(lp) 229 } 230 231 /** See documentation at [AnimatableClockView.offsetGlyphsForStepClockAnimation]. */ offsetGlyphsForStepClockAnimationnull232 fun offsetGlyphsForStepClockAnimation(fromLeft: Int, direction: Int, fraction: Float) { 233 view.offsetGlyphsForStepClockAnimation(fromLeft, direction, fraction) 234 } 235 offsetGlyphsForStepClockAnimationnull236 fun offsetGlyphsForStepClockAnimation(distance: Float, fraction: Float) { 237 view.offsetGlyphsForStepClockAnimation(distance, fraction) 238 } 239 } 240 241 inner class DefaultClockEvents : ClockEvents { 242 override var isReactiveTouchInteractionEnabled: Boolean = false 243 onTimeFormatChangednull244 override fun onTimeFormatChanged(is24Hr: Boolean) = 245 clocks.forEach { it.refreshFormat(is24Hr) } 246 onTimeZoneChangednull247 override fun onTimeZoneChanged(timeZone: TimeZone) = 248 clocks.forEach { it.onTimeZoneChanged(timeZone) } 249 onColorPaletteChangednull250 override fun onColorPaletteChanged(resources: Resources) { 251 largeClock.updateColor() 252 smallClock.updateColor() 253 } 254 onSeedColorChangednull255 override fun onSeedColorChanged(seedColor: Int?) { 256 largeClock.seedColor = seedColor 257 smallClock.seedColor = seedColor 258 259 largeClock.updateColor() 260 smallClock.updateColor() 261 } 262 onLocaleChangednull263 override fun onLocaleChanged(locale: Locale) { 264 val nf = NumberFormat.getInstance(locale) 265 if (nf.format(FORMAT_NUMBER.toLong()) == burmeseNumerals) { 266 clocks.forEach { it.setLineSpacingScale(burmeseLineSpacing) } 267 } else { 268 clocks.forEach { it.setLineSpacingScale(defaultLineSpacing) } 269 } 270 271 clocks.forEach { it.refreshFormat() } 272 } 273 onWeatherDataChangednull274 override fun onWeatherDataChanged(data: WeatherData) {} onAlarmDataChangednull275 override fun onAlarmDataChanged(data: AlarmData) {} onZenDataChangednull276 override fun onZenDataChanged(data: ZenData) {} 277 } 278 279 open inner class DefaultClockAnimations( 280 val view: AnimatableClockView, 281 dozeFraction: Float, 282 foldFraction: Float, 283 ) : ClockAnimations { 284 internal val dozeState = AnimationState(dozeFraction) 285 private val foldState = AnimationState(foldFraction) 286 287 init { 288 if (foldState.isActive) { 289 view.animateFoldAppear(false) 290 } else { 291 view.animateDoze(dozeState.isActive, false) 292 } 293 } 294 enternull295 override fun enter() { 296 if (!dozeState.isActive) { 297 view.animateAppearOnLockscreen() 298 } 299 } 300 <lambda>null301 override fun charge() = view.animateCharge { dozeState.isActive } 302 foldnull303 override fun fold(fraction: Float) { 304 val (hasChanged, hasJumped) = foldState.update(fraction) 305 if (hasChanged) { 306 view.animateFoldAppear(!hasJumped) 307 } 308 } 309 dozenull310 override fun doze(fraction: Float) { 311 val (hasChanged, hasJumped) = dozeState.update(fraction) 312 if (hasChanged) { 313 view.animateDoze(dozeState.isActive, !hasJumped) 314 } 315 } 316 onPickerCarouselSwipingnull317 override fun onPickerCarouselSwiping(swipingFraction: Float) { 318 // TODO(b/278936436): refactor this part when we change recomputePadding 319 // when on the side, swipingFraction = 0, translationY should offset 320 // the top margin change in recomputePadding to make clock be centered 321 view.translationY = 0.5f * view.bottom * (1 - swipingFraction) 322 } 323 onPositionUpdatednull324 override fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) {} 325 onPositionUpdatednull326 override fun onPositionUpdated(distance: Float, fraction: Float) {} 327 } 328 329 inner class LargeClockAnimations( 330 view: AnimatableClockView, 331 dozeFraction: Float, 332 foldFraction: Float, 333 ) : DefaultClockAnimations(view, dozeFraction, foldFraction) { onPositionUpdatednull334 override fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) { 335 largeClock.offsetGlyphsForStepClockAnimation(fromLeft, direction, fraction) 336 } 337 onPositionUpdatednull338 override fun onPositionUpdated(distance: Float, fraction: Float) { 339 largeClock.offsetGlyphsForStepClockAnimation(distance, fraction) 340 } 341 } 342 343 class AnimationState( 344 var fraction: Float, 345 ) { 346 var isActive: Boolean = fraction > 0.5f updatenull347 fun update(newFraction: Float): Pair<Boolean, Boolean> { 348 if (newFraction == fraction) { 349 return Pair(isActive, false) 350 } 351 val wasActive = isActive 352 val hasJumped = 353 (fraction == 0f && newFraction == 1f) || (fraction == 1f && newFraction == 0f) 354 isActive = newFraction > fraction 355 fraction = newFraction 356 return Pair(wasActive != isActive, hasJumped) 357 } 358 } 359 dumpnull360 override fun dump(pw: PrintWriter) { 361 pw.print("smallClock=") 362 smallClock.view.dump(pw) 363 364 pw.print("largeClock=") 365 largeClock.view.dump(pw) 366 } 367 368 companion object { 369 @VisibleForTesting const val DOZE_COLOR = Color.WHITE 370 private const val FORMAT_NUMBER = 1234567890 371 } 372 } 373