WatchFaceMetadataClient.kt

/*
 * Copyright 2021 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.client

import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.content.pm.PackageManager
import android.content.res.Resources
import android.content.res.XmlResourceParser
import android.graphics.RectF
import android.os.Bundle
import android.os.IBinder
import android.util.Log
import androidx.annotation.RestrictTo
import androidx.wear.watchface.BoundingArc
import androidx.wear.watchface.complications.ComplicationSlotBounds
import androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy
import androidx.wear.watchface.complications.data.ComplicationType
import androidx.wear.watchface.utility.AsyncTraceEvent
import androidx.wear.watchface.utility.TraceEvent
import androidx.wear.watchface.client.WatchFaceControlClient.Companion.createWatchFaceControlClient
import androidx.wear.watchface.control.IWatchFaceControlService
import androidx.wear.watchface.control.WatchFaceControlService
import androidx.wear.watchface.control.data.GetComplicationSlotMetadataParams
import androidx.wear.watchface.control.data.GetUserStyleSchemaParams
import androidx.wear.watchface.control.data.HeadlessWatchFaceInstanceParams
import androidx.wear.watchface.ComplicationSlotBoundsType
import androidx.wear.watchface.WatchFaceService
import androidx.wear.watchface.XmlSchemaAndComplicationSlotsDefinition
import androidx.wear.watchface.complications.data.ComplicationExperimental
import androidx.wear.watchface.control.data.GetUserStyleFlavorsParams
import androidx.wear.watchface.style.UserStyleFlavors
import androidx.wear.watchface.style.UserStyleSchema
import androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting
import androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay
import kotlinx.coroutines.CompletableDeferred

/**
 * Interface for fetching watch face metadata. E.g. the [UserStyleSchema] and
 * [ComplicationSlotMetadata]. This must be [close]d after use to release resources.
 */
public interface WatchFaceMetadataClient : AutoCloseable {

    public companion object {
        /** @hide */
        private const val TAG = "WatchFaceMetadataClient"

        /**
         * Constructs a [WatchFaceMetadataClient] for fetching metadata for the specified watch
         * face.
         *
         * @param context Calling application's [Context].
         * @param watchFaceName The [ComponentName] of the watch face to fetch meta data from.
         * @return The [WatchFaceMetadataClient] if there is one.
         * @throws [ServiceNotBoundException] if the underlying watch face control service can not
         * be bound or a [ServiceStartFailureException] if the watch face dies during startup. If
         * the service's manifest contains an
         * androidx.wear.watchface.XmlSchemaAndComplicationSlotsDefinition meta data node then
         * [PackageManager.NameNotFoundException] is thrown if [watchFaceName] is invalid.
         */
        @Throws(
            ServiceNotBoundException::class,
            ServiceStartFailureException::class,
            PackageManager.NameNotFoundException::class
        )
        @SuppressWarnings("MissingJvmstatic") // Can't really call a suspend fun from java.
        public suspend fun create(
            context: Context,
            watchFaceName: ComponentName
        ): WatchFaceMetadataClient {
            // Fallback to binding the service (slow).
            return createImpl(
                context,
                Intent(WatchFaceControlService.ACTION_WATCHFACE_CONTROL_SERVICE).apply {
                    setPackage(watchFaceName.packageName)
                },
                watchFaceName,
                ParserProvider()
            )
        }

        /** @hide */
        private const val ANDROIDX_WATCHFACE_XML_VERSION = "androidx.wear.watchface.xml_version"
        /** @hide */
        private const val ANDROIDX_WATCHFACE_CONTROL_SERVICE =
            "androidx.wear.watchface.control.WatchFaceControlService"

        @Suppress("DEPRECATION") // getServiceInfo
        internal fun isXmlVersionCompatible(
            context: Context,
            resources: Resources,
            controlServicePackage: String,
            controlServiceName: String = ANDROIDX_WATCHFACE_CONTROL_SERVICE
        ): Boolean {
            val controlServiceComponentName = ComponentName(
                controlServicePackage,
                controlServiceName)
            val version = try {
                context.packageManager.getServiceInfo(
                    controlServiceComponentName,
                    PackageManager.GET_META_DATA or PackageManager.MATCH_DISABLED_COMPONENTS
                ).metaData.getInt(ANDROIDX_WATCHFACE_XML_VERSION, 0)
            } catch (exception: PackageManager.NameNotFoundException) {
                // WatchFaceControlService may be missing in case WF is built with
                // pre-androidx watchface library.
                return false
            }

            val ourVersion = resources.getInteger(
                androidx.wear.watchface.R.integer.watch_face_xml_version)

            if (version > ourVersion) {
                Log.w(TAG, "WatchFaceControlService version ($version) " +
                    "of $controlServiceComponentName is higher than $ourVersion")
                return false
            }

            return true
        }

        /** @hide */
        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
        @Suppress("DEPRECATION")
        open class ParserProvider {
            // Open to allow testing without having to install the sample app.
            open fun getParser(context: Context, watchFaceName: ComponentName): XmlResourceParser? {
                if (!isXmlVersionCompatible(context, context.resources, watchFaceName.packageName))
                    return null

                return context.packageManager.getServiceInfo(
                    watchFaceName,
                    PackageManager.GET_META_DATA
                ).loadXmlMetaData(
                    context.packageManager,
                    WatchFaceService.XML_WATCH_FACE_METADATA
                )
            }
        }

        /** @hide */
        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
        @Suppress("ShowingMemberInHiddenClass") // Spurious warning about exposing the
        // 'hidden' companion object, which _isn't_ hidden.
        public suspend fun createImpl(
            context: Context,
            intent: Intent,
            watchFaceName: ComponentName,
            parserProvider: ParserProvider
        ): WatchFaceMetadataClient {
            // Check if there's static metadata we can read (fast).
            parserProvider.getParser(context, watchFaceName)?.let {
                return XmlWatchFaceMetadataClientImpl(
                    XmlSchemaAndComplicationSlotsDefinition.inflate(
                        context.packageManager.getResourcesForApplication(
                            watchFaceName.packageName
                        ),
                        it
                    )
                )
            }

            val deferredService = CompletableDeferred<IWatchFaceControlService>()
            val traceEvent = AsyncTraceEvent("WatchFaceMetadataClientImpl.bindService")
            val serviceConnection = object : ServiceConnection {
                override fun onServiceConnected(name: ComponentName, binder: IBinder) {
                    traceEvent.close()
                    deferredService.complete(IWatchFaceControlService.Stub.asInterface(binder))
                }

                override fun onServiceDisconnected(name: ComponentName?) {
                    // Note if onServiceConnected is called first completeExceptionally will do
                    // nothing because the CompletableDeferred is already completed.
                    traceEvent.close()
                    deferredService.completeExceptionally(ServiceStartFailureException())
                }
            }
            if (!BindHelper.bindService(context, intent, serviceConnection)) {
                traceEvent.close()
                throw ServiceNotBoundException()
            }
            return WatchFaceMetadataClientImpl(
                context,
                deferredService.await(),
                serviceConnection,
                watchFaceName
            )
        }
    }

