/*
* 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.datasource
import android.annotation.SuppressLint
import android.app.Activity
import android.app.Service
import android.content.ComponentName
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.os.RemoteException
import android.support.wearable.complications.ComplicationData as WireComplicationData
import android.support.wearable.complications.ComplicationProviderInfo
import android.support.wearable.complications.IComplicationManager
import android.support.wearable.complications.IComplicationProvider
import androidx.annotation.IntDef
import androidx.annotation.MainThread
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
import androidx.annotation.VisibleForTesting
import androidx.wear.watchface.complications.data.ComplicationData
import androidx.wear.watchface.complications.data.ComplicationDataExpressionEvaluator
import androidx.wear.watchface.complications.data.ComplicationDataExpressionEvaluator.Companion.hasExpression
import androidx.wear.watchface.complications.data.ComplicationType
import androidx.wear.watchface.complications.data.ComplicationType.Companion.fromWireType
import androidx.wear.watchface.complications.data.GoalProgressComplicationData
import androidx.wear.watchface.complications.data.LongTextComplicationData
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.RangedValueComplicationData
import androidx.wear.watchface.complications.data.ShortTextComplicationData
import androidx.wear.watchface.complications.data.SmallImageComplicationData
import androidx.wear.watchface.complications.data.WeightedElementsComplicationData
import androidx.wear.watchface.complications.data.TimeRange
import androidx.wear.watchface.complications.datasource.ComplicationDataSourceService.Companion.METADATA_KEY_IMMEDIATE_UPDATE_PERIOD_MILLISECONDS
import androidx.wear.watchface.complications.datasource.ComplicationDataSourceService.ComplicationRequestListener
import java.time.Duration
import java.util.concurrent.CountDownLatch
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
/**
* Data associated with complication request in
* [ComplicationDataSourceService.onComplicationRequest].
*
* @param complicationInstanceId The system's id for the requested complication which is a unique
* value for the tuple [Watch face ComponentName, complication slot ID].
* @param complicationType The type of complication data requested.
* @param immediateResponseRequired If `true` then [ComplicationRequestListener.onComplicationData]
* should be called as soon as possible (ideally less than 100ms instead of the usual 20s
* deadline). This will only be `true` within a
* [ComplicationDataSourceService.onStartImmediateComplicationRequests]
* [ComplicationDataSourceService.onStopImmediateComplicationRequests] pair.
* @param isForSafeWatchFace Whether this request is on behalf of a 'safe' watch face as defined by
* the [ComplicationDataSourceService.METADATA_KEY_SAFE_WATCH_FACES] meta data in the data
* source's manifest. The data source may choose to serve different results for a 'safe' watch
* face. If the data source does not have the privileged permission
* `com.google.wear.permission.GET_IS_FOR_SAFE_WATCH_FACE`, then this must be null.
*/
public class ComplicationRequest
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
constructor(
complicationInstanceId: Int,
complicationType: ComplicationType,
immediateResponseRequired: Boolean,
@IsForSafeWatchFace isForSafeWatchFace: Int
) {
/** Constructs a [ComplicationRequest] without setting [isForSafeWatchFace]. */
@Suppress("NewApi")
constructor(
complicationInstanceId: Int,
complicationType: ComplicationType,
immediateResponseRequired: Boolean,
) : this(
complicationInstanceId,
complicationType,
immediateResponseRequired,
isForSafeWatchFace = TargetWatchFaceSafety.UNKNOWN
)
/**
* The system's id for the requested complication which is a unique value for the tuple
* [Watch face ComponentName, complication slot ID].
*/
public val complicationInstanceId: Int = complicationInstanceId
/** The [ComplicationType] of complication data requested. */
public val complicationType: ComplicationType = complicationType
/**
* If `true` then [ComplicationRequestListener.onComplicationData] should be called as soon as
* possible (ideally less than 100ms instead of the usual 20s deadline). This will only be
* `true` within a [ComplicationDataSourceService.onStartImmediateComplicationRequests]
* [ComplicationDataSourceService.onStopImmediateComplicationRequests] pair which will not be
* called unless the [ComplicationDataSourceService] has privileged permission
* `com.google.android.wearable.permission.USE_IMMEDIATE_COMPLICATION_UPDATE`.
*/
@get:JvmName("isImmediateResponseRequired")
public val immediateResponseRequired = immediateResponseRequired
/**
* Intended for OEM use, returns whether this request is on behalf of a 'safe' watch face as
* defined by the [ComplicationDataSourceService.METADATA_KEY_SAFE_WATCH_FACES] meta data in the
* data source's manifest. The data source may choose to serve different results for a 'safe'
* watch face.
*
* If the [ComplicationDataSourceService.METADATA_KEY_SAFE_WATCH_FACES] meta data is not defined
* then this will be [TargetWatchFaceSafety.UNKNOWN].
*
* Note if the [ComplicationDataSourceService] does not have the privileged permission
* `com.google.wear.permission.GET_IS_FOR_SAFE_WATCH_FACE`, then this will be
* [TargetWatchFaceSafety.UNKNOWN].
*/
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
@get:JvmName("isForSafeWatchFace")
@IsForSafeWatchFace
public val isForSafeWatchFace: Int = isForSafeWatchFace
@Deprecated("Use a constructor that specifies responseNeededSoon.")
constructor(
complicationInstanceId: Int,
complicationType: ComplicationType
) : this(complicationInstanceId, complicationType, false)
}
/**
* Defines constants that describe whether or not the watch face the complication is being requested
* for is deemed to be safe. I.e. if its in the list defined by the
* [ComplicationDataSourceService.METADATA_KEY_SAFE_WATCH_FACES] meta data in the
* [ComplicationDataSourceService]'s manifest.
*/
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
public object TargetWatchFaceSafety {
/**
* Prior to android T [ComplicationRequest.isForSafeWatchFace] is not supported and it will
* always be UNKNOWN. It will also be unknown if the [ComplicationDataSourceService]'s manifest
* doesn't define [ComplicationDataSourceService.METADATA_KEY_SAFE_WATCH_FACES], or if the
* [ComplicationDataSourceService] does not have the privileged permission
* `com.google.wear.permission.GET_IS_FOR_SAFE_WATCH_FACE`.
*/
public const val UNKNOWN: Int = 0
/**
* The watch face is a member of the list defined by the [ComplicationDataSourceService]'s
* [ComplicationDataSourceService.METADATA_KEY_SAFE_WATCH_FACES] meta data in its manifest.
*/
public const val SAFE: Int = 1
/**
* The watch face is NOT a member of the list defined by the [ComplicationDataSourceService]'s
* [ComplicationDataSourceService.METADATA_KEY_SAFE_WATCH_FACES] meta data in its manifest.
*/
public const val UNSAFE: Int = 2
}
@IntDef(
flag = true, // This is a flag to allow for future expansion.
value =
[TargetWatchFaceSafety.UNKNOWN, TargetWatchFaceSafety.SAFE, TargetWatchFaceSafety.UNSAFE]
)
@RestrictTo(RestrictTo.Scope.LIBRARY)
public annotation class IsForSafeWatchFace
/**
* Class for sources of complication data.
*
* A complication data source service must implement [onComplicationRequest] to respond to requests
* for updates from the complication system.
*
* Manifest requirements:
* - The manifest declaration of this service must include an intent filter for
* android.support.wearable.complications.ACTION_COMPLICATION_UPDATE_REQUEST.
* - A ComplicationDataSourceService must include a `meta-data` tag with
* android.support.wearable.complications.SUPPORTED_TYPES in its manifest entry. The value of this
* tag should be a comma separated list of types supported by the data source, from this table:
*
* | Androidx class | Tag name |
* |---------------------------------------|-------------------|
* | [GoalProgressComplicationData] | GOAL_PROGRESS |
* | [LongTextComplicationData] | LONG_TEXT |
* | [MonochromaticImageComplicationData] | ICON |
* | [PhotoImageComplicationData] | LARGE_IMAGE |
* | [RangedValueComplicationData] | RANGED_TEXT |
* | [ShortTextComplicationData] | SHORT_TEXT |
* | [SmallImageComplicationData] | SMALL_IMAGE |
* | [WeightedElementsComplicationData] | WEIGHTED_ELEMENTS |
*
* The order in which types are listed has no significance. In the case where a watch face supports
* multiple types in a single complication slot, the watch face will determine which types it
* prefers.
*
* For example, a complication data source that supports the RANGED_VALUE, SHORT_TEXT, and ICON
* types would include the following in its manifest entry:
* ```
* <meta-data android:name="android.support.wearable.complications.SUPPORTED_TYPES"
* android:value="RANGED_VALUE,SHORT_TEXT,ICON"/>
* ```
*
* From android T onwards, it is recommended for Complication DataSourceServices to be direct boot
* aware because the system is able to fetch complications before the lock screen has been removed.
* To do this add android:directBootAware="true" to your service tag.
*
* - A provider can choose to trust one or more watch faces by including the following in its
* manifest entry:
* ```
* <meta-data android:name="android.support.wearable.complications.SAFE_WATCH_FACES
* android:value="com.pkg1/com.trusted.wf1,com.pkg2/com.trusted.wf2"/>
* ```
* The listed watch faces will not need
* 'com.google.android.wearable.permission.RECEIVE_COMPLICATION_DATA' in order to receive
* complications from this provider. Also the provider may choose to serve different types to
* safe watch faces by including the following in its manifest:
*
* ```
* <meta-data android:name=
* "androidx.wear.watchface.complications.datasource.SAFE_WATCH_FACE_SUPPORTED_TYPES"
* android:value="ICON"/>
* ```
*
* In addition the provider can learn if a request is for a safe watchface by examining
* [ComplicationRequest.isForSafeWatchFace]. Note SAFE_WATCH_FACE_SUPPORTED_TYPES and
* isForSafeWatchFace are gated behind the privileged permission
* `com.google.wear.permission.GET_IS_FOR_SAFE_WATCH_FACE`.
*
* - A ComplicationDataSourceService should include a `meta-data` tag with
* android.support.wearable.complications.UPDATE_PERIOD_SECONDS its manifest entry. The value of
* this tag is the number of seconds the complication data source would like to elapse between
* update requests.
*
* Note that update requests are not guaranteed to be sent with this frequency.
*
* If a complication data source never needs to receive update requests beyond the one sent when a
* complication is activated, the value of this tag should be 0.
*
* For example, a complication data source that would like to update every ten minutes should
* include the following in its manifest entry:
* ```
* <meta-data android:name="android.support.wearable.complications.UPDATE_PERIOD_SECONDS"
* android:value="600"/>
* ```
*
* There is a lower limit for android.support.wearable.complications.UPDATE_PERIOD_SECONDS imposed
* by the system to prevent excessive power drain. For complications with frequent updates they can
* also register a separate [METADATA_KEY_IMMEDIATE_UPDATE_PERIOD_MILLISECONDS] meta data tag which
* supports sampling at up to 1Hz when the watch face is visible and non-ambient, however this also
* requires the DataSourceService to have the privileged permission
* `com.google.android.wearable.permission.USE_IMMEDIATE_COMPLICATION_UPDATE`.
*
* ```
* <meta-data android:name=
* "androidx.wear.watchface.complications.data.source.IMMEDIATE_UPDATE_PERIOD_MILLISECONDS"
* android:value="1000"/>
* ```
* - A ComplicationDataSourceService can include a `meta-data` tag with
* android.support.wearable.complications.PROVIDER_CONFIG_ACTION its manifest entry to cause a
* configuration activity to be shown when the complication data source is selected.
*
* The configuration activity must reside in the same package as the complication data source, and
* must register an intent filter for the action specified here, including
* android.support.wearable.complications.category.PROVIDER_CONFIG as well as
* [Intent.CATEGORY_DEFAULT] as categories.
*
* The complication id being configured will be included in the intent that starts the config
* activity using the extra key android.support.wearable.complications.EXTRA_CONFIG_COMPLICATION_ID.
*
* The complication type that will be requested from the complication data source will also be
* included, using the extra key android.support.wearable.complications
* .EXTRA_CONFIG_COMPLICATION_TYPE.
*
* The complication data source's [ComponentName] will also be included in the intent that starts
* the config activity, using the extra key
* android.support.wearable.complications.EXTRA_CONFIG_PROVIDER_COMPONENT.
*
* The config activity must call [Activity.setResult] with either [Activity.RESULT_OK] or
* [Activity.RESULT_CANCELED] before it is finished, to tell the system whether or not the
* complication data source should be set on the given complication.
*
* It is possible to provide additional 'meta-data' tag
* androidx.watchface.complications.datasource.DEFAULT_CONFIG_SUPPORTED in the service set to "true"
* to let the system know that the data source is able to provide complication data before it is
* configured.
* - The manifest entry for the service should also include an android:icon attribute. The icon
* provided there should be a single-color white icon that represents the complication data
* source. This icon will be shown in the complication data source chooser interface, and may also
* be included in [ComplicationProviderInfo] given to watch faces for display in their
* configuration activities.
* - The manifest entry should also include
* `android:permission="com.google.android.wearable.permission.BIND_COMPLICATION_PROVIDER"` to
* ensure that only the system can bind to it.
*
* Multiple complication data sources in the same APK are supported but in android R there's a soft
* limit of 100 data sources per APK. Above that the companion watchface editor won't support this
* complication data source app.
*
* There's no need to call setDataSource for any the ComplicationData Builders because the system
* will append this value on your behalf.
*/
public abstract class ComplicationDataSourceService : Service() {
private var wrapper: IComplicationProviderWrapper? = null
private var lastExpressionEvaluationJob: Job? = null
internal val mainThreadHandler by lazy { createMainThreadHandler() }
/**
* Equivalent to [Build.VERSION.SDK_INT], but allows override for any platform-independent
* versioning.
*
* This is meant to only be used in androidTest, which only support testing on one SDK. In
* Robolectric tests use `@Config(sdk = [Build.VERSION_CODES.*])`.
*
* Note that this cannot override platform-dependent versioning, which means inconsistency.
*/
@VisibleForTesting internal open val wearPlatformVersion = Build.VERSION.SDK_INT
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
open fun createMainThreadHandler() = Handler(Looper.getMainLooper())
@SuppressLint("SyntheticAccessor")
final override fun onBind(intent: Intent): IBinder? {
if (ACTION_COMPLICATION_UPDATE_REQUEST == intent.action) {
if (wrapper == null) {
wrapper = IComplicationProviderWrapper()
}
return wrapper
}
return null
}
override fun onDestroy() {
super.onDestroy()
lastExpressionEvaluationJob?.cancel()
}
/**
* Called when a complication is activated.
*
* This occurs when the watch face calls setActiveComplications, or when this data source is
* chosen for a complication which is already active.
*
* This will usually be followed by a call to [onComplicationRequest].
*
* This will be called on the main thread.
*
* @param complicationInstanceId The system's ID for the complication. Note this ID is distinct
* from the complication slot used by the watch face itself.
* @param type The [ComplicationType] of the activated slot.
*/
@MainThread
public open fun onComplicationActivated(complicationInstanceId: Int, type: ComplicationType) {}
/**
* Called when a complication data update is requested for the given complication id.
*
* In response to this request the result callback should be called with the data to be
* displayed. If the request can not be fulfilled or no update is needed then null should be
* passed to the callback.
*
* The callback doesn't have be called within onComplicationRequest but it should be called soon
* after. If this does not occur within around 20 seconds (exact timeout length subject to
* change), then the system will unbind from this service which may cause your eventual update
* to not be received. However if [ComplicationRequest.immediateResponseRequired] is `true` then
* provider should try to deliver the response in under 100 milliseconds, if `false` the
* deadline is 20 seconds. [ComplicationRequest.immediateResponseRequired] will only ever be
* `true` if [METADATA_KEY_IMMEDIATE_UPDATE_PERIOD_MILLISECONDS] is present in the manifest, and
* the provider has the privileged permission
* `com.google.android.wearable.permission.USE_IMMEDIATE_COMPLICATION_UPDATE`, and the
* complication is visible and non-ambient.
*
* @param request The details about the complication that has been requested.
* @param listener The callback to pass the result to the system.
*/
@MainThread
public abstract fun onComplicationRequest(
request: ComplicationRequest,
listener: ComplicationRequestListener
)
/**
* Called when sending [ComplicationData] (through [ComplicationRequestListener]) threw an
* error.
*
* This can be due to expression evaluation error, a [RemoteException], or something else.
*
* It's recommended to override this rather than catch exceptions directly on the
* [ComplicationRequestListener] call, because the implementation can be asynchronous.
*
* When the [ComplicationData] contains an expression, that expression is evaluated locally for
* backward compatibility with older platforms. This evaluation is asynchronous, which means an
* exception will not be thrown synchronously.
*
* IMPORTANT: If not overridden, the error will be propagated to the main thread (and
* potentially crash the process).
*
* @throws Throwable Thrown exception will be propagated to the main thread (and potentially
* crash the process).
*/
@MainThread
@SuppressWarnings("GenericException") // Error propagation.
@Throws(Throwable::class)
public open fun onComplicationDataError(throwable: Throwable) {
throw throwable
}
/**
* A request for representative preview data for the complication, for use in the editor UI.
* Preview data is assumed to be static per type. E.g. for a complication that displays the date
* and time of an event, rather than returning the real time it should return a fixed date and
* time such as 10:10 Aug 1st. This data may be cached by the system and the result should be
* constant based on the current locale.
*
* This will be called on a background thread.
*
* @param type The type of complication preview data requested.
* @return Preview data for the given complication type.
*/
public abstract fun getPreviewData(type: ComplicationType): ComplicationData?
/**
* Callback for [onComplicationRequest] where only one of [onComplicationData] or
* [onComplicationDataTimeline] should be called.
*/
@JvmDefaultWithCompatibility
public interface ComplicationRequestListener {
/**
* Sends the [ComplicationData] to the system. If null is passed then any previous
* complication data will not be overwritten. Can be called on any thread. Should only be
* called once. Note this is mutually exclusive with [onComplicationDataTimeline].
*
* Errors sending the data are provided to [onComplicationDataError], which re-throws the
* error, potentially crashing the main thread.
*
* As the implementation of [onComplicationData] may be asynchronous, it's better to
* override [onComplicationDataError] rather than catch exceptions thrown from this call.
*
* When the [ComplicationData] contains an expression, that expression is evaluated locally
* for backward compatibility with older platforms. This evaluation is asynchronous, which
* means an exception will not be thrown synchronously.
*/
@Throws(RemoteException::class)
public fun onComplicationData(complicationData: ComplicationData?)
/**
* Sends the [ComplicationDataTimeline] to the system. If null is passed then any previous
* complication data will not be overwritten. Can be called on any thread. Should only be
* called once. Note this is mutually exclusive with [onComplicationData]. Note only
* [ComplicationDataTimeline.defaultComplicationData] is supported by older watch faces .
*
* Errors sending the data are provided to [onComplicationDataError], which re-throws the
* error, potentially crashing the main thread.
*
* As the implementation of [onComplicationDataTimeline] may be asynchronous, it's better to
* override [onComplicationDataError] rather than catch exceptions thrown from this call.
*
* When the [ComplicationData] contains an expression, that expression is evaluated locally
* for backward compatibility with older platforms. This evaluation is asynchronous, which
* means an exception will not be thrown synchronously.
*/
// TODO(alexclarke): Plumb a capability bit so the developers can know if timelines are
// supported by the watch face.
@Throws(RemoteException::class)
public fun onComplicationDataTimeline(
complicationDataTimeline: ComplicationDataTimeline?
) {}
}
/**
* If a metadata key with [METADATA_KEY_IMMEDIATE_UPDATE_PERIOD_MILLISECONDS] is present in the
* manifest, and the provider has privileged permission
* `com.google.android.wearable.permission.USE_IMMEDIATE_COMPLICATION_UPDATE`, then
* [onStartImmediateComplicationRequests] will be called when the watch face is visible and
* non-ambient. A series of [onComplicationRequest]s will follow where
* [ComplicationRequest.immediateResponseRequired] is `true`, ending with a call to
* [onStopImmediateComplicationRequests].
*
* @param complicationInstanceId The system's ID for the complication. Note this ID is distinct
* from the complication slot used by the watch face itself.
*/
@MainThread public open fun onStartImmediateComplicationRequests(complicationInstanceId: Int) {}
/**
* If a metadata key with [METADATA_KEY_IMMEDIATE_UPDATE_PERIOD_MILLISECONDS] is present in the
* manifest, and the provider has privileged permission
* `com.google.android.wearable.permission.USE_IMMEDIATE_COMPLICATION_UPDATE`, then
* [onStartImmediateComplicationRequests] will be called when the watch face ceases to be
* visible and non-ambient. No subsequent calls to [onComplicationRequest] where
* [ComplicationRequest.immediateResponseRequired] is `true` will be made unless the
* complication becomes visible and non-ambient again.
*
* @param complicationInstanceId The system's ID for the complication. Note this ID is distinct
* from the complication slot used by the watch face itself.
*/
@MainThread public open fun onStopImmediateComplicationRequests(complicationInstanceId: Int) {}
/**
* Called when a complication is deactivated.
*
* This occurs when the current watch face changes, or when the watch face calls
* setActiveComplications and does not include the given complication (usually because the watch
* face has stopped displaying it).
*
* This will be called on the main thread.
*
* @param complicationInstanceId The system's ID for the complication. Note this ID is distinct
* from the complication slot used by the watch face itself.
*/
@MainThread public open fun onComplicationDeactivated(complicationInstanceId: Int) {}
private inner class IComplicationProviderWrapper : IComplicationProvider.Stub() {
@SuppressLint("SyntheticAccessor")
override fun onUpdate(complicationInstanceId: Int, type: Int, manager: IBinder) {
onUpdate2(complicationInstanceId, type, manager, bundle = null)
}
@SuppressLint("SyntheticAccessor")
override fun onUpdate2(
complicationInstanceId: Int,
type: Int,
manager: IBinder,
bundle: Bundle?
) {
val isForSafeWatchFace =
bundle?.getInt(
IComplicationProvider.BUNDLE_KEY_IS_SAFE_FOR_WATCHFACE,
TargetWatchFaceSafety.UNKNOWN
)
?: TargetWatchFaceSafety.UNKNOWN
val expectedDataType = fromWireType(type)
val iComplicationManager = IComplicationManager.Stub.asInterface(manager)
mainThreadHandler.post {
onComplicationRequest(
@Suppress("NewApi")
ComplicationRequest(
complicationInstanceId,
expectedDataType,
immediateResponseRequired = false,
isForSafeWatchFace = isForSafeWatchFace
),
object : ComplicationRequestListener {
override fun onComplicationData(complicationData: ComplicationData?) {
// This can be run on an arbitrary thread, but that's OK.
val dataType = complicationData?.type ?: ComplicationType.NO_DATA
require(
dataType != ComplicationType.NOT_CONFIGURED &&
dataType != ComplicationType.EMPTY
) {
"Cannot send data of TYPE_NOT_CONFIGURED or " +
"TYPE_EMPTY. Use TYPE_NO_DATA instead."
}
require(
dataType == ComplicationType.NO_DATA || dataType == expectedDataType
) {
"Complication data should match the requested type. " +
"Expected $expectedDataType got $dataType."
}
if (complicationData is NoDataComplicationData) {
complicationData.placeholder?.let {
require(it.type == expectedDataType) {
"Placeholder type must match the requested type. " +
"Expected $expectedDataType got ${it.type}."
}
}
}
complicationData?.asWireComplicationData().evaluateAndUpdateManager()
}
override fun onComplicationDataTimeline(
complicationDataTimeline: ComplicationDataTimeline?
) {
// This can be run on an arbitrary thread, but that's OK.
val defaultComplicationData =
complicationDataTimeline?.defaultComplicationData
val dataType = defaultComplicationData?.type ?: ComplicationType.NO_DATA
require(
dataType != ComplicationType.NOT_CONFIGURED &&
dataType != ComplicationType.EMPTY
) {
"Cannot send data of TYPE_NOT_CONFIGURED or " +
"TYPE_EMPTY. Use TYPE_NO_DATA instead."
}
require(
dataType == ComplicationType.NO_DATA || dataType == expectedDataType
) {
"Complication data should match the requested type. " +
"Expected $expectedDataType got $dataType."
}
if (
defaultComplicationData != null &&
defaultComplicationData is NoDataComplicationData
) {
defaultComplicationData.placeholder?.let {
require(it.type == expectedDataType) {
"Placeholder type must match the requested type. " +
"Expected $expectedDataType got ${it.type}."
}
}
}
complicationDataTimeline?.timelineEntries?.let { timelineEntries ->
for (timelineEntry in timelineEntries) {
val timelineComplicationData = timelineEntry.complicationData
if (timelineComplicationData is NoDataComplicationData) {
timelineComplicationData.placeholder?.let {
require(it.type == expectedDataType) {
"Timeline entry Placeholder types must match the " +
"requested type. Expected $expectedDataType " +
"got ${timelineComplicationData.type}."
}
}
} else {
require(timelineComplicationData.type == expectedDataType) {
"Timeline entry types must match the requested type. " +
"Expected $expectedDataType got " +
"${timelineComplicationData.type}."
}
}
}
}
complicationDataTimeline
?.asWireComplicationData()
.evaluateAndUpdateManager()
}
private fun WireComplicationData?.evaluateAndUpdateManager() {
// Cancelling any previous evaluation.
lastExpressionEvaluationJob?.cancel()
if (
// When no update is needed, the data is going to be null.
this == null ||
// Will be evaluated by the platform.
// TODO(b/257422920): Set this to the exact platform version.
wearPlatformVersion >= Build.VERSION_CODES.TIRAMISU + 1 ||
// Avoid async evaluation to prevent backward incompatibility
// with try/catch.
!hasExpression(this)
) {
try {
iComplicationManager.updateComplicationData(
complicationInstanceId,
this
)
} catch (e: Throwable) {
onComplicationDataError(e)
}
return
}
lastExpressionEvaluationJob =
CoroutineScope(Dispatchers.Main).launch {
// Doing one-off evaluation, the service will be re-invoked.
try {
iComplicationManager.updateComplicationData(
complicationInstanceId,
withTimeout(EXPRESSION_EVALUATION_TIMEOUT.toMillis()) {
ComplicationDataExpressionEvaluator()
.evaluate(this@evaluateAndUpdateManager)
.first()
}
)
} catch (e: Throwable) {
onComplicationDataError(e)
}
}
}
}
)
}
}
@SuppressLint("SyntheticAccessor")
override fun onComplicationDeactivated(complicationInstanceId: Int) {
mainThreadHandler.post {
this@ComplicationDataSourceService.onComplicationDeactivated(complicationInstanceId)
}
}
@SuppressLint("SyntheticAccessor")
override fun onComplicationActivated(
complicationInstanceId: Int,
type: Int,
manager: IBinder
) {
mainThreadHandler.post {
this@ComplicationDataSourceService.onComplicationActivated(
complicationInstanceId,
fromWireType(type)
)
}
}
override fun getApiVersion(): Int {
return API_VERSION
}
@SuppressLint("SyntheticAccessor")
override fun getComplicationPreviewData(
type: Int
): android.support.wearable.complications.ComplicationData? {
val expectedDataType = fromWireType(type)
val complicationData = getPreviewData(expectedDataType)
val dataType = complicationData?.type ?: ComplicationType.NO_DATA
require(dataType == ComplicationType.NO_DATA || dataType == expectedDataType) {
"Preview data should match the requested type. " +
"Expected $expectedDataType got $dataType."
}
if (complicationData != null) {
require(complicationData.validTimeRange == TimeRange.ALWAYS) {
"Preview data should have time range set to ALWAYS."
}
require(!hasExpression(complicationData.asWireComplicationData())) {
"Preview data must not have expressions."
}
}
return complicationData?.asWireComplicationData()
}
override fun onStartSynchronousComplicationRequests(complicationInstanceId: Int) {
mainThreadHandler.post {
this@ComplicationDataSourceService.onStartImmediateComplicationRequests(
complicationInstanceId
)
}
}
override fun onStopSynchronousComplicationRequests(complicationInstanceId: Int) {
mainThreadHandler.post {
this@ComplicationDataSourceService.onStopImmediateComplicationRequests(
complicationInstanceId
)
}
}
override fun onSynchronousComplicationRequest(complicationInstanceId: Int, type: Int) =
onSynchronousComplicationRequest2(complicationInstanceId, type, bundle = null)
override fun onSynchronousComplicationRequest2(
complicationInstanceId: Int,
type: Int,
bundle: Bundle?
): android.support.wearable.complications.ComplicationData? {
val isForSafeWatchFace =
bundle?.getInt(
IComplicationProvider.BUNDLE_KEY_IS_SAFE_FOR_WATCHFACE,
TargetWatchFaceSafety.UNKNOWN
)
?: TargetWatchFaceSafety.UNKNOWN
val expectedDataType = fromWireType(type)
val complicationType = fromWireType(type)
val latch = CountDownLatch(1)
var wireComplicationData: android.support.wearable.complications.ComplicationData? =
null
mainThreadHandler.post {
this@ComplicationDataSourceService.onComplicationRequest(
@Suppress("NewApi")
ComplicationRequest(
complicationInstanceId,
complicationType,
immediateResponseRequired = true,
isForSafeWatchFace = isForSafeWatchFace
),
object : ComplicationRequestListener {
override fun onComplicationData(complicationData: ComplicationData?) {
// This can be run on an arbitrary thread, but that's OK.
val dataType = complicationData?.type ?: ComplicationType.NO_DATA
require(
dataType != ComplicationType.NOT_CONFIGURED &&
dataType != ComplicationType.EMPTY
) {
"Cannot send data of TYPE_NOT_CONFIGURED or " +
"TYPE_EMPTY. Use TYPE_NO_DATA instead."
}
require(
dataType == ComplicationType.NO_DATA || dataType == expectedDataType
) {
"Complication data should match the requested type. " +
"Expected $expectedDataType got $dataType."
}
// When no update is needed, the complicationData is going to be null.
wireComplicationData = complicationData?.asWireComplicationData()
latch.countDown()
}
override fun onComplicationDataTimeline(
complicationDataTimeline: ComplicationDataTimeline?
) {
// This can be run on an arbitrary thread, but that's OK.
val dataType =
complicationDataTimeline?.defaultComplicationData?.type
?: ComplicationType.NO_DATA
require(
dataType != ComplicationType.NOT_CONFIGURED &&
dataType != ComplicationType.EMPTY
) {
"Cannot send data of TYPE_NOT_CONFIGURED or " +
"TYPE_EMPTY. Use TYPE_NO_DATA instead."
}
require(
dataType == ComplicationType.NO_DATA || dataType == expectedDataType
) {
"Complication data should match the requested type. " +
"Expected $expectedDataType got $dataType."
}
// When no update is needed, the complicationData is going to be null.
wireComplicationData =
complicationDataTimeline?.asWireComplicationData()
latch.countDown()
}
}
)
}
latch.await()
return wireComplicationData
}
}
public companion object {
/**
* The intent action used to send update requests to the data source.
* [ComplicationDataSourceService] must declare an intent filter for this action in the
* manifest.
*/
// TODO(b/192233205): Migrate value to androidx.
@SuppressWarnings("ActionValue")
public const val ACTION_COMPLICATION_UPDATE_REQUEST: String =
"android.support.wearable.complications.ACTION_COMPLICATION_UPDATE_REQUEST"
/**
* Metadata key used to declare supported complication types.
*
* A [ComplicationDataSourceService] must include a `meta-data` tag with this name in its
* manifest entry. The value of this tag should be a comma separated list of types supported
* by the complication data source. Types should be given as named as per the type fields in
* the [ComplicationData], but omitting the "TYPE_" prefix, e.g. `SHORT_TEXT`, `LONG_TEXT`,
* `RANGED_VALUE`.
*
* The order in which types are listed has no significance. In the case where a watch face
* supports multiple types in a single complication slot, the watch face will determine
* which types it prefers.
*
* For example, a complication data source that supports the RANGED_VALUE, SHORT_TEXT, and
* ICON type would include the following in its manifest entry:
* ```
* <meta-data android:name="android.support.wearable.complications.SUPPORTED_TYPES"
* android:value="RANGED_VALUE,SHORT_TEXT,ICON"/>
* ```
*/
// TODO(b/192233205): Migrate value to androidx.
public const val METADATA_KEY_SUPPORTED_TYPES: String =
"android.support.wearable.complications.SUPPORTED_TYPES"
/**
* Metadata key used to declare supported complication types for safe watch faces.
*
* Gated behind the privileged permission
* `com.google.wear.permission.GET_IS_FOR_SAFE_WATCH_FACE', this overrides the
* [METADATA_KEY_SUPPORTED_TYPES] list for 'safe' watch faces. I.e.
* watch faces in the [METADATA_KEY_SAFE_WATCH_FACES] metadata list.
*
* This means for example trusted watch faces could receive [ComplicationType.SHORT_TEXT]
* and untrusted ones [ComplicationType.MONOCHROMATIC_IMAGE].
*/
public const val METADATA_KEY_SAFE_WATCH_FACE_SUPPORTED_TYPES: String =
"androidx.wear.watchface.complications.datasource.SAFE_WATCH_FACE_SUPPORTED_TYPES"
/**
* Metadata key used to declare the requested frequency of update requests.
*
* A [ComplicationDataSourceService] should include a `meta-data` tag with this name in its
* manifest entry. The value of this tag is the number of seconds the complication data
* source would like to elapse between update requests.
*
* Note that update requests are not guaranteed to be sent with this frequency.
*
* If a complication data source never needs to receive update requests beyond the one sent
* when a complication is activated, the value of this tag should be 0.
*
* For example, a complication data source that would like to update every ten minutes
* should include the following in its manifest entry:
* ```
* <meta-data android:name="android.support.wearable.complications.UPDATE_PERIOD_SECONDS"
* android:value="600"/>
* ```
*/
// TODO(b/192233205): Migrate value to androidx.
public const val METADATA_KEY_UPDATE_PERIOD_SECONDS: String =
"android.support.wearable.complications.UPDATE_PERIOD_SECONDS"
/**
* Metadata key used to request elevated frequency of [onComplicationRequest]s when the
* watch face is visible and non-ambient.
*
* To do this [ComplicationDataSourceService] should include a `meta-data` tag with this
* name in its manifest entry. The value of this tag is the number of milliseconds the
* complication data source would like to elapse between [onComplicationRequest]s requests
* when the watch face is visible and non-ambient.
*
* Note in addition to this meta-data, the data source must also request the privileged
* permission com.google.android.wearable.permission.USE_IMMEDIATE_COMPLICATION_UPDATE.
*
* Note that update requests are not guaranteed to be sent with this frequency and a lower
* limit exists (initially 1 second).
*/
public const val METADATA_KEY_IMMEDIATE_UPDATE_PERIOD_MILLISECONDS: String =
"androidx.wear.watchface.complications.data.source." +
"IMMEDIATE_UPDATE_PERIOD_MILLISECONDS"
/**
* Metadata key used to declare a list of watch faces that may receive data from a
* complication data source before they are granted the RECEIVE_COMPLICATION_DATA
* permission. This allows the listed watch faces to set the complication data source as a
* default and have the complication populate when the watch face is first seen.
*
* Only trusted watch faces that will set this complication data source as a default should
* be included in this list.
*
* Note that if a watch face is in the same app package as the complication data source, it
* does not need to be added to this list.
*
* The value of this tag should be a comma separated list of watch faces or packages. An
* entry can be a flattened component, as if [ComponentName.flattenToString] had been
* called, to declare a specific watch face as safe. An entry can also be a package name, as
* if [ComponentName.getPackageName] had been called, in which case any watch face under the
* app with that package name will be considered safe for this complication data source.
*
* From Android T, if this provider has the privileged permission
* `com.google.wear.permission.GET_IS_FOR_SAFE_WATCH_FACE`, then
* [ComplicationRequest.isForSafeWatchFace] will be populated.
*/
// TODO(b/192233205): Migrate value to androidx.
public const val METADATA_KEY_SAFE_WATCH_FACES: String =
"android.support.wearable.complications.SAFE_WATCH_FACES"
/**
* Metadata key used to declare that the complication data source should be hidden from the
* complication data source chooser interface. If set to "true", users will not be able to
* select this complication data source. The complication data source may still be specified
* as a default complication data source by watch faces.
*/
// TODO(b/192233205): Migrate value to androidx.
internal const val METADATA_KEY_HIDDEN: String =
"android.support.wearable.complications.HIDDEN"
/**
* Metadata key used to declare an action for a configuration activity for a complication
* data source.
*
* A [ComplicationDataSourceService] can include a `meta-data` tag with this name in its
* manifest entry to cause a configuration activity to be shown when the complication data
* source is selected.
*
* The configuration activity must reside in the same package as the complication data
* source, and must register an intent filter for the action specified here, including
* [CATEGORY_DATA_SOURCE_CONFIG] as well as [Intent.CATEGORY_DEFAULT] as categories.
*
* The complication id being configured will be included in the intent that starts the
* config activity using the extra key [EXTRA_CONFIG_COMPLICATION_ID].
*
* The complication type that will be requested from the complication data source will also
* be included, using the extra key [EXTRA_CONFIG_COMPLICATION_TYPE].
*
* The complication data source's [ComponentName] will also be included in the intent that
* starts the config activity, using the extra key [EXTRA_CONFIG_DATA_SOURCE_COMPONENT].
*
* The config activity must call [Activity.setResult] with either [Activity.RESULT_OK] or
* [Activity.RESULT_CANCELED] before it is finished, to tell the system whether or not the
* complication data source should be set on the given complication.
*/
// TODO(b/192233205): Migrate value to androidx.
@SuppressLint("IntentName")
public const val METADATA_KEY_DATA_SOURCE_CONFIG_ACTION: String =
"android.support.wearable.complications.PROVIDER_CONFIG_ACTION"
/**
* Metadata key. Setting to "true" indicates to the system that this complication data
* source with a PROVIDER_CONFIG_ACTION metadata tag is able to provide complication data
* before it is configured. See [METADATA_KEY_DATA_SOURCE_CONFIG_ACTION].
*/
public const val METADATA_KEY_DATA_SOURCE_DEFAULT_CONFIG_SUPPORTED: String =
"androidx.watchface.complications.datasource.DEFAULT_CONFIG_SUPPORTED"
/**
* Category for complication data source config activities. The configuration activity for a
* complication complication data source must specify this category in its intent filter.
*
* @see METADATA_KEY_DATA_SOURCE_CONFIG_ACTION
*/
// TODO(b/192233205): Migrate value to androidx.
@SuppressLint("IntentName")
public const val CATEGORY_DATA_SOURCE_CONFIG: String =
"android.support.wearable.complications.category.PROVIDER_CONFIG"
/**
* Extra used to supply the complication id to a complication data source configuration
* activity.
*/
// TODO(b/192233205): Migrate value to androidx.
@SuppressLint("ActionValue")
public const val EXTRA_CONFIG_COMPLICATION_ID: String =
"android.support.wearable.complications.EXTRA_CONFIG_COMPLICATION_ID"
/**
* Extra used to supply the complication type to a complication data source configuration
* activity.
*/
// TODO(b/192233205): Migrate value to androidx.
@SuppressLint("ActionValue")
public const val EXTRA_CONFIG_COMPLICATION_TYPE: String =
"android.support.wearable.complications.EXTRA_CONFIG_COMPLICATION_TYPE"
/**
* Extra used to supply the complication data source component to a complication data source
* configuration activity.
*/
// TODO(b/192233205): Migrate value to androidx.
@SuppressLint("ActionValue")
public const val EXTRA_CONFIG_DATA_SOURCE_COMPONENT: String =
"android.support.wearable.complications.EXTRA_CONFIG_PROVIDER_COMPONENT"
/** How long to allow expression evaluation to execute. */
private val EXPRESSION_EVALUATION_TIMEOUT = Duration.ofSeconds(10)
}
}