WatchFace.kt

/*
 * Copyright 2020 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 androidx.wear.watchface

import android.annotation.SuppressLint
import android.app.NotificationManager
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.graphics.Point
import android.graphics.Rect
import android.icu.util.Calendar
import android.icu.util.TimeZone
import android.support.wearable.complications.ComplicationData
import android.support.wearable.watchface.WatchFaceStyle
import android.view.SurfaceHolder
import android.view.ViewConfiguration
import androidx.annotation.ColorInt
import androidx.annotation.IntDef
import androidx.annotation.RestrictTo
import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
import androidx.annotation.UiThread
import androidx.annotation.VisibleForTesting
import androidx.wear.complications.SystemProviders
import androidx.wear.watchface.data.RenderParametersWireFormat
import androidx.wear.watchface.style.UserStyle
import androidx.wear.watchface.style.UserStyleRepository
import androidx.wear.watchface.style.data.UserStyleWireFormat
import androidx.wear.watchface.ui.WatchFaceConfigActivity
import androidx.wear.watchface.ui.WatchFaceConfigDelegate
import java.io.FileNotFoundException
import java.io.InputStreamReader
import java.security.InvalidParameterException
import kotlin.math.max

// Human reaction time is limited to ~100ms.
private const val MIN_PERCEPTABLE_DELAY_MILLIS = 100

/**
 * The type of watch face, whether it's digital or analog. This influences the time displayed for
 * remote previews.
 *
 * @hide
 */
@IntDef(
    value = [
        WatchFaceType.DIGITAL,
        WatchFaceType.ANALOG
    ]
)
public annotation class WatchFaceType {
    public companion object {
        /* The WatchFace has an analog time display. */
        public const val ANALOG: Int = 0

        /* The WatchFace has a digital time display. */
        public const val DIGITAL: Int = 1
    }
}

private fun readPrefs(context: Context, fileName: String): UserStyleWireFormat {
    val hashMap = HashMap<String, String>()
    try {
        val reader = InputStreamReader(context.openFileInput(fileName)).buffered()
        while (true) {
            val key = reader.readLine() ?: break
            val value = reader.readLine() ?: break
            hashMap[key] = value
        }
        reader.close()
    } catch (e: FileNotFoundException) {
        // We don't need to do anything special here.
    }
    return UserStyleWireFormat(hashMap)
}

private fun writePrefs(context: Context, fileName: String, style: UserStyle) {
    val writer = context.openFileOutput(fileName, Context.MODE_PRIVATE).bufferedWriter()
    for ((key, value) in style.selectedOptions) {
        writer.write(key.id)
        writer.newLine()
        writer.write(value.id)
        writer.newLine()
    }
    writer.close()
}

/**
 * A WatchFace is constructed by a user's [WatchFaceService] and brings together rendering,
 * styling, complications and state observers.
 */