    /**
     * Exception thrown by [createWatchFaceControlClient] if the remote service can't be bound.
     */
    public class ServiceNotBoundException : Exception()

    /** Exception thrown by [WatchFaceControlClient] methods if the service dies during start up. */
    public class ServiceStartFailureException(message: String = "") : Exception(message)

    /**
     * Returns the watch face's [UserStyleSchema].
     *
     * @throws [RuntimeException] if the watch face threw an exception while trying to service the
     * request or there was a communication problem with watch face process.
     */
    public fun getUserStyleSchema(): UserStyleSchema

    /**
     * Whether or not the [UserStyleSchema] is static and won't change unless the APK is updated.
     */
    @Suppress("INAPPLICABLE_JVM_NAME")
    @get:JvmName("isUserStyleSchemaStatic")
    public val isUserStyleSchemaStatic: Boolean

    /**
     * Returns a map of [androidx.wear.watchface.ComplicationSlot] ID to [ComplicationSlotMetadata]
     * for each slot in the watch face's [androidx.wear.watchface.ComplicationSlotsManager].
     *
     * @throws [RuntimeException] if the watch face threw an exception while trying to service the
     * request or there was a communication problem with watch face process.
     */
    public fun getComplicationSlotMetadataMap(): Map<Int, ComplicationSlotMetadata>

    /**
     * Returns the watch face's [UserStyleFlavors].
     *
     * @throws [RuntimeException] if the watch face threw an exception while trying to service the
     * request or there was a communication problem with watch face process.
     */
    public fun getUserStyleFlavors(): UserStyleFlavors
}

