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.ComponentName
import android.content.Intent
import android.content.IntentFilter
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Rect
import android.icu.util.Calendar
import android.icu.util.TimeZone
import android.os.BatteryManager
import android.os.Build
import android.os.Bundle
import android.support.wearable.watchface.SharedMemoryImage
import android.support.wearable.watchface.WatchFaceStyle
import android.view.Gravity
import android.view.Surface.FRAME_RATE_COMPATIBILITY_DEFAULT
import androidx.annotation.ColorInt
import androidx.annotation.IntDef
import androidx.annotation.IntRange
import androidx.annotation.Px
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
import androidx.annotation.UiThread
import androidx.annotation.VisibleForTesting
import androidx.wear.complications.SystemProviders
import androidx.wear.complications.data.ComplicationData
import androidx.wear.complications.data.ComplicationType
import androidx.wear.complications.data.toApiComplicationData
import androidx.wear.utility.TraceEvent
import androidx.wear.watchface.ObservableWatchData.MutableObservableWatchData
import androidx.wear.watchface.control.data.ComplicationRenderParams
import androidx.wear.watchface.control.data.WatchFaceRenderParams
import androidx.wear.watchface.data.ComplicationStateWireFormat
import androidx.wear.watchface.data.IdAndComplicationStateWireFormat
import androidx.wear.watchface.style.CurrentUserStyleRepository
import androidx.wear.watchface.style.UserStyle
import androidx.wear.watchface.style.UserStyleData
import androidx.wear.watchface.style.UserStyleSchema
import androidx.wear.watchface.style.WatchFaceLayer
import kotlinx.coroutines.CompletableDeferred
import java.security.InvalidParameterException
import kotlin.math.max

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

// Zero is a special value meaning we will accept the system's choice for the
// display frame rate, which is the default behavior if this function isn't called.
private const val SYSTEM_DECIDES_FRAME_RATE = 0f

/**
 * 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
    }
}

/**
 * The return value of [WatchFaceService.createWatchFace] which brings together rendering, styling,
 * complications and state observers.
 *
 * @param watchFaceType The type of watch face, whether it's digital or analog. Used to determine
 * the default time for editor preview screenshots.
 * @param renderer The [Renderer] for this WatchFace.
 */
