WatchFaceControlService.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.control

import android.annotation.SuppressLint
import android.app.Service
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.os.IBinder
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
import androidx.annotation.UiThread
import androidx.annotation.VisibleForTesting
import androidx.wear.watchface.IndentingPrintWriter
import androidx.wear.watchface.WatchFaceService
import androidx.wear.watchface.control.data.CrashInfoParcel
import androidx.wear.watchface.control.data.DefaultProviderPoliciesParams
import androidx.wear.watchface.control.data.GetComplicationSlotMetadataParams
import androidx.wear.watchface.control.data.GetUserStyleFlavorsParams
import androidx.wear.watchface.control.data.GetUserStyleSchemaParams
import androidx.wear.watchface.control.data.HeadlessWatchFaceInstanceParams
import androidx.wear.watchface.control.data.IdTypeAndDefaultProviderPolicyWireFormat
import androidx.wear.watchface.control.data.WallpaperInteractiveWatchFaceInstanceParams
import androidx.wear.watchface.data.ComplicationSlotMetadataWireFormat
import androidx.wear.watchface.editor.EditorService
import androidx.wear.watchface.runBlockingWithTracing
import androidx.wear.watchface.style.data.UserStyleFlavorsWireFormat
import androidx.wear.watchface.style.data.UserStyleSchemaWireFormat
import androidx.wear.watchface.utility.AsyncTraceEvent
import androidx.wear.watchface.utility.TraceEvent
import java.io.FileDescriptor
import java.io.PrintWriter
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope

/**
 * A service for creating and controlling watch face instances.
 *
 * @hide
 */
@RequiresApi(27)
@VisibleForTesting
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public open class WatchFaceControlService : Service() {
    private var watchFaceInstanceServiceStub: IWatchFaceInstanceServiceStub? = null

    /** @hide */
    public companion object {
        public const val ACTION_WATCHFACE_CONTROL_SERVICE: String =
            "com.google.android.wearable.action.WATCH_FACE_CONTROL"
    }

    override fun onBind(intent: Intent?): IBinder? =
        TraceEvent("WatchFaceControlService.onBind").use {
            if (ACTION_WATCHFACE_CONTROL_SERVICE == intent?.action) {
                if (watchFaceInstanceServiceStub == null) {
                    watchFaceInstanceServiceStub = createServiceStub()
                }
                watchFaceInstanceServiceStub
            } else {
                null
            }
        }

    open fun createWatchFaceService(watchFaceName: ComponentName): WatchFaceService? {
        return try {
            val watchFaceServiceClass = Class.forName(watchFaceName.className) ?: return null
            if (!WatchFaceService::class.java.isAssignableFrom(WatchFaceService::class.java)) {
                return null
            }
            watchFaceServiceClass.getConstructor().newInstance() as WatchFaceService
        } catch (e: ClassNotFoundException) {
            null
        }
    }

    @VisibleForTesting
    public open fun createServiceStub(): IWatchFaceInstanceServiceStub =
        TraceEvent("WatchFaceControlService.createServiceStub").use {
            IWatchFaceInstanceServiceStub(this, MainScope())
        }

    @VisibleForTesting
    public fun setContext(context: Context) {
        attachBaseContext(context)
    }

    @UiThread
    override fun dump(fd: FileDescriptor, writer: PrintWriter, args: Array<String>) {
        val indentingPrintWriter = IndentingPrintWriter(writer)
        indentingPrintWriter.println("WatchFaceControlService:")
        InteractiveInstanceManager.dump(indentingPrintWriter)
        HeadlessWatchFaceImpl.dump(indentingPrintWriter)
        indentingPrintWriter.flush()
    }

    override fun onDestroy() {
        super.onDestroy()
        watchFaceInstanceServiceStub?.onDestroy()
    }
}