/**
 * Static metadata for a [androidx.wear.watchface.ComplicationSlot].
 *
 * @property bounds The complication slot's [ComplicationSlotBounds]. Only non `null` for watch
 * faces with a new enough [androidx.wear.watchface.control.WatchFaceControlService].
 * @property boundsType The [ComplicationSlotBoundsType] of the complication slot.
 * @property supportedTypes The list of [ComplicationType]s accepted by this complication slot. Used
 * during complication data source selection, this list should be non-empty.
 * @property defaultDataSourcePolicy The [DefaultComplicationDataSourcePolicy] which controls the
 * initial complication data source when the watch face is first installed.
 * @property isInitiallyEnabled At creation a complication slot is either enabled or disabled. This
 * can be overridden by a [ComplicationSlotsUserStyleSetting] (see
 * [ComplicationSlotOverlay.enabled]).
 * Editors need to know the initial state of a complication slot to predict the effects of making a
 * style change.
 * @property fixedComplicationDataSource  Whether or not the complication slot's complication data
 * source is fixed (i.e. can't be changed by the user). This is useful for watch faces built
 * around specific complication  complication data sources.
 * @property complicationConfigExtras Extras to be merged into the Intent sent when invoking the
 * complication data source chooser activity.
 */
public class ComplicationSlotMetadata
@ComplicationExperimental constructor(
    public val bounds: ComplicationSlotBounds?,
    @ComplicationSlotBoundsType public val boundsType: Int,
    public val supportedTypes: List<ComplicationType>,
    public val defaultDataSourcePolicy: DefaultComplicationDataSourcePolicy,
    @get:JvmName("isInitiallyEnabled")
    public val isInitiallyEnabled: Boolean,
    public val fixedComplicationDataSource: Boolean,
    public val complicationConfigExtras: Bundle,
    private val boundingArc: BoundingArc?
) {
    /**
     * The optional [BoundingArc] for an edge complication if specified, or `null` otherwise.
     */
    // TODO(b/230364881): Make this a normal primary constructor property when BoundingArc is no
    // longer experimental.
    @ComplicationExperimental
    public fun getBoundingArc(): BoundingArc? = boundingArc

    /**
     * Constructs a [ComplicationSlotMetadata].
     *
     * @param bounds The complication slot's [ComplicationSlotBounds]. Only non `null` for watch faces
     * with a new enough [androidx.wear.watchface.control.WatchFaceControlService].
     * @param boundsType The [ComplicationSlotBoundsType] of the complication slot.
     * @param supportedTypes The list of [ComplicationType]s accepted by this complication slot. Used
     * during complication data source selection, this list should be non-empty.
     * @param defaultDataSourcePolicy The [DefaultComplicationDataSourcePolicy] which controls the
     * initial complication data source when the watch face is first installed.
     * @param isInitiallyEnabled At creation a complication slot is either enabled or disabled. This
     * can be overridden by a [ComplicationSlotsUserStyleSetting] (see
     * [ComplicationSlotOverlay.enabled]).
     * Editors need to know the initial state of a complication slot to predict the effects of making a
     * style change.
     * @param fixedComplicationDataSource  Whether or not the complication slot's complication data
     * source is fixed (i.e. can't be changed by the user). This is useful for watch faces built
     * around specific complication  complication data sources.
     * @param complicationConfigExtras Extras to be merged into the Intent sent when invoking the
     * complication data source chooser activity.
     */
    // TODO(b/230364881): Deprecate when BoundingArc is no longer experimental.
    @OptIn(ComplicationExperimental::class)
    constructor(
        bounds: ComplicationSlotBounds?,
        @ComplicationSlotBoundsType boundsType: Int,
        supportedTypes: List<ComplicationType>,
        defaultDataSourcePolicy: DefaultComplicationDataSourcePolicy,
        isInitiallyEnabled: Boolean,
        fixedComplicationDataSource: Boolean,
        complicationConfigExtras: Bundle
    ) : this(
        bounds,
        boundsType,
        supportedTypes,
        defaultDataSourcePolicy,
        isInitiallyEnabled,
        fixedComplicationDataSource,
        complicationConfigExtras,
        null
    )
}

