blob: 19ac2e479bcb3e234508b64c1913beca9aeed3b5 [file] [log] [blame]
/*
* Copyright (C) 2022 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.Color
import android.graphics.drawable.Drawable
import android.icu.text.NumberFormat
import android.util.TypedValue
import android.view.LayoutInflater
import android.widget.FrameLayout
import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.plugins.Clock
import com.android.systemui.plugins.ClockAnimations
import com.android.systemui.plugins.ClockEvents
import com.android.systemui.plugins.ClockId
import com.android.systemui.plugins.ClockMetadata
import com.android.systemui.plugins.ClockProvider
import com.android.systemui.shared.R
import java.io.PrintWriter
import java.util.Locale
import java.util.TimeZone
import javax.inject.Inject
private val TAG = DefaultClockProvider::class.simpleName
const val DEFAULT_CLOCK_NAME = "Default Clock"
const val DEFAULT_CLOCK_ID = "DEFAULT"
/** Provides the default system clock */
class DefaultClockProvider @Inject constructor(
val ctx: Context,
val layoutInflater: LayoutInflater,
@Main val resources: Resources
) : ClockProvider {
override fun getClocks(): List<ClockMetadata> =
listOf(ClockMetadata(DEFAULT_CLOCK_ID, DEFAULT_CLOCK_NAME))
override fun createClock(id: ClockId): Clock {
if (id != DEFAULT_CLOCK_ID) {
throw IllegalArgumentException("$id is unsupported by $TAG")
}
return DefaultClock(ctx, layoutInflater, resources)
}
override fun getClockThumbnail(id: ClockId): Drawable? {
if (id != DEFAULT_CLOCK_ID) {
throw IllegalArgumentException("$id is unsupported by $TAG")
}
// TODO: Update placeholder to actual resource
return resources.getDrawable(R.drawable.clock_default_thumbnail, null)
}
}
/**
* Controls the default clock visuals.
*
* This serves as an adapter between the clock interface and the
* AnimatableClockView used by the existing lockscreen clock.
*/
class DefaultClock(
ctx: Context,
private val layoutInflater: LayoutInflater,
private val resources: Resources
) : Clock {
override val smallClock: AnimatableClockView
override val largeClock: AnimatableClockView
private val clocks get() = listOf(smallClock, largeClock)
private val burmeseNf = NumberFormat.getInstance(Locale.forLanguageTag("my"))
private val burmeseNumerals = burmeseNf.format(FORMAT_NUMBER.toLong())
private val burmeseLineSpacing =
resources.getFloat(R.dimen.keyguard_clock_line_spacing_scale_burmese)
private val defaultLineSpacing = resources.getFloat(R.dimen.keyguard_clock_line_spacing_scale)
override val events: ClockEvents
override lateinit var animations: ClockAnimations
private set
private var smallRegionDarkness = false
private var largeRegionDarkness = false
init {
val parent = FrameLayout(ctx)
smallClock = layoutInflater.inflate(
R.layout.clock_default_small,
parent,
false
) as AnimatableClockView
largeClock = layoutInflater.inflate(
R.layout.clock_default_large,
parent,
false
) as AnimatableClockView
events = DefaultClockEvents()
animations = DefaultClockAnimations(0f, 0f)
events.onLocaleChanged(Locale.getDefault())
// DOZE_COLOR is a placeholder, and will be assigned correctly in initialize
clocks.forEach { it.setColors(DOZE_COLOR, DOZE_COLOR) }
}
override fun initialize(resources: Resources, dozeFraction: Float, foldFraction: Float) {
recomputePadding()
animations = DefaultClockAnimations(dozeFraction, foldFraction)
events.onColorPaletteChanged(resources, true, true)
events.onTimeZoneChanged(TimeZone.getDefault())
events.onTimeTick()
}
inner class DefaultClockEvents() : ClockEvents {
override fun onTimeTick() = clocks.forEach { it.refreshTime() }
override fun onTimeFormatChanged(is24Hr: Boolean) =
clocks.forEach { it.refreshFormat(is24Hr) }
override fun onTimeZoneChanged(timeZone: TimeZone) =
clocks.forEach { it.onTimeZoneChanged(timeZone) }
override fun onFontSettingChanged() {
smallClock.setTextSize(
TypedValue.COMPLEX_UNIT_PX,
resources.getDimensionPixelSize(R.dimen.small_clock_text_size).toFloat()
)
largeClock.setTextSize(
TypedValue.COMPLEX_UNIT_PX,
resources.getDimensionPixelSize(R.dimen.large_clock_text_size).toFloat()
)
recomputePadding()
}
override fun onColorPaletteChanged(
resources: Resources,
smallClockIsDark: Boolean,
largeClockIsDark: Boolean
) {
if (smallRegionDarkness != smallClockIsDark) {
smallRegionDarkness = smallClockIsDark
updateClockColor(smallClock, smallClockIsDark)
}
if (largeRegionDarkness != largeClockIsDark) {
largeRegionDarkness = largeClockIsDark
updateClockColor(largeClock, largeClockIsDark)
}
}
override fun onLocaleChanged(locale: Locale) {
val nf = NumberFormat.getInstance(locale)
if (nf.format(FORMAT_NUMBER.toLong()) == burmeseNumerals) {
clocks.forEach { it.setLineSpacingScale(burmeseLineSpacing) }
} else {
clocks.forEach { it.setLineSpacingScale(defaultLineSpacing) }
}
clocks.forEach { it.refreshFormat() }
}
}
inner class DefaultClockAnimations(
dozeFraction: Float,
foldFraction: Float
) : ClockAnimations {
private var foldState = AnimationState(0f)
private var dozeState = AnimationState(0f)
init {
dozeState = AnimationState(dozeFraction)
foldState = AnimationState(foldFraction)
if (foldState.isActive) {
clocks.forEach { it.animateFoldAppear(false) }
} else {
clocks.forEach { it.animateDoze(dozeState.isActive, false) }
}
}
override fun enter() {
if (!dozeState.isActive) {
clocks.forEach { it.animateAppearOnLockscreen() }
}
}
override fun charge() = clocks.forEach { it.animateCharge { dozeState.isActive } }
override fun fold(fraction: Float) {
val (hasChanged, hasJumped) = foldState.update(fraction)
if (hasChanged) {
clocks.forEach { it.animateFoldAppear(!hasJumped) }
}
}
override fun doze(fraction: Float) {
val (hasChanged, hasJumped) = dozeState.update(fraction)
if (hasChanged) {
clocks.forEach { it.animateDoze(dozeState.isActive, !hasJumped) }
}
}
}
private class AnimationState(
var fraction: Float
) {
var isActive: Boolean = fraction < 0.5f
fun update(newFraction: Float): Pair<Boolean, Boolean> {
val wasActive = isActive
val hasJumped = (fraction == 0f && newFraction == 1f) ||
(fraction == 1f && newFraction == 0f)
isActive = newFraction > fraction
fraction = newFraction
return Pair(wasActive != isActive, hasJumped)
}
}
private fun updateClockColor(clock: AnimatableClockView, isRegionDark: Boolean) {
val color = if (isRegionDark) {
resources.getColor(android.R.color.system_accent1_100)
} else {
resources.getColor(android.R.color.system_accent2_600)
}
clock.setColors(DOZE_COLOR, color)
clock.animateAppearOnLockscreen()
}
private fun recomputePadding() {
val lp = largeClock.getLayoutParams() as FrameLayout.LayoutParams
lp.topMargin = (-0.5f * largeClock.bottom).toInt()
largeClock.setLayoutParams(lp)
}
override fun dump(pw: PrintWriter) = clocks.forEach { it.dump(pw) }
companion object {
@VisibleForTesting const val DOZE_COLOR = Color.WHITE
private const val FORMAT_NUMBER = 1234567890
}
}