public class WatchFace(
    @WatchFaceType public var watchFaceType: Int,
    public val renderer: Renderer
) {
    internal var tapListener: TapListener? = null

    public companion object {
        /** Returns whether [LegacyWatchFaceOverlayStyle] is supported on this device. */
        @JvmStatic
        public fun isLegacyWatchFaceOverlayStyleSupported(): Boolean = Build.VERSION.SDK_INT <= 27

        private val componentNameToEditorDelegate = HashMap<ComponentName, EditorDelegate>()

        private var pendingComponentName: ComponentName? = null
        private var pendingEditorDelegateCB: CompletableDeferred<EditorDelegate>? = null

        /** @hide */
        @JvmStatic
        @UiThread
        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
        public fun registerEditorDelegate(
            componentName: ComponentName,
            editorDelegate: EditorDelegate
        ) {
            componentNameToEditorDelegate[componentName] = editorDelegate

            if (componentName == pendingComponentName) {
                pendingEditorDelegateCB?.complete(editorDelegate)
            } else {
                pendingEditorDelegateCB?.completeExceptionally(
                    IllegalStateException(
                        "Expected $pendingComponentName to be created but got $componentName"
                    )
                )
            }
            pendingComponentName = null
            pendingEditorDelegateCB = null
        }

        internal fun unregisterEditorDelegate(componentName: ComponentName) {
            componentNameToEditorDelegate.remove(componentName)
        }

        /** @hide */
        @JvmStatic
        @UiThread
        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
        @VisibleForTesting
        public fun clearAllEditorDelegates() {
            componentNameToEditorDelegate.clear()
        }

        /**
         * For use by on watch face editors.
         * @hide
         */
        @JvmStatic
        @UiThread
        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
        public fun getOrCreateEditorDelegate(
            componentName: ComponentName
        ): CompletableDeferred<EditorDelegate> {
            componentNameToEditorDelegate[componentName]?.let {
                return CompletableDeferred(it)
            }

            // There's no pre-existing watch face. We expect Home/SysUI to switch the watchface soon
            // so record a pending request...
            pendingComponentName = componentName
            pendingEditorDelegateCB = CompletableDeferred()
            return pendingEditorDelegateCB!!
        }
    }

    /**
     * Delegate used by on watch face editors.
     * @hide
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    public interface EditorDelegate {
        /** The [WatchFace]'s [UserStyleSchema]. */
        public val userStyleSchema: UserStyleSchema

        /** The watch face's  [UserStyle]. */
        public var userStyle: UserStyle

        /** The [WatchFace]'s [ComplicationsManager]. */
        public val complicationsManager: ComplicationsManager

        /** The [WatchFace]'s screen bounds [Rect]. */
        public val screenBounds: Rect

        /** The UTC reference time to use for previews in milliseconds since the epoch. */
        public val previewReferenceTimeMillis: Long

        /** Renders the watchface to a [Bitmap] with the [CurrentUserStyleRepository]'s [UserStyle]. */
        public fun renderWatchFaceToBitmap(
            renderParameters: RenderParameters,
            calendarTimeMillis: Long,
            idToComplicationData: Map<Int, ComplicationData>?
        ): Bitmap

        /** Signals that the activity is going away and resources should be released. */
        public fun onDestroy()
    }

    /**
     * Interface for getting the current system time.
     * @hide
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    public interface SystemTimeProvider {
        /** Returns the current system time in milliseconds. */
        public fun getSystemTimeMillis(): Long
    }

    /** Listens for taps on the watchface which didn't land on [Complication]s. */
    public interface TapListener {
        /**
         * Called whenever the user taps on the watchface but doesn't hit a [Complication].
         *
         * The watch face receives three different types of touch events:
         * - [TapType.DOWN] when the user puts the finger down on the touchscreen
         * - [TapType.UP] when the user lifts the finger from the touchscreen
         * - [TapType.CANCEL] when the system detects that the user is performing a gesture other
         *   than a tap
         *
         * Note that the watch face is only given tap events, i.e., events where the user puts
         * the finger down on the screen and then lifts it at the position. If the user performs any
         * other type of gesture while their finger in on the touchscreen, the watch face will be
         * receive a cancel, as all other gestures are reserved by the system.
         *
         * Therefore, a [TapType.DOWN] event and the successive [TapType.UP] event are guaranteed
         * to be close enough to be considered a tap according to the value returned by
         * [android.view.ViewConfiguration.getScaledTouchSlop].
         *
         * If the watch face receives a [TapType.CANCEL] event, it should not trigger any action, as
         * the system is already processing the gesture.
         *
         * @param tapType the type of touch event sent to the watch face
         * @param xPos the horizontal position in pixels on the screen where the touch happened
         * @param yPos the vertical position in pixels on the screen where the touch happened
         */
        @UiThread
        public fun onTap(
            @TapType tapType: Int,
            @Px xPos: Int,
            @Px yPos: Int
        )
    }

    /**
     * Legacy Wear 2.0 watch face styling. These settings will be ignored on Wear 3.0 devices.
     *
     * @param viewProtectionMode The view protection mode bit field, must be a combination of zero
     * or more of [WatchFaceStyle.PROTECT_STATUS_BAR], [WatchFaceStyle.PROTECT_HOTWORD_INDICATOR],
     * [WatchFaceStyle.PROTECT_WHOLE_SCREEN].
     * @param statusBarGravity Controls the position of status icons (battery state, lack of
     * connection) on the screen. 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.
     * `[Gravity.LEFT] | [Gravity.BOTTOM]`. On circular screens, only the vertical gravity is
     * respected.
     * @param tapEventsAccepted Controls whether this watch face accepts tap events. Watchfaces
     * that set this `true` are indicating they are prepared to receive [TapType.DOWN],
     * [TapType.CANCEL], and [TapType.UP] events.
     * @param accentColor The accent color which will be used when drawing the unread notification
     * indicator. Default color is white.
     * @throws IllegalArgumentException if [viewProtectionMode] has an unexpected value
     */
    public class LegacyWatchFaceOverlayStyle @JvmOverloads constructor(
        public val viewProtectionMode: Int,
        public val statusBarGravity: Int,
        @get:JvmName("isTapEventsAccepted")
        public val tapEventsAccepted: Boolean,
        @ColorInt public val accentColor: Int = WatchFaceStyle.DEFAULT_ACCENT_COLOR
    ) {
        init {
            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"
                )
            }
        }
    }

    /** The UTC preview time in milliseconds since the epoch, or null if not set. */
    @get:SuppressWarnings("AutoBoxing")
    @IntRange(from = 0)
    public var overridePreviewReferenceTimeMillis: Long? = null
        private set

    /** The legacy [LegacyWatchFaceOverlayStyle] which only affects Wear 2.0 devices. */
    public var legacyWatchFaceStyle: LegacyWatchFaceOverlayStyle = LegacyWatchFaceOverlayStyle(
        0,
        0,
        true
    )
        private set

    internal var systemTimeProvider: SystemTimeProvider = object : SystemTimeProvider {
        override fun getSystemTimeMillis() = System.currentTimeMillis()
    }

    /**
     * Overrides the reference time for editor preview images.
     *
     * @param previewReferenceTimeMillis The UTC preview time in milliseconds since the epoch
     */
    public fun setOverridePreviewReferenceTimeMillis(
        @IntRange(from = 0) previewReferenceTimeMillis: Long
    ): WatchFace = apply {
        overridePreviewReferenceTimeMillis = previewReferenceTimeMillis
    }

    /**
     * Sets the legacy [LegacyWatchFaceOverlayStyle] which only affects Wear 2.0 devices.
     */
    public fun setLegacyWatchFaceStyle(
        legacyWatchFaceStyle: LegacyWatchFaceOverlayStyle
    ): WatchFace = apply {
        this.legacyWatchFaceStyle = legacyWatchFaceStyle
    }

    /**
     * Sets an optional [TapListener] which if not `null` gets called on the ui thread whenever
     * the user taps on the watchface but doesn't hit a [Complication].
     */
    @SuppressWarnings("ExecutorRegistration")
    public fun setTapListener(tapListener: TapListener?): WatchFace = apply {
        this.tapListener = tapListener
    }

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

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