internal class WatchFaceMetadataClientImpl internal constructor(
    private val context: Context,
    private val service: IWatchFaceControlService,
    private val serviceConnection: ServiceConnection,
    private val watchFaceName: ComponentName
) : WatchFaceMetadataClient {
    private var closed = false
    private val headlessClientDelegate = lazy {
        createHeadlessWatchFaceClient(
            watchFaceName
        ) ?: throw WatchFaceMetadataClient.ServiceStartFailureException(
            "Could not open headless client for ${watchFaceName.flattenToString()}"
        )
    }
    private val headlessClient by headlessClientDelegate

    private fun createHeadlessWatchFaceClient(
        watchFaceName: ComponentName
    ): HeadlessWatchFaceClient? = TraceEvent(
        "WatchFaceMetadataClientImpl.createHeadlessWatchFaceClient"
    ).use {
        requireNotClosed()
        return service.createHeadlessWatchFaceInstance(
            HeadlessWatchFaceInstanceParams(
                watchFaceName,
                androidx.wear.watchface.data.DeviceConfig(false, false, 0, 0),
                1,
                1,
                null
            )
        )?.let {
            HeadlessWatchFaceClientImpl(it)
        }
    }

    private fun requireNotClosed() {
        require(!closed) {
            "WatchFaceMetadataClient method called after close"
        }
    }

    override fun getUserStyleSchema(): UserStyleSchema =
        callRemote {
            if (service.apiVersion >= 3) {
                UserStyleSchema(service.getUserStyleSchema(GetUserStyleSchemaParams(watchFaceName)))
            } else {
                headlessClient.userStyleSchema
            }
        }

    override val isUserStyleSchemaStatic: Boolean
        get() = false

    @OptIn(ComplicationExperimental::class)
    override fun getComplicationSlotMetadataMap(): Map<Int, ComplicationSlotMetadata> {
        requireNotClosed()
        return callRemote {
            if (service.apiVersion >= 3) {
                val wireFormat = service.getComplicationSlotMetadata(
                    GetComplicationSlotMetadataParams(watchFaceName)
                )
                wireFormat.associateBy(
                    { it.id },
                    {
                        val perSlotBounds = HashMap<ComplicationType, RectF>()
                        val perSlotMargins = HashMap<ComplicationType, RectF>()
                        for (i in it.complicationBoundsType.indices) {
                            val type = ComplicationType.fromWireType(it.complicationBoundsType[i])
                            perSlotBounds[type] = it.complicationBounds[i] ?: RectF()
                            perSlotMargins[type] = it.complicationMargins?.get(i) ?: RectF()
                        }
                        ComplicationSlotMetadata(
                            ComplicationSlotBounds.createFromPartialMap(
                                perSlotBounds,
                                perSlotMargins
                            ),
                            it.boundsType,
                            it.supportedTypes.map { ComplicationType.fromWireType(it) },
                            DefaultComplicationDataSourcePolicy(
                                it.defaultDataSourcesToTry ?: emptyList(),
                                it.fallbackSystemDataSource,
                                ComplicationType.fromWireType(
                                    it.primaryDataSourceDefaultType
                                ),
                                ComplicationType.fromWireType(
                                    it.secondaryDataSourceDefaultType
                                ),
                                ComplicationType.fromWireType(it.defaultDataSourceType)
                            ),
                            it.isInitiallyEnabled,
                            it.isFixedComplicationDataSource,
                            it.complicationConfigExtras,
                            it.boundingArc?.let { arc ->
                                BoundingArc(arc.arcStartAngle, arc.totalArcAngle, arc.arcThickness)
                            }
                        )
                    }
                )
            } else {
                headlessClient.complicationSlotsState.mapValues {
                    ComplicationSlotMetadata(
                        null,
                        it.value.boundsType,
                        it.value.supportedTypes,
                        it.value.defaultDataSourcePolicy,
                        it.value.isInitiallyEnabled,
                        it.value.fixedComplicationDataSource,
                        it.value.complicationConfigExtras,
                        null
                    )
                }
            }
        }
    }

    override fun getUserStyleFlavors(): UserStyleFlavors = callRemote {
        if (service.apiVersion >= 5) {
            UserStyleFlavors(
                service.getUserStyleFlavors(
                    GetUserStyleFlavorsParams(watchFaceName)
                )
            )
        } else {
            UserStyleFlavors()
        }
    }

    override fun close() = TraceEvent("WatchFaceMetadataClientImpl.close").use {
        closed = true
        if (headlessClientDelegate.isInitialized()) {
            headlessClient.close()
        }
        context.unbindService(serviceConnection)
    }
}

internal class XmlWatchFaceMetadataClientImpl(
    private val xmlSchemaAndComplicationSlotsDefinition: XmlSchemaAndComplicationSlotsDefinition
) : WatchFaceMetadataClient {
    override fun getUserStyleSchema() =
        xmlSchemaAndComplicationSlotsDefinition.schema ?: UserStyleSchema(emptyList())

    override val isUserStyleSchemaStatic: Boolean
        get() = true

    @OptIn(ComplicationExperimental::class)
    override fun getComplicationSlotMetadataMap() =
        xmlSchemaAndComplicationSlotsDefinition.complicationSlots.associateBy(
            { it.slotId },
            {
                ComplicationSlotMetadata(
                    it.bounds,
                    it.boundsType,
                    it.supportedTypes,
                    it.defaultDataSourcePolicy,
                    it.initiallyEnabled,
                    it.fixedComplicationDataSource,
                    Bundle(),
                    it.boundingArc
                )
            }
        )

    override fun getUserStyleFlavors() =
        xmlSchemaAndComplicationSlotsDefinition.flavors ?: UserStyleFlavors()

    override fun close() {}
}