1 /* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.shared.clocks 18 19 import android.graphics.Rect 20 import android.view.ViewGroup 21 import android.view.animation.Interpolator 22 import android.widget.RelativeLayout 23 import androidx.annotation.VisibleForTesting 24 import com.android.systemui.animation.TextAnimator 25 import com.android.systemui.customization.R 26 import com.android.systemui.log.core.Logger 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.ClockEvents 31 import com.android.systemui.plugins.clocks.ClockFaceConfig 32 import com.android.systemui.plugins.clocks.ClockFaceEvents 33 import com.android.systemui.plugins.clocks.ThemeConfig 34 import com.android.systemui.plugins.clocks.WeatherData 35 import com.android.systemui.plugins.clocks.ZenData 36 import com.android.systemui.shared.clocks.view.HorizontalAlignment 37 import com.android.systemui.shared.clocks.view.SimpleDigitalClockTextView 38 import com.android.systemui.shared.clocks.view.VerticalAlignment 39 import java.util.Locale 40 import java.util.TimeZone 41 42 private val TAG = SimpleDigitalHandLayerController::class.simpleName!! 43 44 // TODO(b/364680879): The remains of ClockDesign. Cut further. 45 data class LayerConfig( 46 val style: FontTextStyle, 47 val aodStyle: FontTextStyle, 48 val alignment: DigitalAlignment, 49 val timespec: DigitalTimespec, 50 val dateTimeFormat: String, 51 ) { generateDigitalLayerIdStringnull52 fun generateDigitalLayerIdString(): String { 53 return when { 54 timespec == DigitalTimespec.TIME_FULL_FORMAT -> "$timespec" 55 "h" in dateTimeFormat -> "HOUR_$timespec" 56 else -> "MINUTE_$timespec" 57 } 58 } 59 } 60 61 data class DigitalAlignment( 62 val horizontalAlignment: HorizontalAlignment?, 63 val verticalAlignment: VerticalAlignment?, 64 ) 65 66 data class FontTextStyle( 67 val lineHeight: Float? = null, 68 val fontSizeScale: Float? = null, 69 val transitionDuration: Long = TextAnimator.DEFAULT_ANIMATION_DURATION, 70 val transitionInterpolator: Interpolator? = null, 71 ) 72 73 enum class DigitalTimespec { 74 TIME_FULL_FORMAT, 75 DIGIT_PAIR, 76 FIRST_DIGIT, 77 SECOND_DIGIT, 78 } 79 80 open class SimpleDigitalHandLayerController( 81 private val clockCtx: ClockContext, 82 private val layerCfg: LayerConfig, 83 isLargeClock: Boolean, 84 ) : SimpleClockLayerController { 85 override val view = SimpleDigitalClockTextView(clockCtx, isLargeClock) 86 private val logger = Logger(clockCtx.messageBuffer, TAG) 87 val timespec = DigitalTimespecHandler(layerCfg.timespec, layerCfg.dateTimeFormat) 88 override var onViewBoundsChanged by view::onViewBoundsChanged 89 90 @VisibleForTesting 91 override var fakeTimeMills: Long? 92 get() = timespec.fakeTimeMills 93 set(value) { 94 timespec.fakeTimeMills = value 95 } 96 97 override val config = ClockFaceConfig() 98 var dozeState: DefaultClockController.AnimationState? = null 99 100 init { 101 view.layoutParams = 102 RelativeLayout.LayoutParams( 103 ViewGroup.LayoutParams.WRAP_CONTENT, 104 ViewGroup.LayoutParams.WRAP_CONTENT, 105 ) <lambda>null106 layerCfg.alignment.verticalAlignment?.let { view.verticalAlignment = it } <lambda>null107 layerCfg.alignment.horizontalAlignment?.let { view.horizontalAlignment = it } 108 view.applyStyles(layerCfg.style, layerCfg.aodStyle) 109 view.id = 110 clockCtx.resources.getIdentifier( 111 layerCfg.generateDigitalLayerIdString(), 112 "id", 113 clockCtx.context.getPackageName(), 114 ) 115 } 116 refreshTimenull117 fun refreshTime() { 118 timespec.updateTime() 119 val text = timespec.getDigitString() 120 if (view.text != text) { 121 view.text = text 122 view.refreshTime() 123 logger.d({ "refreshTime: new text=$str1" }) { str1 = text } 124 } 125 } 126 applyLayoutnull127 private fun applyLayout() { 128 // TODO: Remove NO-OP 129 if (view.layoutParams is RelativeLayout.LayoutParams) { 130 val lp = view.layoutParams as RelativeLayout.LayoutParams 131 lp.addRule(RelativeLayout.TEXT_ALIGNMENT_CENTER) 132 when (view.id) { 133 R.id.HOUR_DIGIT_PAIR -> { 134 lp.addRule(RelativeLayout.CENTER_VERTICAL) 135 lp.addRule(RelativeLayout.ALIGN_PARENT_START) 136 } 137 R.id.MINUTE_DIGIT_PAIR -> { 138 lp.addRule(RelativeLayout.CENTER_VERTICAL) 139 lp.addRule(RelativeLayout.END_OF, R.id.HOUR_DIGIT_PAIR) 140 } 141 else -> { 142 throw Exception("cannot apply two pairs layout to view ${view.id}") 143 } 144 } 145 view.layoutParams = lp 146 } 147 } 148 149 override val events = 150 object : ClockEvents { 151 override var isReactiveTouchInteractionEnabled = false 152 onLocaleChangednull153 override fun onLocaleChanged(locale: Locale) { 154 timespec.updateLocale(locale) 155 refreshTime() 156 } 157 158 /** Call whenever the text time format changes (12hr vs 24hr) */ onTimeFormatChangednull159 override fun onTimeFormatChanged(is24Hr: Boolean) { 160 timespec.is24Hr = is24Hr 161 refreshTime() 162 } 163 onTimeZoneChangednull164 override fun onTimeZoneChanged(timeZone: TimeZone) { 165 timespec.timeZone = timeZone 166 refreshTime() 167 } 168 onWeatherDataChangednull169 override fun onWeatherDataChanged(data: WeatherData) {} 170 onAlarmDataChangednull171 override fun onAlarmDataChanged(data: AlarmData) {} 172 onZenDataChangednull173 override fun onZenDataChanged(data: ZenData) {} 174 } 175 176 override val animations = 177 object : ClockAnimations { enternull178 override fun enter() { 179 applyLayout() 180 refreshTime() 181 } 182 dozenull183 override fun doze(fraction: Float) { 184 if (dozeState == null) { 185 dozeState = DefaultClockController.AnimationState(fraction) 186 view.animateDoze(dozeState!!.isActive, false) 187 } else { 188 val (hasChanged, hasJumped) = dozeState!!.update(fraction) 189 if (hasChanged) view.animateDoze(dozeState!!.isActive, !hasJumped) 190 } 191 view.dozeFraction = fraction 192 } 193 194 private var hasFontAxes = false 195 onFontAxesChangednull196 override fun onFontAxesChanged(style: ClockAxisStyle) { 197 view.updateAxes(style, isAnimated = hasFontAxes) 198 hasFontAxes = true 199 } 200 foldnull201 override fun fold(fraction: Float) { 202 applyLayout() 203 refreshTime() 204 } 205 chargenull206 override fun charge() { 207 view.animateCharge() 208 } 209 onPickerCarouselSwipingnull210 override fun onPickerCarouselSwiping(swipingFraction: Float) {} 211 onPositionUpdatednull212 override fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) {} 213 onPositionUpdatednull214 override fun onPositionUpdated(distance: Float, fraction: Float) {} 215 onFidgetTapnull216 override fun onFidgetTap(x: Float, y: Float) { 217 view.animateFidget(x, y) 218 } 219 } 220 221 override val faceEvents = 222 object : ClockFaceEvents { onTimeTicknull223 override fun onTimeTick() { 224 refreshTime() 225 if (layerCfg.timespec == DigitalTimespec.TIME_FULL_FORMAT) { 226 view.contentDescription = timespec.getContentDescription() 227 } 228 } 229 onFontSettingChangednull230 override fun onFontSettingChanged(fontSizePx: Float) { 231 view.applyTextSize(fontSizePx) 232 } 233 onThemeChangednull234 override fun onThemeChanged(theme: ThemeConfig) { 235 view.updateColor(theme.getDefaultColor(clockCtx.context)) 236 refreshTime() 237 } 238 onTargetRegionChangednull239 override fun onTargetRegionChanged(targetRegion: Rect?) {} 240 onSecondaryDisplayChangednull241 override fun onSecondaryDisplayChanged(onSecondaryDisplay: Boolean) {} 242 } 243 } 244