/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@SuppressLint("SyntheticAccessor")
public class WatchFaceImpl @UiThread constructor(
    watchface: WatchFace,
    private val watchFaceHostApi: WatchFaceHostApi,
    private val watchState: WatchState,
    internal val currentUserStyleRepository: CurrentUserStyleRepository,
    internal var complicationsManager: ComplicationsManager,

    /** @hide */
    @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    @get:VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    public val calendar: Calendar,
    private val broadcastsObserver: BroadcastsObserver,
    internal var broadcastsReceiver: BroadcastsReceiver?
) {
    internal companion object {
        internal const val NO_DEFAULT_PROVIDER = SystemProviders.NO_PROVIDER

        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

        // The threshold used to judge whether the battery is low during initialization.  Ideally
        // we would use the threshold for Intent.ACTION_BATTERY_LOW but it's not documented or
        // available programmatically. The value below is the default but it could be overridden
        // by OEMs.
        internal const val INITIAL_LOW_BATTERY_THRESHOLD = 15.0f

        internal val defaultRenderParametersForDrawMode: HashMap<DrawMode, RenderParameters> =
            hashMapOf(
                DrawMode.AMBIENT to
                    RenderParameters(
                        DrawMode.AMBIENT, WatchFaceLayer.ALL_WATCH_FACE_LAYERS, null
                    ),
                DrawMode.INTERACTIVE to
                    RenderParameters(
                        DrawMode.INTERACTIVE, WatchFaceLayer.ALL_WATCH_FACE_LAYERS, null
                    ),
                DrawMode.LOW_BATTERY_INTERACTIVE to
                    RenderParameters(
                        DrawMode.LOW_BATTERY_INTERACTIVE, WatchFaceLayer.ALL_WATCH_FACE_LAYERS,
                        null
                    ),
                DrawMode.MUTE to
                    RenderParameters(
                        DrawMode.MUTE, WatchFaceLayer.ALL_WATCH_FACE_LAYERS, null
                    ),
            )
    }

    private val systemTimeProvider = watchface.systemTimeProvider
    private val legacyWatchFaceStyle = watchface.legacyWatchFaceStyle
    internal val renderer = watchface.renderer
    private val tapListener = watchface.tapListener

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

    private var lastTappedComplicationId: Int? = null

    // True if 'Do Not Disturb' mode is on.
    private var muteMode = false
    private var nextDrawTimeMillis: Long = 0

    private val pendingUpdateTime: CancellableUniqueTask =
        CancellableUniqueTask(watchFaceHostApi.getUiThreadHandler())

    internal val componentName =
        ComponentName(
            watchFaceHostApi.getContext().packageName,
            watchFaceHostApi.getContext().javaClass.name
        )

    internal fun getWatchFaceStyle() = WatchFaceStyle(
        componentName,
        legacyWatchFaceStyle.viewProtectionMode,
        legacyWatchFaceStyle.statusBarGravity,
        legacyWatchFaceStyle.accentColor,
        false,
        false,
        legacyWatchFaceStyle.tapEventsAccepted
    )

    internal fun onActionTimeZoneChanged() {
        calendar.timeZone = TimeZone.getDefault()
        renderer.invalidate()
    }

    internal fun onActionTimeChanged() {
        // System time has changed hence next scheduled draw is invalid.
        nextDrawTimeMillis = systemTimeProvider.getSystemTimeMillis()
        renderer.invalidate()
    }

    internal fun onMockTime(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)
    }

    /** The UTC reference time for editor preview images in milliseconds since the epoch. */
    public val previewReferenceTimeMillis: Long =
        watchface.overridePreviewReferenceTimeMillis ?: when (watchface.watchFaceType) {
            WatchFaceType.ANALOG -> watchState.analogPreviewReferenceTimeMillis
            WatchFaceType.DIGITAL -> watchState.digitalPreviewReferenceTimeMillis
            else -> throw InvalidParameterException("Unrecognized watchFaceType")
        }

    private var inOnSetStyle = false

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

    private val interruptionFilterObserver = Observer<Int> {
        // We are in mute mode in any of the following modes. The specific mode depends on the
        // device's implementation of "Do Not Disturb".
        val inMuteMode = it == NotificationManager.INTERRUPTION_FILTER_NONE ||
            it == NotificationManager.INTERRUPTION_FILTER_PRIORITY ||
            it == NotificationManager.INTERRUPTION_FILTER_ALARMS
        if (muteMode != inMuteMode) {
            muteMode = inMuteMode
            watchFaceHostApi.invalidate()
        }
    }

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

            scheduleDraw()
        }
    }

    // Only installed if Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
    @SuppressLint("NewApi")
    private val batteryLowAndNotChargingObserver = Observer<Boolean> {
        // To save power we request a lower hardware display frame rate when the battery is low
        // and not charging.
        if (renderer.surfaceHolder.surface.isValid) {
            renderer.surfaceHolder.surface.setFrameRate(
                if (it) {
                    1000f / MAX_LOW_POWER_INTERACTIVE_UPDATE_RATE_MS.toFloat()
                } else {
                    SYSTEM_DECIDES_FRAME_RATE
                },
                FRAME_RATE_COMPATIBILITY_DEFAULT
            )
        }
    }

    init {
        renderer.watchFaceHostApi = watchFaceHostApi
        renderer.uiThreadInit()

        setIsBatteryLowAndNotChargingFromBatteryStatus(
            IntentFilter(Intent.ACTION_BATTERY_CHANGED).let { iFilter ->
                watchFaceHostApi.getContext().registerReceiver(null, iFilter)
            }
        )

        if (!watchState.isHeadless) {
            WatchFace.registerEditorDelegate(componentName, WFEditorDelegate())
        }

        watchState.isAmbient.addObserver(ambientObserver)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !watchState.isHeadless) {
            watchState.isBatteryLowAndNotCharging.addObserver(batteryLowAndNotChargingObserver)
        }
        watchState.interruptionFilter.addObserver(interruptionFilterObserver)
        watchState.isVisible.addObserver(visibilityObserver)
    }

    internal fun invalidateIfNotAnimating() {
        // 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
        ) {
            watchFaceHostApi.invalidate()
        }
    }

    internal fun createWFEditorDelegate() = WFEditorDelegate() as WatchFace.EditorDelegate

    internal inner class WFEditorDelegate : WatchFace.EditorDelegate {
        override val userStyleSchema: UserStyleSchema
            get() = currentUserStyleRepository.schema

        override var userStyle: UserStyle
            get() = currentUserStyleRepository.userStyle
            set(value) {
                currentUserStyleRepository.userStyle = value
            }

        override val complicationsManager: ComplicationsManager
            get() = this@WatchFaceImpl.complicationsManager

        override val screenBounds
            get() = renderer.screenBounds

        override val previewReferenceTimeMillis
            get() = this@WatchFaceImpl.previewReferenceTimeMillis

        override fun renderWatchFaceToBitmap(
            renderParameters: RenderParameters,
            calendarTimeMillis: Long,
            idToComplicationData: Map<Int, ComplicationData>?
        ): Bitmap = TraceEvent("WFEditorDelegate.takeScreenshot").use {
            val oldComplicationData =
                complicationsManager.complications.values.associateBy(
                    { it.id },
                    { it.renderer.getData() }
                )

            idToComplicationData?.let {
                for ((id, complicationData) in it) {
                    complicationsManager[id]!!.renderer.loadData(complicationData, false)
                }
            }
            val screenShot = renderer.takeScreenshot(
                Calendar.getInstance(TimeZone.getTimeZone("UTC")).apply {
                    timeInMillis = calendarTimeMillis
                },
                renderParameters
            )
            if (idToComplicationData != null) {
                for ((id, data) in oldComplicationData) {
                    complicationsManager[id]!!.renderer.loadData(data, false)
                }
            }
            return screenShot
        }

        override fun onDestroy(): Unit = TraceEvent("WFEditorDelegate.onDestroy").use {
            if (watchState.isHeadless) {
                this@WatchFaceImpl.onDestroy()
            }
        }
    }

    internal fun setIsBatteryLowAndNotChargingFromBatteryStatus(batteryStatus: Intent?) {
        val status = batteryStatus?.getIntExtra(BatteryManager.EXTRA_STATUS, -1) ?: -1
        val isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING ||
            status == BatteryManager.BATTERY_STATUS_FULL
        val batteryPercent: Float = batteryStatus?.let { intent ->
            val level: Int = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
            val scale: Int = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
            level * 100 / scale.toFloat()
        } ?: 100.0f
        val isBatteryLowAndNotCharging =
            watchState.isBatteryLowAndNotCharging as MutableObservableWatchData
        isBatteryLowAndNotCharging.value =
            (batteryPercent < INITIAL_LOW_BATTERY_THRESHOLD) && !isCharging
    }

    /**
     * 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
        currentUserStyleRepository.userStyle = style
        inOnSetStyle = false
    }

    internal fun onDestroy() {
        pendingUpdateTime.cancel()
        renderer.onDestroy()
        watchState.isAmbient.removeObserver(ambientObserver)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !watchState.isHeadless) {
            watchState.isBatteryLowAndNotCharging.removeObserver(batteryLowAndNotChargingObserver)
        }
        watchState.interruptionFilter.removeObserver(interruptionFilterObserver)
        watchState.isVisible.removeObserver(visibilityObserver)
        if (!watchState.isHeadless) {
            WatchFace.unregisterEditorDelegate(componentName)
        }
        unregisterReceivers()
    }

    @UiThread
    private fun registerReceivers() {
        require(watchFaceHostApi.getUiThreadHandler().looper.isCurrentThread) {
            "registerReceivers must be called the UiThread"
        }

        // There's no point registering BroadcastsReceiver for headless instances.
        if (broadcastsReceiver == null && !watchState.isHeadless) {
            broadcastsReceiver =
                BroadcastsReceiver(watchFaceHostApi.getContext(), broadcastsObserver)
        }
    }

    @UiThread
    private fun unregisterReceivers() {
        require(watchFaceHostApi.getUiThreadHandler().looper.isCurrentThread) {
            "unregisterReceivers must be called the UiThread"
        }
        broadcastsReceiver?.onDestroy()
        broadcastsReceiver = null
    }

    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 {
                watchFaceHostApi.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
        }

        if (renderer.renderParameters.drawMode != newDrawMode) {
            renderer.renderParameters = defaultRenderParametersForDrawMode[newDrawMode]!!
        }
    }

    /** @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) { watchFaceHostApi.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(
                    renderer.interactiveDrawModeUpdateDelayMillis,
                    MAX_LOW_POWER_INTERACTIVE_UPDATE_RATE_MS
                )
            } else {
                renderer.interactiveDrawModeUpdateDelayMillis
            }
        // 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.
     * @param data The [ComplicationData] that should be displayed in the complication.
     */
    @UiThread
    internal fun onComplicationDataUpdate(watchFaceComplicationId: Int, data: ComplicationData) {
        complicationsManager.onComplicationDataUpdate(watchFaceComplicationId, data)
        watchFaceHostApi.invalidate()
    }

    /** Clears all [ComplicationData]. */
    @UiThread
    internal fun clearComplicationData() {
        complicationsManager.clearComplicationData()
        watchFaceHostApi.invalidate()
    }

    /**
     * Called when a tap or touch related event occurs. Detects double and single taps on
     * complications and triggers the associated action.
     *
     * @param tapType 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 tapType: Int,
        x: Int,
        y: Int
    ) {
        val tappedComplication = complicationsManager.getComplicationAt(x, y)
        if (tappedComplication == null) {
            // The event does not belong to any of the complications, pass to the listener.
            lastTappedComplicationId = null
            tapListener?.onTap(tapType, x, y)
            return
        }

        when (tapType) {
            TapType.UP -> {
                if (tappedComplication.id != lastTappedComplicationId &&
                    lastTappedComplicationId != null
                ) {
                    // The UP event belongs to a different complication then the DOWN event,
                    // do not consider this a tap on either of them.
                    lastTappedComplicationId = null
                    return
                }
                complicationsManager.displayPressedAnimation(tappedComplication.id)
                complicationsManager.onComplicationSingleTapped(tappedComplication.id)
                watchFaceHostApi.invalidate()
                lastTappedComplicationId = null
            }
            TapType.DOWN -> {
                lastTappedComplicationId = tappedComplication.id
            }
            else -> lastTappedComplicationId = null
        }
    }

    @UiThread
    internal fun getComplicationState() = complicationsManager.complications.map {
        IdAndComplicationStateWireFormat(
            it.key,
            ComplicationStateWireFormat(
                it.value.computeBounds(renderer.screenBounds),
                it.value.boundsType,
                ComplicationType.toWireTypes(it.value.supportedTypes),
                it.value.defaultProviderPolicy.providersAsList(),
                it.value.defaultProviderPolicy.systemProviderFallback,
                it.value.defaultProviderType.toWireComplicationType(),
                it.value.enabled,
                it.value.initiallyEnabled,
                it.value.renderer.getData()?.type?.toWireComplicationType()
                    ?: ComplicationType.NO_DATA.toWireComplicationType(),
                it.value.fixedComplicationProvider,
                it.value.configExtras
            )
        )
    }

    @UiThread
    @RequiresApi(27)
    internal fun renderWatchFaceToBitmap(
        params: WatchFaceRenderParams
    ): Bundle = TraceEvent("WatchFaceImpl.renderWatchFaceToBitmap").use {
        val oldStyle = HashMap(currentUserStyleRepository.userStyle.selectedOptions)
        params.userStyle?.let {
            onSetStyleInternal(UserStyle(UserStyleData(it), currentUserStyleRepository.schema))
        }

        val oldComplicationData =
            complicationsManager.complications.values.associateBy(
                { it.id },
                { it.renderer.getData() }
            )

        params.idAndComplicationDatumWireFormats?.let {
            for (idAndData in it) {
                complicationsManager[idAndData.id]!!.renderer
                    .loadData(idAndData.complicationData.toApiComplicationData(), false)
            }
        }

        val bitmap = renderer.takeScreenshot(
            Calendar.getInstance(TimeZone.getTimeZone("UTC")).apply {
                timeInMillis = params.calendarTimeMillis
            },
            RenderParameters(params.renderParametersWireFormat)
        )

        // Restore previous style & complications if required.
        if (params.userStyle != null) {
            onSetStyleInternal(UserStyle(oldStyle))
        }

        if (params.idAndComplicationDatumWireFormats != null) {
            for ((id, data) in oldComplicationData) {
                complicationsManager[id]!!.renderer.loadData(data, false)
            }
        }

        return SharedMemoryImage.ashmemWriteImageBundle(bitmap)
    }

    @UiThread
    @RequiresApi(27)
    internal fun renderComplicationToBitmap(
        params: ComplicationRenderParams
    ): Bundle? = TraceEvent("WatchFaceImpl.renderComplicationToBitmap").use {
        val calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")).apply {
            timeInMillis = params.calendarTimeMillis
        }
        return complicationsManager[params.complicationId]?.let {
            val oldStyle = HashMap(currentUserStyleRepository.userStyle.selectedOptions)
            val newStyle = params.userStyle
            if (newStyle != null) {
                onSetStyleInternal(
                    UserStyle(UserStyleData(newStyle), currentUserStyleRepository.schema)
                )
            }

            val bounds = it.computeBounds(renderer.screenBounds)
            val complicationBitmap =
                Bitmap.createBitmap(bounds.width(), bounds.height(), Bitmap.Config.ARGB_8888)

            var prevData: ComplicationData? = null
            val screenshotComplicationData = params.complicationData
            if (screenshotComplicationData != null) {
                prevData = it.renderer.getData()
                it.renderer.loadData(
                    screenshotComplicationData.toApiComplicationData(),
                    false
                )
            }

            it.renderer.render(
                Canvas(complicationBitmap),
                Rect(0, 0, bounds.width(), bounds.height()),
                calendar,
                RenderParameters(params.renderParametersWireFormat)
            )

            // Restore previous ComplicationData & style if required.
            if (params.complicationData != null) {
                it.renderer.loadData(prevData, false)
            }

            if (newStyle != null) {
                onSetStyleInternal(UserStyle(oldStyle))
            }

            SharedMemoryImage.ashmemWriteImageBundle(complicationBitmap)
        }
    }

    @UiThread
    internal fun dump(writer: IndentingPrintWriter) {
        writer.println("WatchFaceImpl ($componentName): ")
        writer.increaseIndent()
        writer.println("calendar=$calendar")
        writer.println("mockTime.maxTime=${mockTime.maxTime}")
        writer.println("mockTime.minTime=${mockTime.minTime}")
        writer.println("mockTime.speed=${mockTime.speed}")
        writer.println("nextDrawTimeMillis=$nextDrawTimeMillis")
        writer.println("muteMode=$muteMode")
        writer.println("pendingUpdateTime=${pendingUpdateTime.isPending()}")
        writer.println("lastTappedComplicationId=$lastTappedComplicationId")
        writer.println(
            "currentUserStyleRepository.userStyle=${currentUserStyleRepository.userStyle}"
        )
        writer.println("currentUserStyleRepository.schema=${currentUserStyleRepository.schema}")
        watchState.dump(writer)
        complicationsManager.dump(writer)
        renderer.dump(writer)
        writer.decreaseIndent()
    }
}