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.ClockFontAxisSetting 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 private val migratedClocks: Boolean = false, 59 messageBuffers: ClockMessageBuffers? = null, 60 ) : ClockController { 61 override val smallClock: DefaultClockFaceController 62 override val largeClock: LargeClockFaceController 63 private val clocks: List<AnimatableClockView> 64 65 private val burmeseNf = NumberFormat.getInstance(Locale.forLanguageTag("my")) 66 private val burmeseNumerals = burmeseNf.format(FORMAT_NUMBER.toLong()) 67 private val burmeseLineSpacing = 68 resources.getFloat(R.dimen.keyguard_clock_line_spacing_scale_burmese) 69 private val defaultLineSpacing = resources.getFloat(R.dimen.keyguard_clock_line_spacing_scale) 70 protected var onSecondaryDisplay: Boolean = false 71 72 override val events: DefaultClockEvents <lambda>null73 override val config: ClockConfig by lazy { 74 ClockConfig( 75 DEFAULT_CLOCK_ID, 76 resources.getString(R.string.clock_default_name), 77 resources.getString(R.string.clock_default_description), 78 ) 79 } 80 81 init { 82 val parent = FrameLayout(ctx) 83 smallClock = 84 DefaultClockFaceController( 85 layoutInflater.inflate(R.layout.clock_default_small, parent, false) 86 as AnimatableClockView, 87 settings?.seedColor, 88 messageBuffers?.smallClockMessageBuffer, 89 ) 90 largeClock = 91 LargeClockFaceController( 92 layoutInflater.inflate(R.layout.clock_default_large, parent, false) 93 as AnimatableClockView, 94 settings?.seedColor, 95 messageBuffers?.largeClockMessageBuffer, 96 ) 97 clocks = listOf(smallClock.view, largeClock.view) 98 99 events = DefaultClockEvents() 100 events.onLocaleChanged(Locale.getDefault()) 101 } 102 initializenull103 override fun initialize(isDarkTheme: Boolean, dozeFraction: Float, foldFraction: Float) { 104 largeClock.recomputePadding(null) 105 106 largeClock.animations = LargeClockAnimations(largeClock.view, dozeFraction, foldFraction) 107 smallClock.animations = DefaultClockAnimations(smallClock.view, dozeFraction, foldFraction) 108 109 largeClock.events.onThemeChanged(largeClock.theme.copy(isDarkTheme = isDarkTheme)) 110 smallClock.events.onThemeChanged(smallClock.theme.copy(isDarkTheme = isDarkTheme)) 111 events.onTimeZoneChanged(TimeZone.getDefault()) 112 113 smallClock.events.onTimeTick() 114 largeClock.events.onTimeTick() 115 } 116 117 open inner class DefaultClockFaceController( 118 override val view: AnimatableClockView, 119 seedColor: Int?, 120 messageBuffer: MessageBuffer?, 121 ) : ClockFaceController { 122 // MAGENTA is a placeholder, and will be assigned correctly in initialize 123 private var currentColor = seedColor ?: Color.MAGENTA 124 protected var targetRegion: Rect? = null 125 126 override val config = ClockFaceConfig() 127 override var theme = ThemeConfig(true, seedColor) 128 override val layout = <lambda>null129 DefaultClockFaceLayout(view).apply { 130 views[0].id = 131 resources.getIdentifier("lockscreen_clock_view", "id", ctx.packageName) 132 } 133 134 override var animations: DefaultClockAnimations = DefaultClockAnimations(view, 0f, 0f) 135 internal set 136 137 init { 138 view.setColors(DOZE_COLOR, currentColor) <lambda>null139 messageBuffer?.let { view.messageBuffer = it } 140 } 141 142 override val events = 143 object : ClockFaceEvents { onTimeTicknull144 override fun onTimeTick() = view.refreshTime() 145 146 override fun onThemeChanged(theme: ThemeConfig) { 147 this@DefaultClockFaceController.theme = theme 148 149 val color = 150 when { 151 theme.seedColor != null -> theme.seedColor!! 152 theme.isDarkTheme -> 153 resources.getColor(android.R.color.system_accent1_100) 154 else -> resources.getColor(android.R.color.system_accent2_600) 155 } 156 157 if (currentColor == color) { 158 return 159 } 160 161 currentColor = color 162 view.setColors(DOZE_COLOR, color) 163 if (!animations.dozeState.isActive) { 164 view.animateColorChange() 165 } 166 } 167 onTargetRegionChangednull168 override fun onTargetRegionChanged(targetRegion: Rect?) { 169 this@DefaultClockFaceController.targetRegion = targetRegion 170 recomputePadding(targetRegion) 171 } 172 onFontSettingChangednull173 override fun onFontSettingChanged(fontSizePx: Float) { 174 view.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSizePx) 175 recomputePadding(targetRegion) 176 } 177 onSecondaryDisplayChangednull178 override fun onSecondaryDisplayChanged(onSecondaryDisplay: Boolean) { 179 this@DefaultClockController.onSecondaryDisplay = onSecondaryDisplay 180 recomputePadding(null) 181 } 182 } 183 recomputePaddingnull184 open fun recomputePadding(targetRegion: Rect?) {} 185 } 186 187 inner class LargeClockFaceController( 188 view: AnimatableClockView, 189 seedColor: Int?, 190 messageBuffer: MessageBuffer?, 191 ) : DefaultClockFaceController(view, seedColor, messageBuffer) { 192 override val layout = <lambda>null193 DefaultClockFaceLayout(view).apply { 194 views[0].id = 195 resources.getIdentifier("lockscreen_clock_view_large", "id", ctx.packageName) 196 } 197 override val config = ClockFaceConfig(hasCustomPositionUpdatedAnimation = true) 198 199 init { 200 view.migratedClocks = migratedClocks 201 view.hasCustomPositionUpdatedAnimation = true 202 animations = LargeClockAnimations(view, 0f, 0f) 203 } 204 recomputePaddingnull205 override fun recomputePadding(targetRegion: Rect?) { 206 if (migratedClocks) { 207 return 208 } 209 // We center the view within the targetRegion instead of within the parent 210 // view by computing the difference and adding that to the padding. 211 val lp = view.getLayoutParams() as FrameLayout.LayoutParams 212 lp.topMargin = 213 if (onSecondaryDisplay) { 214 // On the secondary display we don't want any additional top/bottom margin. 215 0 216 } else { 217 val parent = view.parent 218 val yDiff = 219 if (targetRegion != null && parent is View && parent.isLaidOut()) 220 targetRegion.centerY() - parent.height / 2f 221 else 0f 222 (-0.5f * view.bottom + yDiff).toInt() 223 } 224 view.setLayoutParams(lp) 225 } 226 227 /** See documentation at [AnimatableClockView.offsetGlyphsForStepClockAnimation]. */ offsetGlyphsForStepClockAnimationnull228 fun offsetGlyphsForStepClockAnimation(fromLeft: Int, direction: Int, fraction: Float) { 229 view.offsetGlyphsForStepClockAnimation(fromLeft, direction, fraction) 230 } 231 offsetGlyphsForStepClockAnimationnull232 fun offsetGlyphsForStepClockAnimation(distance: Float, fraction: Float) { 233 view.offsetGlyphsForStepClockAnimation(distance, fraction) 234 } 235 } 236 237 inner class DefaultClockEvents : ClockEvents { 238 override var isReactiveTouchInteractionEnabled: Boolean = false 239 onTimeFormatChangednull240 override fun onTimeFormatChanged(is24Hr: Boolean) = 241 clocks.forEach { it.refreshFormat(is24Hr) } 242 onTimeZoneChangednull243 override fun onTimeZoneChanged(timeZone: TimeZone) = 244 clocks.forEach { it.onTimeZoneChanged(timeZone) } 245 onLocaleChangednull246 override fun onLocaleChanged(locale: Locale) { 247 val nf = NumberFormat.getInstance(locale) 248 if (nf.format(FORMAT_NUMBER.toLong()) == burmeseNumerals) { 249 clocks.forEach { it.setLineSpacingScale(burmeseLineSpacing) } 250 } else { 251 clocks.forEach { it.setLineSpacingScale(defaultLineSpacing) } 252 } 253 254 clocks.forEach { it.refreshFormat() } 255 } 256 onWeatherDataChangednull257 override fun onWeatherDataChanged(data: WeatherData) {} 258 onAlarmDataChangednull259 override fun onAlarmDataChanged(data: AlarmData) {} 260 onZenDataChangednull261 override fun onZenDataChanged(data: ZenData) {} 262 onFontAxesChangednull263 override fun onFontAxesChanged(axes: List<ClockFontAxisSetting>) {} 264 } 265 266 open inner class DefaultClockAnimations( 267 val view: AnimatableClockView, 268 dozeFraction: Float, 269 foldFraction: Float, 270 ) : ClockAnimations { 271 internal val dozeState = AnimationState(dozeFraction) 272 private val foldState = AnimationState(foldFraction) 273 274 init { 275 if (foldState.isActive) { 276 view.animateFoldAppear(false) 277 } else { 278 view.animateDoze(dozeState.isActive, false) 279 } 280 } 281 enternull282 override fun enter() { 283 if (!dozeState.isActive) { 284 view.animateAppearOnLockscreen() 285 } 286 } 287 <lambda>null288 override fun charge() = view.animateCharge { dozeState.isActive } 289 foldnull290 override fun fold(fraction: Float) { 291 val (hasChanged, hasJumped) = foldState.update(fraction) 292 if (hasChanged) { 293 view.animateFoldAppear(!hasJumped) 294 } 295 } 296 dozenull297 override fun doze(fraction: Float) { 298 val (hasChanged, hasJumped) = dozeState.update(fraction) 299 if (hasChanged) { 300 view.animateDoze(dozeState.isActive, !hasJumped) 301 } 302 } 303 onPickerCarouselSwipingnull304 override fun onPickerCarouselSwiping(swipingFraction: Float) { 305 // TODO(b/278936436): refactor this part when we change recomputePadding 306 // when on the side, swipingFraction = 0, translationY should offset 307 // the top margin change in recomputePadding to make clock be centered 308 view.translationY = 0.5f * view.bottom * (1 - swipingFraction) 309 } 310 onPositionUpdatednull311 override fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) {} 312 onPositionUpdatednull313 override fun onPositionUpdated(distance: Float, fraction: Float) {} 314 } 315 316 inner class LargeClockAnimations( 317 view: AnimatableClockView, 318 dozeFraction: Float, 319 foldFraction: Float, 320 ) : DefaultClockAnimations(view, dozeFraction, foldFraction) { onPositionUpdatednull321 override fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) { 322 largeClock.offsetGlyphsForStepClockAnimation(fromLeft, direction, fraction) 323 } 324 onPositionUpdatednull325 override fun onPositionUpdated(distance: Float, fraction: Float) { 326 largeClock.offsetGlyphsForStepClockAnimation(distance, fraction) 327 } 328 } 329 330 class AnimationState(var fraction: Float) { 331 var isActive: Boolean = fraction > 0.5f 332 updatenull333 fun update(newFraction: Float): Pair<Boolean, Boolean> { 334 if (newFraction == fraction) { 335 return Pair(isActive, false) 336 } 337 val wasActive = isActive 338 val hasJumped = 339 (fraction == 0f && newFraction == 1f) || (fraction == 1f && newFraction == 0f) 340 isActive = newFraction > fraction 341 fraction = newFraction 342 return Pair(wasActive != isActive, hasJumped) 343 } 344 } 345 dumpnull346 override fun dump(pw: PrintWriter) { 347 pw.print("smallClock=") 348 smallClock.view.dump(pw) 349 350 pw.print("largeClock=") 351 largeClock.view.dump(pw) 352 } 353 354 companion object { 355 @VisibleForTesting const val DOZE_COLOR = Color.WHITE 356 private const val FORMAT_NUMBER = 1234567890 357 } 358 } 359