ProviderInfoRetriever.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.complications

import android.annotation.SuppressLint
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.graphics.drawable.Icon
import android.os.Build
import android.os.IBinder
import android.support.wearable.complications.IPreviewComplicationDataCallback
import android.support.wearable.complications.IProviderInfoService
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
import androidx.annotation.VisibleForTesting
import androidx.wear.complications.ProviderInfoRetriever.ProviderInfo
import androidx.wear.complications.data.ComplicationData
import androidx.wear.complications.data.ComplicationType
import androidx.wear.complications.data.ComplicationType.Companion.fromWireType
import androidx.wear.complications.data.toApiComplicationData
import androidx.wear.utility.TraceEvent
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException

private typealias WireComplicationProviderInfo =
    android.support.wearable.complications.ComplicationProviderInfo

/**
 * Retrieves [ComplicationProviderInfo] for a watch face's complications.
 *
 *
 * To use construct an instance and call [retrieveProviderInfo] which returns an array of
 * [ProviderInfo] objects.
 *
 *
 * Further calls to [retrieveProviderInfo] may be made using the same instance of this
 * class, but [close] must be called when it is no longer needed. Once release has been
 * called, further retrieval attempts will fail.
 */
public class ProviderInfoRetriever : AutoCloseable {
    /** Results for [retrieveProviderInfo]. */
    public class ProviderInfo internal constructor(
        /** The id for the complication, as provided to [retrieveProviderInfo].  */
        public val watchFaceComplicationId: Int,

        /**
         * Details of the provider for that complication, or `null` if no provider is currently
         * configured.
         */
        public val info: ComplicationProviderInfo?
    )

    private inner class ProviderInfoServiceConnection : ServiceConnection {
        @SuppressLint("SyntheticAccessor")
        override fun onServiceConnected(name: ComponentName, service: IBinder) {
            deferredService.complete(IProviderInfoService.Stub.asInterface(service))
        }

        @SuppressLint("SyntheticAccessor")
        override fun onServiceDisconnected(name: ComponentName) {
            deferredService.completeExceptionally(ServiceDisconnectedException())
        }
    }

    @SuppressLint("SyntheticAccessor")
    private val serviceConnection: ServiceConnection = ProviderInfoServiceConnection()
    private var context: Context? = null
    private val deferredService = CompletableDeferred<IProviderInfoService>()

    /**
     * @hide
     */
    @VisibleForTesting
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    public var closed: Boolean = false
        private set

    /** @param context the current context */
    public constructor(context: Context) {
        this.context = context
        val intent = Intent(ACTION_GET_COMPLICATION_CONFIG)
        intent.setPackage(PROVIDER_INFO_SERVICE_PACKAGE)
        context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
    }

    /** Exception thrown if the service disconnects. */
    public class ServiceDisconnectedException : Exception()

    /**
     * @hide
     */
    @VisibleForTesting
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    public constructor(service: IProviderInfoService) {
        deferredService.complete(service)
    }

    /**
     * Requests [ComplicationProviderInfo] for the specified complication ids on the specified
     * watch face. When the info is received, the listener will receive a callback for each id.
     * These callbacks will occur on the main thread.
     *
     *
     * This will only work if the package of the current app is the same as the package of the
     * specified watch face.
     *
     * @param watchFaceComponent the ComponentName of the WatchFaceService for which info is
     * being requested
     * @param watchFaceComplicationIds ids of the complications that info is being requested for
     * @return The requested provider info. If the look up fails null will be returned
     * @throws [ServiceDisconnectedException] if the service disconnected during the call.
     */
    @Throws(ServiceDisconnectedException::class)
    public suspend fun retrieveProviderInfo(
        watchFaceComponent: ComponentName,
        watchFaceComplicationIds: IntArray
    ): Array<ProviderInfo>? = TraceEvent("ProviderInfoRetriever.retrieveProviderInfo").use {
        require(!closed) {
            "retrieveProviderInfo called after close"
        }
        awaitDeferredService().getProviderInfos(
            watchFaceComponent, watchFaceComplicationIds
        )?.mapIndexed { index, info ->
            ProviderInfo(watchFaceComplicationIds[index], info?.toApiComplicationProviderInfo())
        }?.toTypedArray()
    }