@SuppressLint("SyntheticAccessor")
public class WatchFace private constructor(
    internal val previewReferenceTimeMillis: Long,
    private var interactiveUpdateRateMillis: Long,
    internal val userStyleRepository: UserStyleRepository,
    internal var complicationsManager: ComplicationsManager,
    internal val renderer: Renderer,
    private val watchFaceHostApi: WatchFaceHostApi,
    private val watchState: WatchState,
    // Not to be confused with a user style.
    internal val watchFaceStyle: WatchFaceStyle,
    private val componentName: ComponentName,
    private val systemTimeProvider: SystemTimeProvider
) {
    /**
     * Interface for getting the current system time.
     * @hide
     */
    @RestrictTo(LIBRARY_GROUP)
    public interface SystemTimeProvider {
        /** Returns the current system time in milliseconds. */
        public fun getSystemTimeMillis(): Long
    }

    /**
     * Builder for a [WatchFace].
     *
     * If unreadCountIndicator or notificationIndicator are hidden then the WatchState class will
     * receive updates necessary for the watch to draw its own indicators.
     */
    public class Builder(
        /**
         * The type of watch face, whether it's digital or analog. Used to determine the
         * default time for editor preview screenshots.
         */
        @WatchFaceType watchFaceType: Int,

        /**
         * The interval in milliseconds between frames in interactive mode. To render at 60hz pass in
         * 16. Note when battery is low, the framerate will be clamped to 10fps. Watch faces are
         * recommended to use lower frame rates if possible for better battery life.
         */
        private var interactiveUpdateRateMillis: Long,

        /** The {@UserStyleRepository} for this WatchFace. */
        internal val userStyleRepository: UserStyleRepository,

        /** The [ComplicationsManager] for this WatchFace. */
        internal var complicationsManager: ComplicationsManager,

        /** The [Renderer] for this WatchFace. */
        internal val renderer: Renderer,

        /** Holder for the internal API the WatchFace uses to communicate with the host service.  */
        private val watchFaceHost: WatchFaceHost,

        /**
         * The [WatchState] of the device we're running on. Contains data needed to draw
         * surface indicators if we've opted to draw them ourselves (see [onCreateWatchFaceStyle]).
         */
        private val watchState: WatchState
    ) {
        private var viewProtectionMode: Int = 0
        private var statusBarGravity: Int = 0
        private var previewReferenceTimeMillis: Long =
            when (watchFaceType) {
                WatchFaceType.ANALOG -> ANALOG_WATCHFACE_REFERENCE_TIME_MS
                WatchFaceType.DIGITAL -> DIGITAL_WATCHFACE_REFERENCE_TIME_MS
                else -> throw InvalidParameterException("Unrecognized watchFaceType")
            }

        @ColorInt
        private var accentColor: Int = WatchFaceStyle.DEFAULT_ACCENT_COLOR
        private var acceptsTapEvents: Boolean = true
        private var systemTimeProvider: SystemTimeProvider = object : SystemTimeProvider {
            override fun getSystemTimeMillis() = System.currentTimeMillis()
        }

        /**
         * Overrides the reference time for editor preview images.
         *
         * @param previewReferenceTimeMillis The preview time in milliseconds since the epoch
         */
        public fun setPreviewReferenceTimeMillis(
            previewReferenceTimeMillis: Long
        ): Builder = apply {
            this.previewReferenceTimeMillis = previewReferenceTimeMillis
        }

        /**
         * Only has an impact on devices running Wear 2.x, on other devices this is a no-op and the
         * functionality is replaced by... TODO(alexclarke): Design the replacement.
         *
         * @param viewProtectionMode The view protection mode bit field, must be a combination of
         *     zero or more of [PROTECT_STATUS_BAR], [PROTECT_HOTWORD_INDICATOR],
         *     [PROTECT_WHOLE_SCREEN].
         * @throws IllegalArgumentException if viewProtectionMode has an unexpected value
         */
        public fun setWear2ViewProtectionMode(viewProtectionMode: Int): Builder = apply {
            if (viewProtectionMode < 0 ||
                viewProtectionMode >
                WatchFaceStyle.PROTECT_STATUS_BAR + WatchFaceStyle.PROTECT_HOTWORD_INDICATOR +
                WatchFaceStyle.PROTECT_WHOLE_SCREEN
            ) {
                throw IllegalArgumentException(
                    "View protection must be combination " +
                        "PROTECT_STATUS_BAR, PROTECT_HOTWORD_INDICATOR or PROTECT_WHOLE_SCREEN"
                )
            }
            this.viewProtectionMode = viewProtectionMode
        }

        /**
         * Sets position of status icons (battery state, lack of connection) on the screen.
         *
         * <p>Only has an impact on devices running Wear 2.x, on other devices this is a no-op and
         * the functionality is replaced by... TODO(alexclarke): Design the replacement.
         *
         * @param statusBarGravity This must be any combination of horizontal Gravity constant
         *     ([Gravity.LEFT], [Gravity.CENTER_HORIZONTAL], [Gravity.RIGHT])
         *     and vertical Gravity constants ([Gravity.TOP], [Gravity,CENTER_VERTICAL},
         *     [Gravity,BOTTOM]), e.g. {@code Gravity.LEFT | Gravity.BOTTOM}. On circular screens,
         *     only the vertical gravity is respected.
         */
        public fun setWear2StatusBarGravity(statusBarGravity: Int): Builder = apply {
            this.statusBarGravity = statusBarGravity
        }

        /**
         * Sets the accent color which can be set by developers to customise watch face. It will be
         * used when drawing the unread notification indicator. Default color is white.
         *
         * <p>Only has an impact on devices running Wear 2.x, on other devices this is a no-op and
         * the functionality is replaced by... TODO(alexclarke): Design the replacement.
         */
        public fun setWear2AccentColor(@ColorInt accentColor: Int): Builder = apply {
            this.accentColor = accentColor
        }

        /**
         * Sets whether this watchface accepts tap events. The default is false.
         *
         * <p>Only has an impact on devices running Wear 2.x, on other devices this is a no-op and
         * the functionality is replaced by... TODO(alexclarke): Design the replacement.
         *
         * <p>Watchfaces that set this {@code true} are indicating they are prepared to receive
         * [android.support.wearable.watchface.WatchFaceService.TAP_TYPE_TOUCH],
         * [android.support.wearable.watchface.WatchFaceService.TAP_TYPE_TOUCH_CANCEL], and
         * [android.support.wearable.watchface.WatchFaceService.TAP_TYPE_TAP] events.
         *
         * @param acceptsTapEvents whether to receive touch events.
         */
        public fun setWear2AcceptsTapEvents(acceptsTapEvents: Boolean): Builder = apply {
            this.acceptsTapEvents = acceptsTapEvents
        }

        /** @hide */
        @RestrictTo(LIBRARY_GROUP)
        public fun setSystemTimeProvider(systemTimeProvider: SystemTimeProvider): Builder = apply {
            this.systemTimeProvider = systemTimeProvider
        }

        /** Constructs the [WatchFace]. */
        public fun build(): WatchFace {
            val componentName =
                ComponentName(
                    watchFaceHost.api!!.getContext().packageName,
                    watchFaceHost.api!!.getContext().javaClass.typeName
                )
            return WatchFace(
                previewReferenceTimeMillis,
                interactiveUpdateRateMillis,
                userStyleRepository,
                complicationsManager,
                renderer,
                watchFaceHost.api!!,
                watchState,
                WatchFaceStyle(
                    componentName,
                    viewProtectionMode,
                    statusBarGravity,
                    accentColor,
                    false,
                    false,
                    acceptsTapEvents
                ),
                componentName,
                systemTimeProvider
            )
        }
    }

    internal companion object {
        // Reference time for editor screenshots for analog watch faces.
        // 2020/10/10 at 09:30 Note the date doesn't matter, only the hour.
        internal const val ANALOG_WATCHFACE_REFERENCE_TIME_MS = 1602318600000L

        // Reference time for editor screenshots for digital watch faces.
        // 2020/10/10 at 10:10 Note the date doesn't matter, only the hour.
        internal const val DIGITAL_WATCHFACE_REFERENCE_TIME_MS = 1602321000000L

        internal const val NO_DEFAULT_PROVIDER = SystemProviders.NO_PROVIDER
        internal const val DEFAULT_PROVIDER_TYPE_NONE = -2

        internal const val MOCK_TIME_INTENT = "androidx.wear.watchface.MockTime"

        // For debug purposes we support speeding up or slowing down time, these pair of constants
        // configure reading the mock time speed multiplier from a mock time intent.
        internal const val EXTRA_MOCK_TIME_SPEED_MULTIPLIER =
            "androidx.wear.watchface.extra.MOCK_TIME_SPEED_MULTIPLIER"
        private const val MOCK_TIME_DEFAULT_SPEED_MULTIPLIER = 1.0f

        // We support wrapping time between two instants, e.g. to loop an infrequent animation.
        // These constants configure reading this from a mock time intent.
        internal const val EXTRA_MOCK_TIME_WRAPPING_MIN_TIME =
            "androidx.wear.watchface.extra.MOCK_TIME_WRAPPING_MIN_TIME"
        private const val MOCK_TIME_WRAPPING_MIN_TIME_DEFAULT = -1L
        internal const val EXTRA_MOCK_TIME_WRAPPING_MAX_TIME =
            "androidx.wear.watchface.extra.MOCK_TIME_WRAPPING_MAX_TIME"

        // Many devices will enter Time Only Mode to reduce power consumption when the battery is
        // low, in which case only the system watch face will be displayed. On others there is a
        // battery saver mode triggering at 5% battery using an SCR to draw the display. For these
        // there's a gap of 10% battery (Intent.ACTION_BATTERY_LOW gets sent when < 15%) where we
        // clamp the framerate to a maximum of 10fps to conserve power.
        internal const val MAX_LOW_POWER_INTERACTIVE_UPDATE_RATE_MS = 100L

        // Complications are highlighted when tapped and after this delay the highlight is removed.
        internal const val CANCEL_COMPLICATION_HIGHLIGHTED_DELAY_MS = 300L
    }

    private data class MockTime(var speed: Double, var minTime: Long, var maxTime: Long)

    private var mockTime = MockTime(1.0, 0, Long.MAX_VALUE)

    private var lastTappedComplicationId: Int? = null
    private var lastTappedPosition: Point? = null
    private var registeredReceivers = false

    // True if NotificationManager.INTERRUPTION_FILTER_NONE.
    private var muteMode = false
    private var nextDrawTimeMillis: Long = 0

    /** @hide */
    @RestrictTo(LIBRARY_GROUP)
    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    public val calendar: Calendar = Calendar.getInstance()

    private val pendingSingleTap: CancellableUniqueTask =
        CancellableUniqueTask(watchFaceHostApi.getHandler())
    private val pendingUpdateTime: CancellableUniqueTask =
        CancellableUniqueTask(watchFaceHostApi.getHandler())
    private val pendingPostDoubleTap: CancellableUniqueTask =
        CancellableUniqueTask(watchFaceHostApi.getHandler())

    private val timeZoneReceiver: BroadcastReceiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent) {
            calendar.timeZone = TimeZone.getDefault()
            invalidate()
        }
    }

    private val timeReceiver: BroadcastReceiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context?, intent: Intent?) {
            // System time has changed hence next scheduled draw is invalid.
            nextDrawTimeMillis = systemTimeProvider.getSystemTimeMillis()
            invalidate()
        }
    }

    internal val batteryLevelReceiver: BroadcastReceiver = object : BroadcastReceiver() {
        @SuppressWarnings("SyntheticAccessor")
        override fun onReceive(context: Context, intent: Intent) {
            val isBatteryLowAndNotCharging =
                watchState.isBatteryLowAndNotCharging as MutableObservableWatchData
            when (intent.action) {
                Intent.ACTION_BATTERY_LOW -> isBatteryLowAndNotCharging.value = true
                Intent.ACTION_BATTERY_OKAY -> isBatteryLowAndNotCharging.value = false
                Intent.ACTION_POWER_CONNECTED -> isBatteryLowAndNotCharging.value = false
            }
            invalidate()
        }
    }

    /**
     * We listen for MOCK_TIME_INTENTs which we interpret as a request to modify time. E.g. speeding
     * up or slowing down time, and providing support for making time loop between two instants.
     * This is intended to help implement animations which may occur infrequently (e.g. hourly).
     */
    internal val mockTimeReceiver: BroadcastReceiver = object : BroadcastReceiver() {
        @SuppressWarnings("SyntheticAccessor")
        override fun onReceive(context: Context, intent: Intent) {
            mockTime.speed = intent.getFloatExtra(
                EXTRA_MOCK_TIME_SPEED_MULTIPLIER,
                MOCK_TIME_DEFAULT_SPEED_MULTIPLIER
            ).toDouble()
            mockTime.minTime = intent.getLongExtra(
                EXTRA_MOCK_TIME_WRAPPING_MIN_TIME,
                MOCK_TIME_WRAPPING_MIN_TIME_DEFAULT
            )
            // If MOCK_TIME_WRAPPING_MIN_TIME_DEFAULT is specified then use the current time.
            if (mockTime.minTime == MOCK_TIME_WRAPPING_MIN_TIME_DEFAULT) {
                mockTime.minTime = systemTimeProvider.getSystemTimeMillis()
            }
            mockTime.maxTime =
                intent.getLongExtra(EXTRA_MOCK_TIME_WRAPPING_MAX_TIME, Long.MAX_VALUE)
        }
    }

    init {
        // If the system has a stored user style then Home/SysUI is in charge of style
        // persistence, otherwise we need to do our own.
        val storedUserStyle = watchFaceHostApi.getInitialUserStyle()
        if (storedUserStyle != null) {
            userStyleRepository.userStyle =
                UserStyle(storedUserStyle, userStyleRepository.userStyleCategories)
        } else {
            // The system doesn't support preference persistence we need to do it ourselves.
            val preferencesFile =
                "watchface_prefs_${watchFaceHostApi.getContext().javaClass.typeName}.txt"

            userStyleRepository.userStyle = UserStyle(
                readPrefs(watchFaceHostApi.getContext(), preferencesFile),
                userStyleRepository.userStyleCategories
            )

            userStyleRepository.addUserStyleListener(
                object : UserStyleRepository.UserStyleListener {
                    @SuppressLint("SyntheticAccessor")
                    override fun onUserStyleChanged(userStyle: UserStyle) {
                        writePrefs(watchFaceHostApi.getContext(), preferencesFile, userStyle)
                    }
                })
        }
    }

    private var inOnSetStyle = false

    private val ambientObserver = Observer<Boolean> {
        scheduleDraw()
        invalidate()
    }

    private val interruptionFilterObserver = Observer<Int> {
        val inMuteMode = it == NotificationManager.INTERRUPTION_FILTER_NONE
        if (muteMode != inMuteMode) {
            muteMode = inMuteMode
            invalidate()
        }
    }

    private val visibilityObserver = Observer<Boolean> {
        if (it) {
            registerReceivers()
            // Update time zone in case it changed while we weren't visible.
            calendar.timeZone = TimeZone.getDefault()
            invalidate()
        } else {
            unregisterReceivers()
        }

        scheduleDraw()
    }

    init {
        // We need to inhibit an immediate callback during initialization because members are not
        // fully constructed and it will fail. It's also superfluous because we're going to render
        // anyway.
        var initFinished = false
        complicationsManager.init(
            watchFaceHostApi, calendar, renderer,
            object : Complication.InvalidateCallback {
                @SuppressWarnings("SyntheticAccessor")
                override fun onInvalidate() {
                    // Ensure we render a frame if the Complication needs rendering, e.g. because it
                    // loaded an image. However if we're animating there's no need to trigger an
                    // extra invalidation.
                    if (renderer.shouldAnimate() && computeDelayTillNextFrame(
                            nextDrawTimeMillis,
                            systemTimeProvider.getSystemTimeMillis()
                        ) < MIN_PERCEPTABLE_DELAY_MILLIS
                    ) {
                        return
                    }
                    if (initFinished) {
                        this@WatchFace.invalidate()
                    }
                }
            }
        )

        WatchFaceConfigActivity.registerWatchFace(
            componentName,
            object : WatchFaceConfigDelegate {
                override fun getUserStyleSchema() = userStyleRepository.toSchemaWireFormat()

                override fun getUserStyle() = userStyleRepository.userStyle.toWireFormat()

                override fun setUserStyle(userStyle: UserStyleWireFormat) {
                    userStyleRepository.userStyle =
                        UserStyle(userStyle, userStyleRepository.userStyleCategories)
                }

                override fun getBackgroundComplicationId() =
                    complicationsManager.getBackgroundComplication()?.id

                override fun getComplicationsMap() = complicationsManager.complications

                override fun getCalendar() = calendar

                override fun getComplicationIdAt(tapX: Int, tapY: Int) =
                    complicationsManager.getComplicationAt(tapX, tapY)?.id

                override fun brieflyHighlightComplicationId(complicationId: Int) {
                    complicationsManager.bringAttentionToComplication(complicationId)
                }

                override fun takeScreenshot(
                    drawRect: Rect,
                    calendar: Calendar,
                    renderParameters: RenderParametersWireFormat
                ) = renderer.takeScreenshot(calendar, RenderParameters(renderParameters))
            }
        )

        watchState.isAmbient.addObserver(ambientObserver)
        watchState.interruptionFilter.addObserver(interruptionFilterObserver)
        watchState.isVisible.addObserver(visibilityObserver)

        initFinished = true
    }

    /**
     * Called by the system in response to remote configuration, on the main thread.
     */
    internal fun onSetStyleInternal(style: UserStyle) {
        // No need to echo the userStyle back.
        inOnSetStyle = true
        userStyleRepository.userStyle = style
        inOnSetStyle = false
    }

    internal fun onDestroy() {
        pendingSingleTap.cancel()
        pendingUpdateTime.cancel()
        pendingPostDoubleTap.cancel()
        renderer.onDestroy()
        watchState.isAmbient.removeObserver(ambientObserver)
        watchState.interruptionFilter.removeObserver(interruptionFilterObserver)
        watchState.isVisible.removeObserver(visibilityObserver)
        WatchFaceConfigActivity.unregisterWatchFace(componentName)
    }

    private fun registerReceivers() {
        if (registeredReceivers) {
            return
        }
        registeredReceivers = true
        watchFaceHostApi.getContext().registerReceiver(
            timeZoneReceiver,
            IntentFilter(Intent.ACTION_TIMEZONE_CHANGED)
        )
        watchFaceHostApi.getContext().registerReceiver(
            timeReceiver,
            IntentFilter(Intent.ACTION_TIME_CHANGED)
        )
        watchFaceHostApi.getContext().registerReceiver(
            batteryLevelReceiver,
            IntentFilter(Intent.ACTION_BATTERY_CHANGED)
        )
        watchFaceHostApi.getContext().registerReceiver(
            mockTimeReceiver,
            IntentFilter(MOCK_TIME_INTENT)
        )
    }

    private fun unregisterReceivers() {
        if (!registeredReceivers) {
            return
        }
        registeredReceivers = false
        watchFaceHostApi.getContext().unregisterReceiver(timeZoneReceiver)
        watchFaceHostApi.getContext().unregisterReceiver(timeReceiver)
        watchFaceHostApi.getContext().unregisterReceiver(batteryLevelReceiver)
        watchFaceHostApi.getContext().unregisterReceiver(mockTimeReceiver)
    }

    private fun scheduleDraw() {
        // Separate calls are issued to deliver the state of isAmbient and isVisible, so during init
        // we might not yet know the state of both (which is required by the shouldAnimate logic).
        if (!watchState.isAmbient.hasValue() || !watchState.isVisible.hasValue()) {
            return
        }

        setCalendarTime(systemTimeProvider.getSystemTimeMillis())
        if (renderer.shouldAnimate()) {
            pendingUpdateTime.postUnique {
                invalidate()
            }
        }
    }

    /**
     * Convenience for [SurfaceHolder.Callback.surfaceChanged]. Called when the
     * [SurfaceHolder] containing the display surface changes.
     *
     * @param holder The new [SurfaceHolder] containing the display surface
     * @param format The new [android.graphics.PixelFormat] of the surface
     * @param width The width of the new display surface
     * @param height The height of the new display surface
     */
    internal fun onSurfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
        renderer.onSurfaceChanged(holder, format, width, height)
        invalidate()
    }

    /**
     * Sets the calendar's time in milliseconds adjusted by the mock time controls.
     */
    private fun setCalendarTime(timeMillis: Long) {
        // This adjustment allows time to be sped up or slowed down and to wrap between two
        // instants. This is useful when developing animations that occur infrequently (e.g.
        // hourly).
        val millis = (mockTime.speed * (timeMillis - mockTime.minTime).toDouble()).toLong()
        val range = mockTime.maxTime - mockTime.minTime
        var delta = millis % range
        if (delta < 0) {
            delta += range
        }
        calendar.timeInMillis = mockTime.minTime + delta
    }

    /** @hide */
    @UiThread
    internal fun maybeUpdateDrawMode() {
        var newDrawMode = if (watchState.isBatteryLowAndNotCharging.getValueOr(false)) {
            DrawMode.LOW_BATTERY_INTERACTIVE
        } else {
            DrawMode.INTERACTIVE
        }
        // Watch faces may wish to run an animation while entering ambient mode and we let them
        // defer entering ambient mode.
        if (watchState.isAmbient.value && !renderer.shouldAnimate()) {
            newDrawMode = DrawMode.AMBIENT
        } else if (muteMode) {
            newDrawMode = DrawMode.MUTE
        }
        renderer.renderParameters =
            RenderParameters(newDrawMode, RenderParameters.DRAW_ALL_LAYERS)
    }

    /** @hide */
    @UiThread
    internal fun onDraw() {
        setCalendarTime(systemTimeProvider.getSystemTimeMillis())
        maybeUpdateDrawMode()
        renderer.renderInternal(calendar)

        val currentTimeMillis = systemTimeProvider.getSystemTimeMillis()
        setCalendarTime(currentTimeMillis)
        if (renderer.shouldAnimate()) {
            val delayMillis = computeDelayTillNextFrame(nextDrawTimeMillis, currentTimeMillis)
            nextDrawTimeMillis = currentTimeMillis + delayMillis
            pendingUpdateTime.postDelayedUnique(delayMillis) { invalidate() }
        }
    }

    internal fun onSurfaceRedrawNeeded() {
        setCalendarTime(systemTimeProvider.getSystemTimeMillis())
        maybeUpdateDrawMode()
        renderer.renderInternal(calendar)
    }

    /** @hide */
    @UiThread
    internal fun computeDelayTillNextFrame(
        beginFrameTimeMillis: Long,
        currentTimeMillis: Long
    ): Long {
        // Limit update rate to conserve power when the battery is low and not charging.
        val updateRateMillis =
            if (watchState.isBatteryLowAndNotCharging.getValueOr(false)) {
                max(interactiveUpdateRateMillis, MAX_LOW_POWER_INTERACTIVE_UPDATE_RATE_MS)
            } else {
                interactiveUpdateRateMillis
            }
        // Note beginFrameTimeMillis could be in the future if the user adjusted the time so we need
        // to compute min(beginFrameTimeMillis, currentTimeMillis).
        var nextFrameTimeMillis =
            Math.min(beginFrameTimeMillis, currentTimeMillis) + updateRateMillis
        // Drop frames if needed (happens when onDraw is slow).
        if (nextFrameTimeMillis <= currentTimeMillis) {
            // Compute the next runtime after currentTimeMillis with the same phase as
            //  beginFrameTimeMillis to keep the animation smooth.
            val phaseAdjust =
                updateRateMillis +
                    ((nextFrameTimeMillis - currentTimeMillis) % updateRateMillis)
            nextFrameTimeMillis = currentTimeMillis + phaseAdjust
        }
        return nextFrameTimeMillis - currentTimeMillis
    }

    /**
     * Called when new complication data is received.
     *
     * @param watchFaceComplicationId The id of the complication that the data relates to. This will
     *     be an id that was previously sent in a call to [setActiveComplications].
     * @param data The [ComplicationData] that should be displayed in the complication.
     */
    @UiThread
    internal fun onComplicationDataUpdate(watchFaceComplicationId: Int, data: ComplicationData) {
        complicationsManager.onComplicationDataUpdate(watchFaceComplicationId, data)
        invalidate()
    }

    /**
     * Called when a tap or touch related event occurs. Detects double and single taps on
     * complications and triggers the associated action.
     *
     * @param originalTapType Value representing the event sent to the wallpaper
     * @param x X coordinate of the event
     * @param y Y coordinate of the event
     */
    @UiThread
    internal fun onTapCommand(
        @TapType originalTapType: Int,
        x: Int,
        y: Int
    ) {
        // Unfortunately we don't get MotionEvents so we can't directly use the GestureDetector
        // to distinguish between single and double taps. Currently we do that ourselves.
        // TODO(alexclarke): Revisit this

        var tapType = originalTapType
        when (tapType) {
            TapType.TOUCH -> {
                lastTappedPosition = Point(x, y)
            }
            TapType.TOUCH_CANCEL -> {
                lastTappedPosition?.let { safeLastTappedPosition ->
                    if ((safeLastTappedPosition.x == x) && (safeLastTappedPosition.y == y)) {
                        tapType = TapType.TAP
                    }
                }
                lastTappedPosition = null
            }
        }
        val tappedComplication = complicationsManager.getComplicationAt(x, y)
        if (tappedComplication == null) {
            clearGesture()
            return
        }

        when (tapType) {
            TapType.TAP -> {
                if (tappedComplication.id != lastTappedComplicationId &&
                    lastTappedComplicationId != null
                ) {
                    clearGesture()
                    return
                }
                if (pendingPostDoubleTap.isPending()) {
                    return
                }
                if (pendingSingleTap.isPending()) {
                    // The user tapped twice rapidly on the same complication so treat this as
                    // a double tap.
                    complicationsManager.onComplicationDoubleTapped(tappedComplication.id)
                    clearGesture()

                    // Block subsequent taps for a short time, to prevent accidental triple taps.
                    pendingPostDoubleTap.postDelayedUnique(
                        ViewConfiguration.getDoubleTapTimeout().toLong()
                    ) {
                        // NOP.
                    }
                } else {
                    // Give the user immediate visual feedback, the UI feels sluggish if we defer
                    // this.
                    complicationsManager.bringAttentionToComplication(tappedComplication.id)

                    lastTappedComplicationId = tappedComplication.id

                    // This could either be a single or a double tap, post a task to process the
                    // single tap which will get canceled if a double tap gets there first
                    pendingSingleTap.postDelayedUnique(
                        ViewConfiguration.getDoubleTapTimeout().toLong()
                    ) {
                        complicationsManager.onComplicationSingleTapped(tappedComplication.id)
                        invalidate()
                        clearGesture()
                    }
                }
            }
            TapType.TOUCH -> {
                // Make sure the user isn't doing a swipe.
                if (tappedComplication.id != lastTappedComplicationId &&
                    lastTappedComplicationId != null
                ) {
                    clearGesture()
                }
                lastTappedComplicationId = tappedComplication.id
            }
            else -> clearGesture()
        }
    }

    private fun clearGesture() {
        lastTappedComplicationId = null
        pendingSingleTap.cancel()
    }

    /** Schedules a call to [onDraw] to draw the next frame. */
    @UiThread
    public fun invalidate() {
        watchFaceHostApi.invalidate()
    }

    /**
     * Posts a message to schedule a call to [onDraw] to draw the next frame. Unlike
     * [invalidate], this method is thread-safe and may be called on any thread.
     */
    public fun postInvalidate() {
        watchFaceHostApi.getHandler().post { watchFaceHostApi.invalidate() }
    }
}