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.View 21 import android.view.ViewGroup 22 import android.widget.RelativeLayout 23 import androidx.annotation.VisibleForTesting 24 import com.android.systemui.customization.R 25 import com.android.systemui.log.core.Logger 26 import com.android.systemui.plugins.clocks.AlarmData 27 import com.android.systemui.plugins.clocks.ClockAnimations 28 import com.android.systemui.plugins.clocks.ClockEvents 29 import com.android.systemui.plugins.clocks.ClockFaceConfig 30 import com.android.systemui.plugins.clocks.ClockFaceEvents 31 import com.android.systemui.plugins.clocks.ClockFontAxisSetting 32 import com.android.systemui.plugins.clocks.ThemeConfig 33 import com.android.systemui.plugins.clocks.WeatherData 34 import com.android.systemui.plugins.clocks.ZenData 35 import com.android.systemui.shared.clocks.view.SimpleDigitalClockView 36 import java.util.Locale 37 import java.util.TimeZone 38 39 private val TAG = SimpleDigitalHandLayerController::class.simpleName!! 40 41 open class SimpleDigitalHandLayerController<T>( 42 private val clockCtx: ClockContext, 43 private val layer: DigitalHandLayer, 44 override val view: T, 45 ) : SimpleClockLayerController where T : View, T : SimpleDigitalClockView { 46 private val logger = Logger(clockCtx.messageBuffer, TAG) 47 val timespec = DigitalTimespecHandler(layer.timespec, layer.dateTimeFormat) 48 49 @VisibleForTesting hasLeadingZeronull50 fun hasLeadingZero() = layer.dateTimeFormat.startsWith("hh") || timespec.is24Hr 51 52 @VisibleForTesting 53 override var fakeTimeMills: Long? 54 get() = timespec.fakeTimeMills 55 set(value) { 56 timespec.fakeTimeMills = value 57 } 58 59 override val config = ClockFaceConfig() 60 var dozeState: DefaultClockController.AnimationState? = null 61 62 init { 63 view.layoutParams = 64 RelativeLayout.LayoutParams( 65 ViewGroup.LayoutParams.WRAP_CONTENT, 66 ViewGroup.LayoutParams.WRAP_CONTENT, 67 ) 68 if (layer.alignment != null) { <lambda>null69 layer.alignment.verticalAlignment?.let { view.verticalAlignment = it } <lambda>null70 layer.alignment.horizontalAlignment?.let { view.horizontalAlignment = it } 71 } 72 view.applyStyles(layer.style, layer.aodStyle) 73 view.id = 74 clockCtx.resources.getIdentifier( 75 generateDigitalLayerIdString(layer), 76 "id", 77 clockCtx.context.getPackageName(), 78 ) 79 } 80 applyLayoutnull81 fun applyLayout(layout: DigitalFaceLayout?) { 82 when (layout) { 83 DigitalFaceLayout.FOUR_DIGITS_ALIGN_CENTER, 84 DigitalFaceLayout.FOUR_DIGITS_HORIZONTAL -> applyFourDigitsLayout(layout) 85 DigitalFaceLayout.TWO_PAIRS_HORIZONTAL, 86 DigitalFaceLayout.TWO_PAIRS_VERTICAL -> applyTwoPairsLayout(layout) 87 else -> { 88 // one view always use FrameLayout 89 // no need to change here 90 } 91 } 92 applyMargin() 93 } 94 applyMarginnull95 private fun applyMargin() { 96 if (view.layoutParams is RelativeLayout.LayoutParams) { 97 val lp = view.layoutParams as RelativeLayout.LayoutParams 98 layer.marginRatio?.let { 99 lp.setMargins( 100 /* left = */ (it.left * view.measuredWidth).toInt(), 101 /* top = */ (it.top * view.measuredHeight).toInt(), 102 /* right = */ (it.right * view.measuredWidth).toInt(), 103 /* bottom = */ (it.bottom * view.measuredHeight).toInt(), 104 ) 105 } 106 view.layoutParams = lp 107 } 108 } 109 applyTwoPairsLayoutnull110 private fun applyTwoPairsLayout(twoPairsLayout: DigitalFaceLayout) { 111 val lp = view.layoutParams as RelativeLayout.LayoutParams 112 lp.addRule(RelativeLayout.TEXT_ALIGNMENT_CENTER) 113 if (twoPairsLayout == DigitalFaceLayout.TWO_PAIRS_HORIZONTAL) { 114 when (view.id) { 115 R.id.HOUR_DIGIT_PAIR -> { 116 lp.addRule(RelativeLayout.CENTER_VERTICAL) 117 lp.addRule(RelativeLayout.ALIGN_PARENT_START) 118 } 119 R.id.MINUTE_DIGIT_PAIR -> { 120 lp.addRule(RelativeLayout.CENTER_VERTICAL) 121 lp.addRule(RelativeLayout.END_OF, R.id.HOUR_DIGIT_PAIR) 122 } 123 else -> { 124 throw Exception("cannot apply two pairs layout to view ${view.id}") 125 } 126 } 127 } else { 128 when (view.id) { 129 R.id.HOUR_DIGIT_PAIR -> { 130 lp.addRule(RelativeLayout.CENTER_HORIZONTAL) 131 lp.addRule(RelativeLayout.ALIGN_PARENT_TOP) 132 } 133 R.id.MINUTE_DIGIT_PAIR -> { 134 lp.addRule(RelativeLayout.CENTER_HORIZONTAL) 135 lp.addRule(RelativeLayout.BELOW, R.id.HOUR_DIGIT_PAIR) 136 } 137 else -> { 138 throw Exception("cannot apply two pairs layout to view ${view.id}") 139 } 140 } 141 } 142 view.layoutParams = lp 143 } 144 applyFourDigitsLayoutnull145 private fun applyFourDigitsLayout(fourDigitsfaceLayout: DigitalFaceLayout) { 146 val lp = view.layoutParams as RelativeLayout.LayoutParams 147 when (fourDigitsfaceLayout) { 148 DigitalFaceLayout.FOUR_DIGITS_ALIGN_CENTER -> { 149 when (view.id) { 150 R.id.HOUR_FIRST_DIGIT -> { 151 lp.addRule(RelativeLayout.ALIGN_PARENT_START) 152 lp.addRule(RelativeLayout.ALIGN_PARENT_TOP) 153 } 154 R.id.HOUR_SECOND_DIGIT -> { 155 lp.addRule(RelativeLayout.END_OF, R.id.HOUR_FIRST_DIGIT) 156 lp.addRule(RelativeLayout.ALIGN_TOP, R.id.HOUR_FIRST_DIGIT) 157 } 158 R.id.MINUTE_FIRST_DIGIT -> { 159 lp.addRule(RelativeLayout.ALIGN_START, R.id.HOUR_FIRST_DIGIT) 160 lp.addRule(RelativeLayout.BELOW, R.id.HOUR_FIRST_DIGIT) 161 } 162 R.id.MINUTE_SECOND_DIGIT -> { 163 lp.addRule(RelativeLayout.ALIGN_START, R.id.HOUR_SECOND_DIGIT) 164 lp.addRule(RelativeLayout.BELOW, R.id.HOUR_SECOND_DIGIT) 165 } 166 else -> { 167 throw Exception("cannot apply four digits layout to view ${view.id}") 168 } 169 } 170 } 171 DigitalFaceLayout.FOUR_DIGITS_HORIZONTAL -> { 172 when (view.id) { 173 R.id.HOUR_FIRST_DIGIT -> { 174 lp.addRule(RelativeLayout.CENTER_VERTICAL) 175 lp.addRule(RelativeLayout.ALIGN_PARENT_START) 176 } 177 R.id.HOUR_SECOND_DIGIT -> { 178 lp.addRule(RelativeLayout.CENTER_VERTICAL) 179 lp.addRule(RelativeLayout.END_OF, R.id.HOUR_FIRST_DIGIT) 180 } 181 R.id.MINUTE_FIRST_DIGIT -> { 182 lp.addRule(RelativeLayout.CENTER_VERTICAL) 183 lp.addRule(RelativeLayout.END_OF, R.id.HOUR_SECOND_DIGIT) 184 } 185 R.id.MINUTE_SECOND_DIGIT -> { 186 lp.addRule(RelativeLayout.CENTER_VERTICAL) 187 lp.addRule(RelativeLayout.END_OF, R.id.MINUTE_FIRST_DIGIT) 188 } 189 else -> { 190 throw Exception("cannot apply FOUR_DIGITS_HORIZONTAL to view ${view.id}") 191 } 192 } 193 } 194 else -> { 195 throw IllegalArgumentException( 196 "applyFourDigitsLayout function should not " + 197 "have parameters as ${layer.faceLayout}" 198 ) 199 } 200 } 201 if (lp == view.layoutParams) { 202 return 203 } 204 view.layoutParams = lp 205 } 206 refreshTimenull207 fun refreshTime() { 208 timespec.updateTime() 209 val text = timespec.getDigitString() 210 if (view.text != text) { 211 view.text = text 212 view.refreshTime() 213 logger.d({ "refreshTime: new text=$str1" }) { str1 = text } 214 } 215 } 216 217 override val events = 218 object : ClockEvents { 219 override var isReactiveTouchInteractionEnabled = false 220 onLocaleChangednull221 override fun onLocaleChanged(locale: Locale) { 222 timespec.updateLocale(locale) 223 refreshTime() 224 } 225 226 /** Call whenever the text time format changes (12hr vs 24hr) */ onTimeFormatChangednull227 override fun onTimeFormatChanged(is24Hr: Boolean) { 228 timespec.is24Hr = is24Hr 229 refreshTime() 230 } 231 onTimeZoneChangednull232 override fun onTimeZoneChanged(timeZone: TimeZone) { 233 timespec.timeZone = timeZone 234 refreshTime() 235 } 236 onWeatherDataChangednull237 override fun onWeatherDataChanged(data: WeatherData) {} 238 onAlarmDataChangednull239 override fun onAlarmDataChanged(data: AlarmData) {} 240 onZenDataChangednull241 override fun onZenDataChanged(data: ZenData) {} 242 onFontAxesChangednull243 override fun onFontAxesChanged(axes: List<ClockFontAxisSetting>) { 244 view.updateAxes(axes) 245 } 246 } 247 248 override val animations = 249 object : ClockAnimations { enternull250 override fun enter() { 251 applyLayout(layer.faceLayout) 252 refreshTime() 253 } 254 dozenull255 override fun doze(fraction: Float) { 256 if (dozeState == null) { 257 dozeState = DefaultClockController.AnimationState(fraction) 258 view.animateDoze(dozeState!!.isActive, false) 259 } else { 260 val (hasChanged, hasJumped) = dozeState!!.update(fraction) 261 if (hasChanged) view.animateDoze(dozeState!!.isActive, !hasJumped) 262 } 263 view.dozeFraction = fraction 264 } 265 foldnull266 override fun fold(fraction: Float) { 267 applyLayout(layer.faceLayout) 268 refreshTime() 269 } 270 chargenull271 override fun charge() { 272 view.animateCharge() 273 } 274 onPickerCarouselSwipingnull275 override fun onPickerCarouselSwiping(swipingFraction: Float) {} 276 onPositionUpdatednull277 override fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) {} 278 onPositionUpdatednull279 override fun onPositionUpdated(distance: Float, fraction: Float) {} 280 } 281 282 override val faceEvents = 283 object : ClockFaceEvents { onTimeTicknull284 override fun onTimeTick() { 285 refreshTime() 286 if ( 287 layer.timespec == DigitalTimespec.TIME_FULL_FORMAT || 288 layer.timespec == DigitalTimespec.DATE_FORMAT 289 ) { 290 view.contentDescription = timespec.getContentDescription() 291 } 292 } 293 onFontSettingChangednull294 override fun onFontSettingChanged(fontSizePx: Float) { 295 view.applyTextSize(fontSizePx) 296 applyMargin() 297 } 298 onThemeChangednull299 override fun onThemeChanged(theme: ThemeConfig) { 300 val color = 301 when { 302 theme.seedColor != null -> theme.seedColor!! 303 theme.isDarkTheme -> 304 clockCtx.resources.getColor(android.R.color.system_accent1_100) 305 else -> clockCtx.resources.getColor(android.R.color.system_accent2_600) 306 } 307 308 view.updateColor(color) 309 refreshTime() 310 } 311 onTargetRegionChangednull312 override fun onTargetRegionChanged(targetRegion: Rect?) {} 313 onSecondaryDisplayChangednull314 override fun onSecondaryDisplayChanged(onSecondaryDisplay: Boolean) {} 315 } 316 } 317