diff options
10 files changed, 1526 insertions, 12 deletions
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt new file mode 100644 index 000000000000..9b94c91a348c --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shared.clocks + +import android.content.Context +import android.content.res.Resources +import android.graphics.Rect +import androidx.annotation.VisibleForTesting +import com.android.systemui.log.core.Logger +import com.android.systemui.log.core.MessageBuffer +import com.android.systemui.plugins.clocks.AlarmData +import com.android.systemui.plugins.clocks.ClockAnimations +import com.android.systemui.plugins.clocks.ClockEvents +import com.android.systemui.plugins.clocks.ClockFaceConfig +import com.android.systemui.plugins.clocks.ClockFaceEvents +import com.android.systemui.plugins.clocks.ClockReactiveSetting +import com.android.systemui.plugins.clocks.WeatherData +import com.android.systemui.plugins.clocks.ZenData +import com.android.systemui.shared.clocks.view.DigitalClockFaceView +import com.android.systemui.shared.clocks.view.FlexClockView +import java.util.Locale +import java.util.TimeZone + +class ComposedDigitalLayerController( + private val ctx: Context, + private val assets: AssetLoader, + private val layer: ComposedDigitalHandLayer, + private val isLargeClock: Boolean, + messageBuffer: MessageBuffer, +) : SimpleClockLayerController { + private val logger = Logger(messageBuffer, ComposedDigitalLayerController::class.simpleName!!) + + val layerControllers = mutableListOf<SimpleClockLayerController>() + val dozeState = DefaultClockController.AnimationState(1F) + var isRegionDark = true + + override var view: DigitalClockFaceView = + when (layer.customizedView) { + "FlexClockView" -> FlexClockView(ctx, assets, messageBuffer) + else -> { + throw IllegalStateException("CustomizedView string is not valid") + } + } + + // Matches LayerControllerConstructor + internal constructor( + ctx: Context, + assets: AssetLoader, + layer: ClockLayer, + isLargeClock: Boolean, + messageBuffer: MessageBuffer, + ) : this(ctx, assets, layer as ComposedDigitalHandLayer, isLargeClock, messageBuffer) + + init { + layer.digitalLayers.forEach { + val controller = + SimpleClockLayerController.Factory.create( + ctx, + assets, + it, + isLargeClock, + messageBuffer, + ) + view.addView(controller.view) + layerControllers.add(controller) + } + } + + private fun refreshTime() { + layerControllers.forEach { it.faceEvents.onTimeTick() } + view.refreshTime() + } + + override val events = + object : ClockEvents { + override fun onTimeZoneChanged(timeZone: TimeZone) { + layerControllers.forEach { it.events.onTimeZoneChanged(timeZone) } + refreshTime() + } + + override fun onTimeFormatChanged(is24Hr: Boolean) { + layerControllers.forEach { it.events.onTimeFormatChanged(is24Hr) } + refreshTime() + } + + override fun onLocaleChanged(locale: Locale) { + layerControllers.forEach { it.events.onLocaleChanged(locale) } + view.onLocaleChanged(locale) + refreshTime() + } + + override fun onWeatherDataChanged(data: WeatherData) { + view.onWeatherDataChanged(data) + } + + override fun onAlarmDataChanged(data: AlarmData) { + view.onAlarmDataChanged(data) + } + + override fun onZenDataChanged(data: ZenData) { + view.onZenDataChanged(data) + } + + override fun onColorPaletteChanged(resources: Resources) {} + + override fun onSeedColorChanged(seedColor: Int?) {} + + override fun onReactiveAxesChanged(axes: List<ClockReactiveSetting>) {} + + override var isReactiveTouchInteractionEnabled + get() = view.isReactiveTouchInteractionEnabled + set(value) { + view.isReactiveTouchInteractionEnabled = value + } + } + + override fun updateColors() { + view.updateColors(assets, isRegionDark) + } + + override val animations = + object : ClockAnimations { + override fun enter() { + refreshTime() + } + + override fun doze(fraction: Float) { + val (hasChanged, hasJumped) = dozeState.update(fraction) + if (hasChanged) view.animateDoze(dozeState.isActive, !hasJumped) + view.dozeFraction = fraction + view.invalidate() + } + + override fun fold(fraction: Float) { + refreshTime() + } + + override fun charge() { + view.animateCharge() + } + + override fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) { + view.onPositionUpdated(fromLeft, direction, fraction) + } + + override fun onPositionUpdated(distance: Float, fraction: Float) {} + + override fun onPickerCarouselSwiping(swipingFraction: Float) { + view.onPickerCarouselSwiping(swipingFraction) + } + } + + override val faceEvents = + object : ClockFaceEvents { + override fun onTimeTick() { + refreshTime() + } + + override fun onRegionDarknessChanged(isRegionDark: Boolean) { + this@ComposedDigitalLayerController.isRegionDark = isRegionDark + updateColors() + } + + override fun onFontSettingChanged(fontSizePx: Float) { + view.onFontSettingChanged(fontSizePx) + } + + override fun onTargetRegionChanged(targetRegion: Rect?) {} + + override fun onSecondaryDisplayChanged(onSecondaryDisplay: Boolean) {} + } + + override val config = + ClockFaceConfig( + hasCustomWeatherDataDisplay = view.hasCustomWeatherDataDisplay, + hasCustomPositionUpdatedAnimation = view.hasCustomPositionUpdatedAnimation, + useCustomClockScene = view.useCustomClockScene, + ) + + @VisibleForTesting + override var fakeTimeMills: Long? = null + get() = field + set(timeInMills) { + field = timeInMills + for (layerController in layerControllers) { + layerController.fakeTimeMills = timeInMills + } + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt index 07191c671a34..ac268420fb75 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt @@ -24,6 +24,8 @@ import com.android.systemui.plugins.clocks.ClockMetadata import com.android.systemui.plugins.clocks.ClockPickerConfig import com.android.systemui.plugins.clocks.ClockProvider import com.android.systemui.plugins.clocks.ClockSettings +import com.android.systemui.shared.clocks.view.HorizontalAlignment +import com.android.systemui.shared.clocks.view.VerticalAlignment private val TAG = DefaultClockProvider::class.simpleName const val DEFAULT_CLOCK_ID = "DEFAULT" @@ -33,8 +35,9 @@ class DefaultClockProvider( val ctx: Context, val layoutInflater: LayoutInflater, val resources: Resources, - val hasStepClockAnimation: Boolean = false, - val migratedClocks: Boolean = false, + private val hasStepClockAnimation: Boolean = false, + private val migratedClocks: Boolean = false, + private val clockReactiveVariants: Boolean = false, ) : ClockProvider { private var messageBuffers: ClockMessageBuffers? = null @@ -49,15 +52,23 @@ class DefaultClockProvider( throw IllegalArgumentException("${settings.clockId} is unsupported by $TAG") } - return DefaultClockController( - ctx, - layoutInflater, - resources, - settings, - hasStepClockAnimation, - migratedClocks, - messageBuffers, - ) + return if (clockReactiveVariants) { + // TODO handle the case here where only the smallClock message buffer is added + val assetLoader = + AssetLoader(ctx, ctx, "clocks/", messageBuffers?.smallClockMessageBuffer!!) + + SimpleClockController(ctx, assetLoader, FLEX_DESIGN, messageBuffers) + } else { + DefaultClockController( + ctx, + layoutInflater, + resources, + settings, + hasStepClockAnimation, + migratedClocks, + messageBuffers, + ) + } } override fun getClockPickerConfig(id: ClockId): ClockPickerConfig { @@ -73,4 +84,163 @@ class DefaultClockProvider( resources.getDrawable(R.drawable.clock_default_thumbnail, null), ) } + + companion object { + val FLEX_DESIGN = run { + val largeLayer = + listOf( + ComposedDigitalHandLayer( + layerBounds = LayerBounds.FIT, + customizedView = "FlexClockView", + digitalLayers = + listOf( + DigitalHandLayer( + layerBounds = LayerBounds.FIT, + timespec = DigitalTimespec.FIRST_DIGIT, + style = + FontTextStyle( + fontFamily = "google_sans_flex.ttf", + lineHeight = 147.25f, + fontVariation = + "'wght' 603, 'wdth' 100, 'opsz' 144, 'ROND' 100", + ), + aodStyle = + FontTextStyle( + fontVariation = + "'wght' 74, 'wdth' 43, 'opsz' 144, 'ROND' 100", + fontFamily = "google_sans_flex.ttf", + fillColorLight = "#FFFFFFFF", + outlineColor = "#00000000", + renderType = RenderType.CHANGE_WEIGHT, + transitionInterpolator = InterpolatorEnum.EMPHASIZED, + transitionDuration = 750, + ), + alignment = + DigitalAlignment( + HorizontalAlignment.CENTER, + VerticalAlignment.CENTER + ), + dateTimeFormat = "hh" + ), + DigitalHandLayer( + layerBounds = LayerBounds.FIT, + timespec = DigitalTimespec.SECOND_DIGIT, + style = + FontTextStyle( + fontFamily = "google_sans_flex.ttf", + lineHeight = 147.25f, + fontVariation = + "'wght' 603, 'wdth' 100, 'opsz' 144, 'ROND' 100", + ), + aodStyle = + FontTextStyle( + fontVariation = + "'wght' 74, 'wdth' 43, 'opsz' 144, 'ROND' 100", + fontFamily = "google_sans_flex.ttf", + fillColorLight = "#FFFFFFFF", + outlineColor = "#00000000", + renderType = RenderType.CHANGE_WEIGHT, + transitionInterpolator = InterpolatorEnum.EMPHASIZED, + transitionDuration = 750, + ), + alignment = + DigitalAlignment( + HorizontalAlignment.CENTER, + VerticalAlignment.CENTER + ), + dateTimeFormat = "hh" + ), + DigitalHandLayer( + layerBounds = LayerBounds.FIT, + timespec = DigitalTimespec.FIRST_DIGIT, + style = + FontTextStyle( + fontFamily = "google_sans_flex.ttf", + lineHeight = 147.25f, + fontVariation = + "'wght' 603, 'wdth' 100, 'opsz' 144, 'ROND' 100", + ), + aodStyle = + FontTextStyle( + fontVariation = + "'wght' 74, 'wdth' 43, 'opsz' 144, 'ROND' 100", + fontFamily = "google_sans_flex.ttf", + fillColorLight = "#FFFFFFFF", + outlineColor = "#00000000", + renderType = RenderType.CHANGE_WEIGHT, + transitionInterpolator = InterpolatorEnum.EMPHASIZED, + transitionDuration = 750, + ), + alignment = + DigitalAlignment( + HorizontalAlignment.CENTER, + VerticalAlignment.CENTER + ), + dateTimeFormat = "mm" + ), + DigitalHandLayer( + layerBounds = LayerBounds.FIT, + timespec = DigitalTimespec.SECOND_DIGIT, + style = + FontTextStyle( + fontFamily = "google_sans_flex.ttf", + lineHeight = 147.25f, + fontVariation = + "'wght' 603, 'wdth' 100, 'opsz' 144, 'ROND' 100", + ), + aodStyle = + FontTextStyle( + fontVariation = + "'wght' 74, 'wdth' 43, 'opsz' 144, 'ROND' 100", + fontFamily = "google_sans_flex.ttf", + fillColorLight = "#FFFFFFFF", + outlineColor = "#00000000", + renderType = RenderType.CHANGE_WEIGHT, + transitionInterpolator = InterpolatorEnum.EMPHASIZED, + transitionDuration = 750, + ), + alignment = + DigitalAlignment( + HorizontalAlignment.CENTER, + VerticalAlignment.CENTER + ), + dateTimeFormat = "mm" + ) + ) + ) + ) + + val smallLayer = + listOf( + DigitalHandLayer( + layerBounds = LayerBounds.FIT, + timespec = DigitalTimespec.TIME_FULL_FORMAT, + style = + FontTextStyle( + fontFamily = "google_sans_flex.ttf", + fontVariation = "'wght' 600, 'wdth' 100, 'opsz' 144, 'ROND' 100", + fontSizeScale = 0.98f, + ), + aodStyle = + FontTextStyle( + fontFamily = "google_sans_flex.ttf", + fontVariation = "'wght' 133, 'wdth' 43, 'opsz' 144, 'ROND' 100", + fillColorLight = "#FFFFFFFF", + outlineColor = "#00000000", + renderType = RenderType.CHANGE_WEIGHT, + ), + alignment = DigitalAlignment(HorizontalAlignment.LEFT, null), + dateTimeFormat = "h:mm" + ) + ) + + ClockDesign( + id = DEFAULT_CLOCK_ID, + name = "@string/clock_default_name", + description = "@string/clock_default_description", + large = ClockFace(layers = largeLayer), + small = ClockFace(layers = smallLayer) + ) + } + } } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/LayoutUtils.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/LayoutUtils.kt new file mode 100644 index 000000000000..ef8bee0875d2 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/LayoutUtils.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shared.clocks + +import android.graphics.Rect +import android.view.View + +fun computeLayoutDiff( + view: View, + targetRegion: Rect, + isLargeClock: Boolean, +): Pair<Float, Float> { + val parent = view.parent + if (parent is View && parent.isLaidOut() && isLargeClock) { + return Pair( + targetRegion.centerX() - parent.width / 2f, + targetRegion.centerY() - parent.height / 2f + ) + } + return Pair(0f, 0f) +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockController.kt new file mode 100644 index 000000000000..ec7779825bda --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockController.kt @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shared.clocks + +import android.content.Context +import android.content.res.Resources +import com.android.systemui.monet.Style as MonetStyle +import com.android.systemui.plugins.clocks.AlarmData +import com.android.systemui.plugins.clocks.ClockConfig +import com.android.systemui.plugins.clocks.ClockController +import com.android.systemui.plugins.clocks.ClockEvents +import com.android.systemui.plugins.clocks.ClockMessageBuffers +import com.android.systemui.plugins.clocks.ClockReactiveSetting +import com.android.systemui.plugins.clocks.WeatherData +import com.android.systemui.plugins.clocks.ZenData +import java.io.PrintWriter +import java.util.Locale +import java.util.TimeZone + +/** Controller for a simple json specified clock */ +class SimpleClockController( + private val ctx: Context, + private val assets: AssetLoader, + val design: ClockDesign, + val messageBuffers: ClockMessageBuffers?, +) : ClockController { + override val smallClock = run { + val buffer = messageBuffers?.smallClockMessageBuffer ?: LogUtil.DEFAULT_MESSAGE_BUFFER + SimpleClockFaceController( + ctx, + assets.copy(messageBuffer = buffer), + design.small ?: design.large!!, + false, + buffer, + ) + } + + override val largeClock = run { + val buffer = messageBuffers?.largeClockMessageBuffer ?: LogUtil.DEFAULT_MESSAGE_BUFFER + SimpleClockFaceController( + ctx, + assets.copy(messageBuffer = buffer), + design.large ?: design.small!!, + true, + buffer, + ) + } + + override val config: ClockConfig by lazy { + ClockConfig( + design.id, + design.name?.let { assets.tryReadString(it) ?: it } ?: "", + design.description?.let { assets.tryReadString(it) ?: it } ?: "", + isReactiveToTone = + design.colorPalette == null || design.colorPalette == MonetStyle.CLOCK, + useAlternateSmartspaceAODTransition = + smallClock.config.hasCustomWeatherDataDisplay || + largeClock.config.hasCustomWeatherDataDisplay, + useCustomClockScene = + smallClock.config.useCustomClockScene || largeClock.config.useCustomClockScene, + ) + } + + override val events = + object : ClockEvents { + override var isReactiveTouchInteractionEnabled = false + set(value) { + field = value + smallClock.events.isReactiveTouchInteractionEnabled = value + largeClock.events.isReactiveTouchInteractionEnabled = value + } + + override fun onTimeZoneChanged(timeZone: TimeZone) { + smallClock.events.onTimeZoneChanged(timeZone) + largeClock.events.onTimeZoneChanged(timeZone) + } + + override fun onTimeFormatChanged(is24Hr: Boolean) { + smallClock.events.onTimeFormatChanged(is24Hr) + largeClock.events.onTimeFormatChanged(is24Hr) + } + + override fun onLocaleChanged(locale: Locale) { + smallClock.events.onLocaleChanged(locale) + largeClock.events.onLocaleChanged(locale) + } + + override fun onColorPaletteChanged(resources: Resources) { + assets.refreshColorPalette(design.colorPalette) + smallClock.assets.refreshColorPalette(design.colorPalette) + largeClock.assets.refreshColorPalette(design.colorPalette) + + smallClock.events.onColorPaletteChanged(resources) + largeClock.events.onColorPaletteChanged(resources) + } + + override fun onSeedColorChanged(seedColor: Int?) { + assets.setSeedColor(seedColor, design.colorPalette) + smallClock.assets.setSeedColor(seedColor, design.colorPalette) + largeClock.assets.setSeedColor(seedColor, design.colorPalette) + + smallClock.events.onSeedColorChanged(seedColor) + largeClock.events.onSeedColorChanged(seedColor) + } + + override fun onWeatherDataChanged(data: WeatherData) { + smallClock.events.onWeatherDataChanged(data) + largeClock.events.onWeatherDataChanged(data) + } + + override fun onAlarmDataChanged(data: AlarmData) { + smallClock.events.onAlarmDataChanged(data) + largeClock.events.onAlarmDataChanged(data) + } + + override fun onZenDataChanged(data: ZenData) { + smallClock.events.onZenDataChanged(data) + largeClock.events.onZenDataChanged(data) + } + + override fun onReactiveAxesChanged(axes: List<ClockReactiveSetting>) { + smallClock.events.onReactiveAxesChanged(axes) + largeClock.events.onReactiveAxesChanged(axes) + } + } + + override fun initialize(resources: Resources, dozeFraction: Float, foldFraction: Float) { + events.onColorPaletteChanged(resources) + smallClock.animations.doze(dozeFraction) + largeClock.animations.doze(dozeFraction) + smallClock.animations.fold(foldFraction) + largeClock.animations.fold(foldFraction) + smallClock.events.onTimeTick() + largeClock.events.onTimeTick() + } + + override fun dump(pw: PrintWriter) {} +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockFaceController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockFaceController.kt new file mode 100644 index 000000000000..ef398d1a52a0 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockFaceController.kt @@ -0,0 +1,314 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shared.clocks + +import android.content.Context +import android.content.res.Resources +import android.graphics.Rect +import android.view.Gravity +import android.view.View +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.widget.FrameLayout +import com.android.systemui.log.core.MessageBuffer +import com.android.systemui.plugins.clocks.AlarmData +import com.android.systemui.plugins.clocks.ClockAnimations +import com.android.systemui.plugins.clocks.ClockEvents +import com.android.systemui.plugins.clocks.ClockFaceConfig +import com.android.systemui.plugins.clocks.ClockFaceController +import com.android.systemui.plugins.clocks.ClockFaceEvents +import com.android.systemui.plugins.clocks.ClockFaceLayout +import com.android.systemui.plugins.clocks.ClockReactiveSetting +import com.android.systemui.plugins.clocks.ClockTickRate +import com.android.systemui.plugins.clocks.DefaultClockFaceLayout +import com.android.systemui.plugins.clocks.WeatherData +import com.android.systemui.plugins.clocks.ZenData +import com.android.systemui.shared.clocks.view.DigitalClockFaceView +import java.util.Locale +import java.util.TimeZone +import kotlin.math.max + +interface ClockEventUnion : ClockEvents, ClockFaceEvents + +class SimpleClockFaceController( + ctx: Context, + val assets: AssetLoader, + face: ClockFace, + isLargeClock: Boolean, + messageBuffer: MessageBuffer, +) : ClockFaceController { + override val view: View + override val config: ClockFaceConfig by lazy { + ClockFaceConfig( + hasCustomWeatherDataDisplay = layers.any { it.config.hasCustomWeatherDataDisplay }, + hasCustomPositionUpdatedAnimation = + layers.any { it.config.hasCustomPositionUpdatedAnimation }, + tickRate = getTickRate(), + useCustomClockScene = layers.any { it.config.useCustomClockScene }, + ) + } + + val layers = mutableListOf<SimpleClockLayerController>() + + val timespecHandler = DigitalTimespecHandler(DigitalTimespec.TIME_FULL_FORMAT, "hh:mm") + + init { + val lp = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) + lp.gravity = Gravity.CENTER + view = + if (face.layers.size == 1) { + // Optimize a clocks with a single layer by excluding the face level view group. We + // expect the view container from the host process to always be a FrameLayout. + val layer = face.layers[0] + val controller = + SimpleClockLayerController.Factory.create( + ctx, + assets, + layer, + isLargeClock, + messageBuffer, + ) + layers.add(controller) + controller.view.layoutParams = lp + controller.view + } else { + // For multiple views, we use an intermediate RelativeLayout so that we can do some + // intelligent laying out between the children views. + val group = SimpleClockRelativeLayout(ctx, face.faceLayout) + group.layoutParams = lp + group.gravity = Gravity.CENTER + group.clipChildren = false + for (layer in face.layers) { + face.faceLayout?.let { + if (layer is DigitalHandLayer) { + layer.faceLayout = it + } + } + val controller = + SimpleClockLayerController.Factory.create( + ctx, + assets, + layer, + isLargeClock, + messageBuffer, + ) + group.addView(controller.view) + layers.add(controller) + } + group + } + } + + override val layout: ClockFaceLayout = + DefaultClockFaceLayout(view).apply { + views[0].id = + if (isLargeClock) { + assets.getResourcesId("lockscreen_clock_view_large") + } else { + assets.getResourcesId("lockscreen_clock_view") + } + } + + override val events = + object : ClockEventUnion { + override var isReactiveTouchInteractionEnabled = false + get() = field + set(value) { + field = value + layers.forEach { it.events.isReactiveTouchInteractionEnabled = value } + } + + override fun onTimeTick() { + timespecHandler.updateTime() + if ( + config.tickRate == ClockTickRate.PER_MINUTE || + view.contentDescription != timespecHandler.getContentDescription() + ) { + view.contentDescription = timespecHandler.getContentDescription() + } + layers.forEach { it.faceEvents.onTimeTick() } + } + + override fun onTimeZoneChanged(timeZone: TimeZone) { + timespecHandler.timeZone = timeZone + layers.forEach { it.events.onTimeZoneChanged(timeZone) } + } + + override fun onTimeFormatChanged(is24Hr: Boolean) { + timespecHandler.is24Hr = is24Hr + layers.forEach { it.events.onTimeFormatChanged(is24Hr) } + } + + override fun onLocaleChanged(locale: Locale) { + timespecHandler.updateLocale(locale) + layers.forEach { it.events.onLocaleChanged(locale) } + } + + override fun onFontSettingChanged(fontSizePx: Float) { + layers.forEach { it.faceEvents.onFontSettingChanged(fontSizePx) } + } + + override fun onColorPaletteChanged(resources: Resources) { + layers.forEach { + it.events.onColorPaletteChanged(resources) + it.updateColors() + } + } + + override fun onSeedColorChanged(seedColor: Int?) { + layers.forEach { + it.events.onSeedColorChanged(seedColor) + it.updateColors() + } + } + + override fun onRegionDarknessChanged(isRegionDark: Boolean) { + layers.forEach { it.faceEvents.onRegionDarknessChanged(isRegionDark) } + } + + override fun onReactiveAxesChanged(axes: List<ClockReactiveSetting>) {} + + /** + * targetRegion passed to all customized clock applies counter translationY of + * KeyguardStatusView and keyguard_large_clock_top_margin from default clock + */ + override fun onTargetRegionChanged(targetRegion: Rect?) { + // When a clock needs to be aligned with screen, like weather clock + // it needs to offset back the translation of keyguard_large_clock_top_margin + if (view is DigitalClockFaceView && view.isAlignedWithScreen()) { + val topMargin = getKeyguardLargeClockTopMargin(assets) + targetRegion?.let { + val (_, yDiff) = computeLayoutDiff(view, it, isLargeClock) + // In LS, we use yDiff to counter translate + // the translation of KeyguardLargeClockTopMargin + // With the targetRegion passed from picker, + // we will have yDiff = 0, no translation is needed for weather clock + if (yDiff.toInt() != 0) view.translationY = yDiff - topMargin / 2 + } + return + } + + var maxWidth = 0f + var maxHeight = 0f + + for (layer in layers) { + layer.faceEvents.onTargetRegionChanged(targetRegion) + maxWidth = max(maxWidth, layer.view.layoutParams.width.toFloat()) + maxHeight = max(maxHeight, layer.view.layoutParams.height.toFloat()) + } + + val lp = + if (maxHeight <= 0 || maxWidth <= 0 || targetRegion == null) { + // No specified width/height. Just match parent size. + FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) + } else { + // Scale to fit in targetRegion based on largest child elements. + val ratio = maxWidth / maxHeight + val targetRatio = targetRegion.width() / targetRegion.height().toFloat() + val scale = + if (ratio > targetRatio) targetRegion.width() / maxWidth + else targetRegion.height() / maxHeight + + FrameLayout.LayoutParams( + (maxWidth * scale).toInt(), + (maxHeight * scale).toInt(), + ) + } + + lp.gravity = Gravity.CENTER + view.layoutParams = lp + targetRegion?.let { + val (xDiff, yDiff) = computeLayoutDiff(view, it, isLargeClock) + view.translationX = xDiff + view.translationY = yDiff + } + } + + override fun onSecondaryDisplayChanged(onSecondaryDisplay: Boolean) {} + + override fun onWeatherDataChanged(data: WeatherData) { + layers.forEach { it.events.onWeatherDataChanged(data) } + } + + override fun onAlarmDataChanged(data: AlarmData) { + layers.forEach { it.events.onAlarmDataChanged(data) } + } + + override fun onZenDataChanged(data: ZenData) { + layers.forEach { it.events.onZenDataChanged(data) } + } + } + + override val animations = + object : ClockAnimations { + override fun enter() { + layers.forEach { it.animations.enter() } + } + + override fun doze(fraction: Float) { + layers.forEach { it.animations.doze(fraction) } + } + + override fun fold(fraction: Float) { + layers.forEach { it.animations.fold(fraction) } + } + + override fun charge() { + layers.forEach { it.animations.charge() } + } + + override fun onPickerCarouselSwiping(swipingFraction: Float) { + face.pickerScale?.let { + view.scaleX = swipingFraction * (1 - it.scaleX) + it.scaleX + view.scaleY = swipingFraction * (1 - it.scaleY) + it.scaleY + } + if (!(view is DigitalClockFaceView && view.isAlignedWithScreen())) { + val topMargin = getKeyguardLargeClockTopMargin(assets) + view.translationY = topMargin / 2F * swipingFraction + } + layers.forEach { it.animations.onPickerCarouselSwiping(swipingFraction) } + view.invalidate() + } + + override fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) { + layers.forEach { it.animations.onPositionUpdated(fromLeft, direction, fraction) } + } + + override fun onPositionUpdated(distance: Float, fraction: Float) { + layers.forEach { it.animations.onPositionUpdated(distance, fraction) } + } + } + + private fun getTickRate(): ClockTickRate { + var tickRate = ClockTickRate.PER_MINUTE + for (layer in layers) { + if (layer.config.tickRate.value < tickRate.value) { + tickRate = layer.config.tickRate + } + } + return tickRate + } + + private fun getKeyguardLargeClockTopMargin(assets: AssetLoader): Int { + val topMarginRes = + assets.resolveResourceId(null, "dimen", "keyguard_large_clock_top_margin") + if (topMarginRes != null) { + val (res, id) = topMarginRes + return res.getDimensionPixelSize(id) + } + return 0 + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockLayerController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockLayerController.kt new file mode 100644 index 000000000000..f71543efa650 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockLayerController.kt @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shared.clocks + +import android.content.Context +import android.view.View +import androidx.annotation.VisibleForTesting +import com.android.systemui.log.core.MessageBuffer +import com.android.systemui.plugins.clocks.ClockAnimations +import com.android.systemui.plugins.clocks.ClockEvents +import com.android.systemui.plugins.clocks.ClockFaceConfig +import com.android.systemui.plugins.clocks.ClockFaceEvents +import com.android.systemui.shared.clocks.view.SimpleDigitalClockTextView +import kotlin.reflect.KClass + +typealias LayerControllerConstructor = + ( + ctx: Context, + assets: AssetLoader, + layer: ClockLayer, + isLargeClock: Boolean, + messageBuffer: MessageBuffer, + ) -> SimpleClockLayerController + +interface SimpleClockLayerController { + val view: View + val events: ClockEvents + val animations: ClockAnimations + val faceEvents: ClockFaceEvents + val config: ClockFaceConfig + + @VisibleForTesting var fakeTimeMills: Long? + + // Called immediately after either onColorPaletteChanged or onSeedColorChanged is called. + // Provided for convience to not duplicate color update logic after state updated. + fun updateColors() {} + + companion object Factory { + val constructorMap = mutableMapOf<Pair<KClass<*>, KClass<*>?>, LayerControllerConstructor>() + + internal inline fun <reified TLayer> registerConstructor( + noinline constructor: LayerControllerConstructor, + ) where TLayer : ClockLayer { + constructorMap[Pair(TLayer::class, null)] = constructor + } + + inline fun <reified TLayer, reified TStyle> registerTextConstructor( + noinline constructor: LayerControllerConstructor, + ) where TLayer : ClockLayer, TStyle : TextStyle { + constructorMap[Pair(TLayer::class, TStyle::class)] = constructor + } + + init { + registerConstructor<ComposedDigitalHandLayer>(::ComposedDigitalLayerController) + registerTextConstructor<DigitalHandLayer, FontTextStyle>(::createSimpleDigitalLayer) + } + + private fun createSimpleDigitalLayer( + ctx: Context, + assets: AssetLoader, + layer: ClockLayer, + isLargeClock: Boolean, + messageBuffer: MessageBuffer + ): SimpleClockLayerController { + val view = SimpleDigitalClockTextView(ctx, messageBuffer) + return SimpleDigitalHandLayerController( + ctx, + assets, + layer as DigitalHandLayer, + view, + messageBuffer + ) + } + + fun create( + ctx: Context, + assets: AssetLoader, + layer: ClockLayer, + isLargeClock: Boolean, + messageBuffer: MessageBuffer + ): SimpleClockLayerController { + val styleClass = if (layer is DigitalHandLayer) layer.style::class else null + val key = Pair(layer::class, styleClass) + return constructorMap[key]?.invoke(ctx, assets, layer, isLargeClock, messageBuffer) + ?: throw IllegalArgumentException("Unrecognized ClockLayer type: $key") + } + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockRelativeLayout.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockRelativeLayout.kt new file mode 100644 index 000000000000..6e1b9aabf86d --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockRelativeLayout.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shared.clocks + +import android.content.Context +import android.view.View.MeasureSpec.EXACTLY +import android.widget.RelativeLayout +import androidx.core.view.children +import com.android.systemui.shared.clocks.view.SimpleDigitalClockView + +class SimpleClockRelativeLayout(context: Context, val faceLayout: DigitalFaceLayout?) : + RelativeLayout(context) { + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + // For migrate_clocks_to_blueprint, mode is EXACTLY + // when the flag is turned off, we won't execute this codes + if (MeasureSpec.getMode(heightMeasureSpec) == EXACTLY) { + if ( + faceLayout == DigitalFaceLayout.TWO_PAIRS_VERTICAL || + faceLayout == DigitalFaceLayout.FOUR_DIGITS_ALIGN_CENTER + ) { + val constrainedHeight = MeasureSpec.getSize(heightMeasureSpec) / 2F + children.forEach { + // The assumption here is the height of text view is linear to font size + (it as SimpleDigitalClockView).applyTextSize( + constrainedHeight, + constrainedByHeight = true, + ) + } + } + } + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt new file mode 100644 index 000000000000..a3240f81e499 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt @@ -0,0 +1,328 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shared.clocks + +import android.content.Context +import android.content.res.Resources +import android.graphics.Rect +import android.view.View +import android.view.ViewGroup +import android.widget.RelativeLayout +import androidx.annotation.VisibleForTesting +import com.android.systemui.customization.R +import com.android.systemui.log.core.Logger +import com.android.systemui.log.core.MessageBuffer +import com.android.systemui.plugins.clocks.AlarmData +import com.android.systemui.plugins.clocks.ClockAnimations +import com.android.systemui.plugins.clocks.ClockEvents +import com.android.systemui.plugins.clocks.ClockFaceConfig +import com.android.systemui.plugins.clocks.ClockFaceEvents +import com.android.systemui.plugins.clocks.ClockReactiveSetting +import com.android.systemui.plugins.clocks.WeatherData +import com.android.systemui.plugins.clocks.ZenData +import com.android.systemui.shared.clocks.view.SimpleDigitalClockView +import java.util.Locale +import java.util.TimeZone + +private val TAG = SimpleDigitalHandLayerController::class.simpleName!! + +open class SimpleDigitalHandLayerController<T>( + private val ctx: Context, + private val assets: AssetLoader, + private val layer: DigitalHandLayer, + override val view: T, + messageBuffer: MessageBuffer, +) : SimpleClockLayerController where T : View, T : SimpleDigitalClockView { + private val logger = Logger(messageBuffer, TAG) + val timespec = DigitalTimespecHandler(layer.timespec, layer.dateTimeFormat) + + @VisibleForTesting + fun hasLeadingZero() = layer.dateTimeFormat.startsWith("hh") || timespec.is24Hr + + @VisibleForTesting + override var fakeTimeMills: Long? + get() = timespec.fakeTimeMills + set(value) { + timespec.fakeTimeMills = value + } + + override val config = ClockFaceConfig() + var dozeState: DefaultClockController.AnimationState? = null + var isRegionDark: Boolean = true + + init { + view.layoutParams = + RelativeLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + if (layer.alignment != null) { + layer.alignment.verticalAlignment?.let { view.verticalAlignment = it } + layer.alignment.horizontalAlignment?.let { view.horizontalAlignment = it } + } + view.applyStyles(assets, layer.style, layer.aodStyle) + view.id = + ctx.resources.getIdentifier( + generateDigitalLayerIdString(layer), + "id", + ctx.getPackageName(), + ) + } + + fun applyLayout(layout: DigitalFaceLayout?) { + when (layout) { + DigitalFaceLayout.FOUR_DIGITS_ALIGN_CENTER, + DigitalFaceLayout.FOUR_DIGITS_HORIZONTAL -> applyFourDigitsLayout(layout) + DigitalFaceLayout.TWO_PAIRS_HORIZONTAL, + DigitalFaceLayout.TWO_PAIRS_VERTICAL -> applyTwoPairsLayout(layout) + else -> { + // one view always use FrameLayout + // no need to change here + } + } + applyMargin() + } + + private fun applyMargin() { + if (view.layoutParams is RelativeLayout.LayoutParams) { + val lp = view.layoutParams as RelativeLayout.LayoutParams + layer.marginRatio?.let { + lp.setMargins( + /* left = */ (it.left * view.measuredWidth).toInt(), + /* top = */ (it.top * view.measuredHeight).toInt(), + /* right = */ (it.right * view.measuredWidth).toInt(), + /* bottom = */ (it.bottom * view.measuredHeight).toInt(), + ) + } + view.layoutParams = lp + } + } + + private fun applyTwoPairsLayout(twoPairsLayout: DigitalFaceLayout) { + val lp = view.layoutParams as RelativeLayout.LayoutParams + lp.addRule(RelativeLayout.TEXT_ALIGNMENT_CENTER) + if (twoPairsLayout == DigitalFaceLayout.TWO_PAIRS_HORIZONTAL) { + when (view.id) { + R.id.HOUR_DIGIT_PAIR -> { + lp.addRule(RelativeLayout.CENTER_VERTICAL) + lp.addRule(RelativeLayout.ALIGN_PARENT_START) + } + R.id.MINUTE_DIGIT_PAIR -> { + lp.addRule(RelativeLayout.CENTER_VERTICAL) + lp.addRule(RelativeLayout.END_OF, R.id.HOUR_DIGIT_PAIR) + } + else -> { + throw Exception("cannot apply two pairs layout to view ${view.id}") + } + } + } else { + when (view.id) { + R.id.HOUR_DIGIT_PAIR -> { + lp.addRule(RelativeLayout.CENTER_HORIZONTAL) + lp.addRule(RelativeLayout.ALIGN_PARENT_TOP) + } + R.id.MINUTE_DIGIT_PAIR -> { + lp.addRule(RelativeLayout.CENTER_HORIZONTAL) + lp.addRule(RelativeLayout.BELOW, R.id.HOUR_DIGIT_PAIR) + } + else -> { + throw Exception("cannot apply two pairs layout to view ${view.id}") + } + } + } + view.layoutParams = lp + } + + private fun applyFourDigitsLayout(fourDigitsfaceLayout: DigitalFaceLayout) { + val lp = view.layoutParams as RelativeLayout.LayoutParams + when (fourDigitsfaceLayout) { + DigitalFaceLayout.FOUR_DIGITS_ALIGN_CENTER -> { + when (view.id) { + R.id.HOUR_FIRST_DIGIT -> { + lp.addRule(RelativeLayout.ALIGN_PARENT_START) + lp.addRule(RelativeLayout.ALIGN_PARENT_TOP) + } + R.id.HOUR_SECOND_DIGIT -> { + lp.addRule(RelativeLayout.END_OF, R.id.HOUR_FIRST_DIGIT) + lp.addRule(RelativeLayout.ALIGN_TOP, R.id.HOUR_FIRST_DIGIT) + } + R.id.MINUTE_FIRST_DIGIT -> { + lp.addRule(RelativeLayout.ALIGN_START, R.id.HOUR_FIRST_DIGIT) + lp.addRule(RelativeLayout.BELOW, R.id.HOUR_FIRST_DIGIT) + } + R.id.MINUTE_SECOND_DIGIT -> { + lp.addRule(RelativeLayout.ALIGN_START, R.id.HOUR_SECOND_DIGIT) + lp.addRule(RelativeLayout.BELOW, R.id.HOUR_SECOND_DIGIT) + } + else -> { + throw Exception("cannot apply four digits layout to view ${view.id}") + } + } + } + DigitalFaceLayout.FOUR_DIGITS_HORIZONTAL -> { + when (view.id) { + R.id.HOUR_FIRST_DIGIT -> { + lp.addRule(RelativeLayout.CENTER_VERTICAL) + lp.addRule(RelativeLayout.ALIGN_PARENT_START) + } + R.id.HOUR_SECOND_DIGIT -> { + lp.addRule(RelativeLayout.CENTER_VERTICAL) + lp.addRule(RelativeLayout.END_OF, R.id.HOUR_FIRST_DIGIT) + } + R.id.MINUTE_FIRST_DIGIT -> { + lp.addRule(RelativeLayout.CENTER_VERTICAL) + lp.addRule(RelativeLayout.END_OF, R.id.HOUR_SECOND_DIGIT) + } + R.id.MINUTE_SECOND_DIGIT -> { + lp.addRule(RelativeLayout.CENTER_VERTICAL) + lp.addRule(RelativeLayout.END_OF, R.id.MINUTE_FIRST_DIGIT) + } + else -> { + throw Exception("cannot apply FOUR_DIGITS_HORIZONTAL to view ${view.id}") + } + } + } + else -> { + throw IllegalArgumentException( + "applyFourDigitsLayout function should not " + + "have parameters as ${layer.faceLayout}" + ) + } + } + if (lp == view.layoutParams) { + return + } + view.layoutParams = lp + } + + fun refreshTime() { + timespec.updateTime() + val text = timespec.getDigitString() + if (view.text != text) { + view.text = text + view.refreshTime() + logger.d({ "refreshTime: new text=$str1" }) { str1 = text } + } + } + + override val events = + object : ClockEvents { + override var isReactiveTouchInteractionEnabled = false + + override fun onLocaleChanged(locale: Locale) { + timespec.updateLocale(locale) + refreshTime() + } + + /** Call whenever the text time format changes (12hr vs 24hr) */ + override fun onTimeFormatChanged(is24Hr: Boolean) { + timespec.is24Hr = is24Hr + refreshTime() + } + + override fun onTimeZoneChanged(timeZone: TimeZone) { + timespec.timeZone = timeZone + refreshTime() + } + + override fun onColorPaletteChanged(resources: Resources) {} + + override fun onSeedColorChanged(seedColor: Int?) {} + + override fun onWeatherDataChanged(data: WeatherData) {} + + override fun onAlarmDataChanged(data: AlarmData) {} + + override fun onZenDataChanged(data: ZenData) {} + + override fun onReactiveAxesChanged(axes: List<ClockReactiveSetting>) {} + } + + override fun updateColors() { + view.updateColors(assets, isRegionDark) + refreshTime() + } + + override val animations = + object : ClockAnimations { + override fun enter() { + applyLayout(layer.faceLayout) + refreshTime() + } + + override fun doze(fraction: Float) { + if (dozeState == null) { + dozeState = DefaultClockController.AnimationState(fraction) + view.animateDoze(dozeState!!.isActive, false) + } else { + val (hasChanged, hasJumped) = dozeState!!.update(fraction) + if (hasChanged) view.animateDoze(dozeState!!.isActive, !hasJumped) + } + view.dozeFraction = fraction + } + + override fun fold(fraction: Float) { + applyLayout(layer.faceLayout) + refreshTime() + } + + override fun charge() { + view.animateCharge() + } + + override fun onPickerCarouselSwiping(swipingFraction: Float) {} + + override fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) {} + + override fun onPositionUpdated(distance: Float, fraction: Float) {} + } + + override val faceEvents = + object : ClockFaceEvents { + override fun onTimeTick() { + refreshTime() + if ( + layer.timespec == DigitalTimespec.TIME_FULL_FORMAT || + layer.timespec == DigitalTimespec.DATE_FORMAT + ) { + view.contentDescription = timespec.getContentDescription() + } + } + + override fun onFontSettingChanged(fontSizePx: Float) { + view.applyTextSize(fontSizePx) + applyMargin() + } + + override fun onRegionDarknessChanged(isRegionDark: Boolean) { + this@SimpleDigitalHandLayerController.isRegionDark = isRegionDark + updateColors() + } + + override fun onTargetRegionChanged(targetRegion: Rect?) {} + + override fun onSecondaryDisplayChanged(onSecondaryDisplay: Boolean) {} + } + + companion object { + private val DEFAULT_LIGHT_COLOR = "@android:color/system_accent1_100+0" + private val DEFAULT_DARK_COLOR = "@android:color/system_accent2_600+0" + + fun getDefaultColor(assets: AssetLoader, isRegionDark: Boolean) = + assets.readColor(if (isRegionDark) DEFAULT_LIGHT_COLOR else DEFAULT_DARK_COLOR) + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/TimespecHandler.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/TimespecHandler.kt new file mode 100644 index 000000000000..ed6a403a7c7e --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/TimespecHandler.kt @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shared.clocks + +import android.icu.text.DateFormat +import android.icu.text.SimpleDateFormat +import android.icu.util.TimeZone as IcuTimeZone +import android.icu.util.ULocale +import androidx.annotation.VisibleForTesting +import java.util.Calendar +import java.util.Locale +import java.util.TimeZone + +open class TimespecHandler( + val cal: Calendar, +) { + var timeZone: TimeZone + get() = cal.timeZone + set(value) { + cal.timeZone = value + onTimeZoneChanged() + } + + @VisibleForTesting var fakeTimeMills: Long? = null + + fun updateTime() { + var timeMs = fakeTimeMills ?: System.currentTimeMillis() + cal.timeInMillis = (timeMs * TIME_TRAVEL_SCALE).toLong() + } + + protected open fun onTimeZoneChanged() {} + + companion object { + // Modifying this will cause the clock to run faster or slower. This is a useful way of + // manually checking that clocks are correctly animating through time. + private const val TIME_TRAVEL_SCALE = 1.0 + } +} + +class DigitalTimespecHandler( + val timespec: DigitalTimespec, + private val timeFormat: String, + cal: Calendar = Calendar.getInstance(), +) : TimespecHandler(cal) { + var is24Hr = false + set(value) { + field = value + applyPattern() + } + + private var dateFormat = updateSimpleDateFormat(Locale.getDefault()) + private var contentDescriptionFormat = getContentDescriptionFormat(Locale.getDefault()) + + init { + applyPattern() + } + + override fun onTimeZoneChanged() { + dateFormat.timeZone = IcuTimeZone.getTimeZone(cal.timeZone.id) + contentDescriptionFormat?.timeZone = IcuTimeZone.getTimeZone(cal.timeZone.id) + applyPattern() + } + + fun updateLocale(locale: Locale) { + dateFormat = updateSimpleDateFormat(locale) + contentDescriptionFormat = getContentDescriptionFormat(locale) + onTimeZoneChanged() + } + + private fun updateSimpleDateFormat(locale: Locale): DateFormat { + if ( + locale.language.equals(Locale.ENGLISH.language) || + timespec != DigitalTimespec.DATE_FORMAT + ) { + // force date format in English, and time format to use format defined in json + return SimpleDateFormat(timeFormat, timeFormat, ULocale.forLocale(locale)) + } else { + return SimpleDateFormat.getInstanceForSkeleton(timeFormat, locale) + } + } + + private fun getContentDescriptionFormat(locale: Locale): DateFormat? { + return when (timespec) { + DigitalTimespec.TIME_FULL_FORMAT -> + SimpleDateFormat.getInstanceForSkeleton("hh:mm", locale) + DigitalTimespec.DATE_FORMAT -> + SimpleDateFormat.getInstanceForSkeleton("EEEE MMMM d", locale) + else -> { + null + } + } + } + + private fun applyPattern() { + val timeFormat24Hour = timeFormat.replace("hh", "h").replace("h", "HH") + val format = if (is24Hr) timeFormat24Hour else timeFormat + if (timespec != DigitalTimespec.DATE_FORMAT) { + (dateFormat as SimpleDateFormat).applyPattern(format) + (contentDescriptionFormat as? SimpleDateFormat)?.applyPattern( + if (is24Hr) CONTENT_DESCRIPTION_TIME_FORMAT_24_HOUR + else CONTENT_DESCRIPTION_TIME_FORMAT_12_HOUR + ) + } + } + + private fun getSingleDigit(): String { + val isFirstDigit = timespec == DigitalTimespec.FIRST_DIGIT + val text = dateFormat.format(cal.time).toString() + return text.substring( + if (isFirstDigit) 0 else text.length - 1, + if (isFirstDigit) text.length - 1 else text.length + ) + } + + fun getDigitString(): String { + return when (timespec) { + DigitalTimespec.FIRST_DIGIT, + DigitalTimespec.SECOND_DIGIT -> getSingleDigit() + DigitalTimespec.DIGIT_PAIR -> { + dateFormat.format(cal.time).toString() + } + DigitalTimespec.TIME_FULL_FORMAT -> { + dateFormat.format(cal.time).toString() + } + DigitalTimespec.DATE_FORMAT -> { + dateFormat.format(cal.time).toString().uppercase() + } + } + } + + fun getContentDescription(): String? { + return when (timespec) { + DigitalTimespec.TIME_FULL_FORMAT, + DigitalTimespec.DATE_FORMAT -> { + contentDescriptionFormat?.format(cal.time).toString() + } + else -> { + return null + } + } + } + + companion object { + const val CONTENT_DESCRIPTION_TIME_FORMAT_12_HOUR = "hh:mm" + const val CONTENT_DESCRIPTION_TIME_FORMAT_24_HOUR = "HH:mm" + } +} diff --git a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java index 831543da3237..ef172a1b24f6 100644 --- a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java +++ b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java @@ -69,7 +69,9 @@ public abstract class ClockRegistryModule { layoutInflater, resources, featureFlags.isEnabled(Flags.STEP_CLOCK_ANIMATION), - MigrateClocksToBlueprint.isEnabled()), + MigrateClocksToBlueprint.isEnabled(), + com.android.systemui.Flags.clockReactiveVariants() + ), context.getString(R.string.lockscreen_clock_id_fallback), clockBuffers, /* keepAllLoaded = */ false, |