    /**
     * Requests preview [ComplicationData] for a provider [ComponentName] and
     * [ComplicationType].
     *
     * @param providerComponent The [ComponentName] of the complication provider from which
     * preview data is requested.
     * @param complicationType The requested [ComplicationType] for the preview data.
     * @return The preview [ComplicationData] or `null` if the provider component doesn't exist, or
     * if it doesn't support complicationType, or if the remote service doesn't support this API.
     * @throws [ServiceDisconnectedException] if the service disconnected during the call.
     */
    @Throws(ServiceDisconnectedException::class)
    @RequiresApi(Build.VERSION_CODES.R)
    public suspend fun retrievePreviewComplicationData(
        providerComponent: ComponentName,
        complicationType: ComplicationType
    ): ComplicationData? = TraceEvent(
        "ProviderInfoRetriever.requestPreviewComplicationData"
    ).use {
        require(!closed) {
            "retrievePreviewComplicationData called after close"
        }
        val service = awaitDeferredService()
        if (service.apiVersion < 1) {
            return null
        }

        return suspendCancellableCoroutine { continuation ->
            val deathObserver = IBinder.DeathRecipient {
                continuation.resumeWithException(ServiceDisconnectedException())
            }
            service.asBinder().linkToDeath(deathObserver, 0)

            // Not a huge deal but we might as well unlink the deathObserver.
            continuation.invokeOnCancellation {
                service.asBinder().unlinkToDeath(deathObserver, 0)
            }

            if (!service.requestPreviewComplicationData(
                    providerComponent,
                    complicationType.toWireComplicationType(),
                    object : IPreviewComplicationDataCallback.Stub() {
                        override fun updateComplicationData(
                            data: android.support.wearable.complications.ComplicationData?
                        ) {
                            service.asBinder().unlinkToDeath(deathObserver, 0)
                            continuation.resume(data?.toApiComplicationData())
                        }
                    }
                )
            ) {
                service.asBinder().unlinkToDeath(deathObserver, 0)
                continuation.resume(null)
            }
        }
    }

    private suspend fun awaitDeferredService(): IProviderInfoService =
        TraceEvent("ProviderInfoRetriever.awaitDeferredService").use {
            deferredService.await()
        }

    /**
     * Releases the connection to the complication system used by this class. This must
     * be called when the retriever is no longer needed.
     *
     *
     * Any outstanding or subsequent futures returned by [retrieveProviderInfo] will
     * resolve with null.
     *
     * This class implements the Java `AutoClosable` interface and
     * may be used with try-with-resources.
     */
    override fun close() {
        closed = true
        context?.unbindService(serviceConnection)
    }

    private companion object {
        /** The package of the service that supplies provider info.  */
        private const val PROVIDER_INFO_SERVICE_PACKAGE = "com.google.android.wearable.app"
        private const val ACTION_GET_COMPLICATION_CONFIG =
            "android.support.wearable.complications.ACTION_GET_COMPLICATION_CONFIG"
    }
}

/**
 * Holder of details of a complication provider, for use by watch faces (for example,
 * to show the current provider in settings). A [ProviderInfoRetriever] can be used to obtain
 * references of this class for each of a watch face's complications.
 */
public class ComplicationProviderInfo(
    /** The name of the application containing the complication provider. */
    public val appName: String,

    /** The name of the complication provider. */
    public val name: String,

    /** The icon for the complication provider. */
    public val icon: Icon,

    /** The type of the complication provided by the provider. */
    public val type: ComplicationType,

    /**
     * The provider's {@link ComponentName}.
     *
     * This field is populated only on Android R and above and it is `null` otherwise.
     */
    public val componentName: ComponentName?,
) {
    init {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            require(componentName != null) {
                "ComponentName is required on Android R and above"
            }
        }
    }
    /**
     * Converts this value to [WireComplicationProviderInfo] object used for serialization.
     *
     * @hide
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    public fun toWireComplicationProviderInfo(): WireComplicationProviderInfo =
        WireComplicationProviderInfo(
            appName, name, icon, type.toWireComplicationType(),
            componentName
        )
}

/**
 * @hide
 */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public fun WireComplicationProviderInfo.toApiComplicationProviderInfo(): ComplicationProviderInfo =
    ComplicationProviderInfo(
        appName!!, providerName!!, providerIcon!!, fromWireType(complicationType),
        providerComponentName
    )