/*
* 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.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.ComplicationProviderInfo as WireComplicationProviderInfo
import android.support.wearable.complications.IPreviewComplicationDataCallback
import android.support.wearable.complications.IProviderInfoService
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
import androidx.annotation.VisibleForTesting
import androidx.wear.watchface.complications.ComplicationDataSourceInfoRetriever.Result
import androidx.wear.watchface.complications.data.ComplicationData
import androidx.wear.watchface.complications.data.ComplicationType
import androidx.wear.watchface.complications.data.ComplicationType.Companion.fromWireType
import androidx.wear.watchface.complications.data.LongTextComplicationData
import androidx.wear.watchface.complications.data.MonochromaticImage
import androidx.wear.watchface.complications.data.MonochromaticImageComplicationData
import androidx.wear.watchface.complications.data.NoDataComplicationData
import androidx.wear.watchface.complications.data.PhotoImageComplicationData
import androidx.wear.watchface.complications.data.PlainComplicationText
import androidx.wear.watchface.complications.data.RangedValueComplicationData
import androidx.wear.watchface.complications.data.ShortTextComplicationData
import androidx.wear.watchface.complications.data.SmallImage
import androidx.wear.watchface.complications.data.SmallImageComplicationData
import androidx.wear.watchface.complications.data.SmallImageType
import androidx.wear.watchface.complications.data.toApiComplicationData
import androidx.wear.watchface.utility.TraceEvent
import java.lang.IllegalArgumentException
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.suspendCancellableCoroutine
/**
* Retrieves [Result] for a watch face's complications.
*
* To use construct an instance and call [retrieveComplicationDataSourceInfo] which returns an array
* of [Result] objects.
*
* Further calls to [retrieveComplicationDataSourceInfo] 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 ComplicationDataSourceInfoRetriever : AutoCloseable {
/** Results for [retrieveComplicationDataSourceInfo]. */
public class Result
internal constructor(
/** The id for the complication slot, as passed to [retrieveComplicationDataSourceInfo]. */
public val slotId: Int,
/**
* Details of the complication data source for that complication, or `null` if no
* complication data source is currently configured.
*/
public val info: ComplicationDataSourceInfo?
)
private inner class ProviderInfoServiceConnection : ServiceConnection {
@SuppressLint("SyntheticAccessor")
override fun onServiceConnected(name: ComponentName, service: IBinder) {
deferredService.complete(IProviderInfoService.Stub.asInterface(service))
}
@SuppressLint("SyntheticAccessor")
override fun onBindingDied(name: ComponentName?) {
synchronized(lock) { closed = true }
deferredService.completeExceptionally(ServiceDisconnectedException())
}
@SuppressLint("SyntheticAccessor")
override fun onServiceDisconnected(name: ComponentName) {
synchronized(lock) { closed = true }
deferredService.completeExceptionally(ServiceDisconnectedException())
}
}
@SuppressLint("SyntheticAccessor")
private val serviceConnection: ServiceConnection = ProviderInfoServiceConnection()
private var context: Context? = null
private val deferredService = CompletableDeferred<IProviderInfoService>()
private val lock = Any()
/** @hide */
@VisibleForTesting
@RestrictTo(RestrictTo.Scope.LIBRARY)
public var closed: Boolean = false
private set
internal constructor(context: Context, intent: Intent) {
this.context = context
context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
}
/** @param context the current context */
public constructor(
context: Context
) : this(
context,
Intent(ACTION_GET_COMPLICATION_CONFIG).apply { setPackage(PROVIDER_INFO_SERVICE_PACKAGE) }
)
/** Exception thrown if the service disconnects. */
public class ServiceDisconnectedException : Exception()
/** @hide */
@VisibleForTesting
@RestrictTo(RestrictTo.Scope.LIBRARY)
public constructor(service: IProviderInfoService) {
deferredService.complete(service)
}
/**
* Requests [Result] 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 An array of [Result]. If the look up fails null will be returned.
* @throws [ServiceDisconnectedException] if the service disconnected during the call.
*/
@Throws(ServiceDisconnectedException::class)
public suspend fun retrieveComplicationDataSourceInfo(
watchFaceComponent: ComponentName,
watchFaceComplicationIds: IntArray
): Array<Result>? =
TraceEvent("ComplicationDataSourceInfoRetriever.retrieveComplicationDataSourceInfo").use {
synchronized(lock) {
require(!closed) { "retrieveComplicationDataSourceInfo called after close" }
}
awaitDeferredService()
.getProviderInfos(watchFaceComponent, watchFaceComplicationIds)
?.mapIndexed { index, info ->
Result(watchFaceComplicationIds[index], info?.toApiComplicationDataSourceInfo())
}
?.toTypedArray()
}
/**
* Requests preview [ComplicationData] for a complication data source [ComponentName] and
* [ComplicationType]. Note if `null` is returned
* [ComplicationDataSourceInfo.fallbackPreviewData] can be used to generate fallback preview
* data based on the name and icon of the provider.
*
* @param complicationDataSourceComponent The [ComponentName] of the complication data source
* from which preview data is requested.
* @param complicationType The requested [ComplicationType] for the preview data.
* @return The preview [ComplicationData] or `null` if the complication data source 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(
complicationDataSourceComponent: ComponentName,
complicationType: ComplicationType
): ComplicationData? =
TraceEvent("ComplicationDataSourceInfoRetriever.requestPreviewComplicationData").use {
synchronized(lock) {
require(!closed) { "retrievePreviewComplicationData called after close" }
}
val service = awaitDeferredService()
if (service.apiVersion < 1) {
return null
}
return suspendCancellableCoroutine { continuation ->
val callback = PreviewComplicationDataCallback(service, continuation)
if (
!service.requestPreviewComplicationData(
complicationDataSourceComponent,
complicationType.toWireComplicationType(),
callback
)
) {
callback.safeUnlinkToDeath()
continuation.resume(null)
}
}
}
private class PreviewComplicationDataCallback(
val service: IProviderInfoService,
var continuation: CancellableContinuation<ComplicationData?>?
) : IPreviewComplicationDataCallback.Stub() {
val deathObserver: IBinder.DeathRecipient =
IBinder.DeathRecipient {
continuation?.resumeWithException(ServiceDisconnectedException())
}
init {
service.asBinder().linkToDeath(deathObserver, 0)
// Not a huge deal but we might as well unlink the deathObserver.
continuation?.invokeOnCancellation { safeUnlinkToDeath() }
}
override fun updateComplicationData(
data: android.support.wearable.complications.ComplicationData?
) {
safeUnlinkToDeath()
continuation!!.resume(data?.toApiComplicationData())
// Re http://b/249121838 this is important, it prevents a memory leak.
continuation = null
}
internal fun safeUnlinkToDeath() {
try {
service.asBinder().unlinkToDeath(deathObserver, 0)
} catch (e: NoSuchElementException) {
// This really shouldn't happen.
Log.w(TAG, "retrievePreviewComplicationData encountered", e)
}
}
}
private suspend fun awaitDeferredService(): IProviderInfoService =
TraceEvent("ComplicationDataSourceInfoRetriever.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 [retrieveComplicationDataSourceInfo] will
* resolve with null.
*
* This class implements the Java `AutoClosable` interface and may be used with
* try-with-resources.
*/
override fun close() {
synchronized(lock) {
if (closed) {
Log.e(
TAG,
"Error ComplicationDataSourceInfoRetriever.close called when already closed",
Throwable()
)
} else {
closed = true
try {
context?.unbindService(serviceConnection)
context = null
} catch (e: IllegalArgumentException) {
Log.e(TAG, "unbindService failed", e)
}
}
}
}
private companion object {
private const val TAG = "ComplicationDataS"
/** The package of the service that supplies complication data source 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 data source, for use by watch faces (for example, to show the
* current complication data source in settings). A [ComplicationDataSourceInfoRetriever] can be
* used to obtain references of this class for each of a watch face's complications.
*/
public class ComplicationDataSourceInfo(
/** The name of the application containing the complication data source. */
public val appName: String,
/** The name of the complication data source. */
public val name: String,
/** The icon for the complication data source. */
public val icon: Icon,
/** The type of the complication provided by the data source. */
public val type: ComplicationType,
/**
* The complication data source's {@link ComponentName}.
*
* This field is populated only on Android R and above and it is `null` otherwise.
*/
public val componentName: ComponentName?,
) {
/**
* Lazily constructed fallback preview [ComplicationData] based on this
* ComplicationDataSourceInfo. This is useful when
* [ComplicationDataSourceInfoRetriever.retrievePreviewComplicationData] returns `null` (e.g. on
* a pre-android R device).
*/
public val fallbackPreviewData: ComplicationData by lazy {
val contentDescription = PlainComplicationText.Builder(name).build()
when (type) {
ComplicationType.SHORT_TEXT ->
ShortTextComplicationData.Builder(
PlainComplicationText.Builder(
name.take(ShortTextComplicationData.MAX_TEXT_LENGTH)
)
.build(),
contentDescription
)
.setMonochromaticImage(MonochromaticImage.Builder(icon).build())
.build()
ComplicationType.LONG_TEXT ->
LongTextComplicationData.Builder(
PlainComplicationText.Builder(name).build(),
contentDescription
)
.setMonochromaticImage(MonochromaticImage.Builder(icon).build())
.build()
ComplicationType.SMALL_IMAGE ->
SmallImageComplicationData.Builder(
SmallImage.Builder(icon, SmallImageType.ICON).build(),
contentDescription
)
.build()
ComplicationType.MONOCHROMATIC_IMAGE ->
MonochromaticImageComplicationData.Builder(
MonochromaticImage.Builder(icon).build(),
contentDescription
)
.build()
ComplicationType.PHOTO_IMAGE ->
PhotoImageComplicationData.Builder(icon, contentDescription).build()
ComplicationType.RANGED_VALUE ->
RangedValueComplicationData.Builder(42f, 0f, 100f, contentDescription)
.setMonochromaticImage(MonochromaticImage.Builder(icon).build())
.setText(PlainComplicationText.Builder(name).build())
.build()
else -> NoDataComplicationData()
}
}
init {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
require(componentName != null) { "ComponentName is required on Android R and above" }
}
}
override fun toString(): String =
"ComplicationDataSourceInfo(appName=$appName, name=$name, type=$type" +
", icon=$icon, componentName=$componentName)"
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ComplicationDataSourceInfo
if (appName != other.appName) return false
if (name != other.name) return false
if (type != other.type) return false
if (icon != other.icon) return false
if (componentName != other.componentName) return false
return true
}
override fun hashCode(): Int {
var result = appName.hashCode()
result = 31 * result + name.hashCode()
result = 31 * result + type.hashCode()
result = 31 * result + icon.hashCode()
result = 31 * result + componentName.hashCode()
return result
}
/**
* 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.toApiComplicationDataSourceInfo() =
ComplicationDataSourceInfo(
appName!!,
providerName!!,
providerIcon!!,
fromWireType(complicationType),
providerComponentName
)