/** @hide */
@RequiresApi(27)
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public open class IWatchFaceInstanceServiceStub(
    // We need to explicitly null this object in onDestroy to avoid a memory leak.
    private var service: WatchFaceControlService?,
    private val uiThreadCoroutineScope: CoroutineScope
) : IWatchFaceControlService.Stub() {
    override fun getApiVersion(): Int = IWatchFaceControlService.API_VERSION

    internal companion object {
        const val TAG = "IWatchFaceInstanceServiceStub"
    }

    override fun getInteractiveWatchFaceInstance(instanceId: String): IInteractiveWatchFace? =
        TraceEvent("IWatchFaceInstanceServiceStub.getInteractiveWatchFaceInstance").use {
            // This call is thread safe so we don't need to trampoline via the UI thread.
            InteractiveInstanceManager.getAndRetainInstance(instanceId)
        }

    override fun createHeadlessWatchFaceInstance(
        params: HeadlessWatchFaceInstanceParams
    ): IHeadlessWatchFace? =
        TraceEvent("IWatchFaceInstanceServiceStub.createHeadlessWatchFaceInstance").use {
            createServiceAndHeadlessEngine(params.watchFaceName)?.let { serviceAndEngine ->
                // This is serviced on a background thread so it should be fine to block.
                uiThreadCoroutineScope.runBlockingWithTracing("createHeadlessInstance") {
                    // However the WatchFaceService.createWatchFace method needs to be run on the UI
                    // thread.
                    serviceAndEngine.engine.createHeadlessInstance(params)
                }
            }
        }

    private class ServiceAndEngine(
        val service: WatchFaceService,
        val engine: WatchFaceService.EngineWrapper
    ) {
        fun destroy() {
            try {
                engine.onDestroy()
                service.onDestroy()
            } catch (e: Exception) {
                Log.e(TAG, "ServiceAndEngine.destroy failed due to exception", e)
                throw e
            }
        }
    }

    @SuppressLint("BanUncheckedReflection")
    private fun createServiceAndHeadlessEngine(watchFaceName: ComponentName) =
        TraceEvent("IWatchFaceInstanceServiceStub.createEngine").use {
            // Attempt to construct the class for the specified watchFaceName, failing if it either
            // doesn't exist or isn't a [WatchFaceService].
            val watchFaceService = service?.createWatchFaceService(watchFaceName)

            if (watchFaceService != null) {
                // Set the context and if possible the application for watchFaceService.
                try {
                    val method = Service::class.java.declaredMethods.find { it.name == "attach" }
                    method!!.isAccessible = true
                    method.invoke(
                        watchFaceService,
                        service as Context,
                        null,
                        watchFaceService::class.qualifiedName,
                        null,
                        service!!.application,
                        null
                    )
                } catch (e: Exception) {
                    Log.w(
                        TAG,
                        "createServiceAndHeadlessEngine can't call attach by reflection, " +
                            "falling back to setContext",
                        e
                    )
                    watchFaceService.setContext(watchFaceService)
                }
                watchFaceService.onCreate()
                val engine =
                    watchFaceService.createHeadlessEngine() as WatchFaceService.EngineWrapper
                ServiceAndEngine(watchFaceService, engine)
            } else {
                null
            }
        }

    override fun getOrCreateInteractiveWatchFace(
        params: WallpaperInteractiveWatchFaceInstanceParams,
        callback: IPendingInteractiveWatchFace
    ): IInteractiveWatchFace? {
        val asyncTraceEvent =
            AsyncTraceEvent("IWatchFaceInstanceServiceStub.getOrCreateInteractiveWatchFaceWCS")
        return try {
            InteractiveInstanceManager
                .getExistingInstanceOrSetPendingWallpaperInteractiveWatchFaceInstance(
                    InteractiveInstanceManager.PendingWallpaperInteractiveWatchFaceInstance(
                        params,
                        // Wrapped IPendingInteractiveWatchFace to support tracing.
                        object : IPendingInteractiveWatchFace.Stub() {
                            override fun getApiVersion() = callback.apiVersion

                            override fun onInteractiveWatchFaceCreated(
                                iInteractiveWatchFaceWcs: IInteractiveWatchFace?
                            ) {
                                asyncTraceEvent.close()
                                callback.onInteractiveWatchFaceCreated(iInteractiveWatchFaceWcs)
                            }

                            override fun onInteractiveWatchFaceCrashed(exception: CrashInfoParcel) {
                                asyncTraceEvent.close()
                                callback.onInteractiveWatchFaceCrashed(exception)
                            }
                        }
                    )
                )
        } catch (e: Exception) {
            Log.e(TAG, "getOrCreateInteractiveWatchFace failed ", e)
            throw e
        }
    }

    override fun getEditorService(): EditorService = EditorService.globalEditorService

    override fun getDefaultProviderPolicies(
        params: DefaultProviderPoliciesParams
    ): Array<IdTypeAndDefaultProviderPolicyWireFormat>? =
        createServiceAndHeadlessEngineAndEvaluate(
            params.watchFaceName,
            "IWatchFaceInstanceServiceStub.getDefaultProviderPolicies"
        ) {
            it.engine.getDefaultProviderPolicies()
        }

    override fun getUserStyleSchema(params: GetUserStyleSchemaParams): UserStyleSchemaWireFormat? =
        createServiceAndHeadlessEngineAndEvaluate(
            params.watchFaceName,
            "IWatchFaceInstanceServiceStub.getUserStyleSchema"
        ) {
            it.engine.getUserStyleSchemaWireFormat()
        }

    override fun getComplicationSlotMetadata(
        params: GetComplicationSlotMetadataParams
    ): Array<ComplicationSlotMetadataWireFormat>? =
        createServiceAndHeadlessEngineAndEvaluate(
            params.watchFaceName,
            "IWatchFaceInstanceServiceStub.getComplicationSlotMetadata"
        ) {
            it.engine.getComplicationSlotMetadataWireFormats()
        }

    override fun hasComplicationCache() = true

    override fun getUserStyleFlavors(
        params: GetUserStyleFlavorsParams
    ): UserStyleFlavorsWireFormat? =
        createServiceAndHeadlessEngineAndEvaluate(
            params.watchFaceName,
            "IWatchFaceInstanceServiceStub.getUserStyleFlavors"
        ) {
            it.engine.getUserStyleFlavorsWireFormat()
        }

    private fun <T> createServiceAndHeadlessEngineAndEvaluate(
        watchFaceName: ComponentName,
        functionName: String,
        function: (serviceAndEngine: ServiceAndEngine) -> T
    ): T? =
        TraceEvent(functionName).use {
            return try {
                createServiceAndHeadlessEngine(watchFaceName)?.let { serviceAndEngine ->
                    try {
                        function(serviceAndEngine)
                    } finally {
                        serviceAndEngine.destroy()
                    }
                }
            } catch (e: Exception) {
                Log.e(TAG, "$functionName failed due to exception", e)
                throw e
            }
        }

    fun onDestroy() {
        service = null
    }
}