/*
* 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.client
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import androidx.concurrent.futures.ResolvableFuture
import androidx.wear.complications.data.ComplicationData
import androidx.wear.watchface.control.IInteractiveWatchFaceWCS
import androidx.wear.watchface.control.IPendingInteractiveWatchFaceWCS
import androidx.wear.watchface.control.IWatchFaceControlService
import androidx.wear.watchface.control.WatchFaceControlService
import androidx.wear.watchface.control.data.HeadlessWatchFaceInstanceParams
import androidx.wear.watchface.control.data.WallpaperInteractiveWatchFaceInstanceParams
import androidx.wear.watchface.data.IdAndComplicationDataWireFormat
import androidx.wear.watchface.style.UserStyle
import androidx.wear.watchface.style.data.UserStyleWireFormat
import com.google.common.util.concurrent.ListenableFuture
/**
* Connects to a watch face's WatchFaceControlService which allows the user to control the watch
* face.
*/
public interface WatchFaceControlClient : AutoCloseable {
public companion object {
/**
* Constructs a [WatchFaceControlClient] which attempts to connect to a watch face in the
* android package [watchFacePackageName]. If this fails the [ListenableFuture]s returned by
* WatchFaceControlClient methods will fail with [ServiceNotBoundException].
*/
@JvmStatic
public fun createWatchFaceControlClient(
/** Calling application's [Context]. */
context: Context,
/** The name of the package containing the watch face control service to bind to. */
watchFacePackageName: String
): WatchFaceControlClient = WatchFaceControlClientImpl(
context,
Intent(WatchFaceControlService.ACTION_WATCHFACE_CONTROL_SERVICE).apply {
this.setPackage(watchFacePackageName)
}
)
}
/**
* Exception thrown by [WatchFaceControlClient] methods when the remote service is not bound.
*/
public class ServiceNotBoundException : Exception()
/**
* Returns the [InteractiveWatchFaceSysUiClient] for the given instance id, or null if no such
* instance exists.
*
* When finished call [InteractiveWatchFaceSysUiClient.close] to release resources.
*
* @param instanceId The name of the interactive watch face instance to retrieve
* @return A [ListenableFuture] for the [InteractiveWatchFaceSysUiClient] or `null` if
* [instanceId] is unrecognized, or [ServiceNotBoundException] if the
* WatchFaceControlService is not bound.
*/
public fun getInteractiveWatchFaceSysUiClientInstance(
instanceId: String
): ListenableFuture<InteractiveWatchFaceSysUiClient?>
/**
* Creates a [HeadlessWatchFaceClient] with the specified [DeviceConfig]. Screenshots made with
* [HeadlessWatchFaceClient.takeWatchFaceScreenshot] will be `surfaceWidth` x `surfaceHeight` in
* size.
*
* When finished call [HeadlessWatchFaceClient.close] to release resources.
*
* @param watchFaceName The [ComponentName] of the watch face to create a headless instance for
* must be in the same APK the WatchFaceControlClient is connected to. NB a single apk can
* contain multiple watch faces.
* @param deviceConfig The hardware [DeviceConfig]
* @param surfaceWidth The width of screen shots taken by the [HeadlessWatchFaceClient]
* @param surfaceHeight The height of screen shots taken by the [HeadlessWatchFaceClient]
* @return A [ListenableFuture] for the [HeadlessWatchFaceClient] or `null` if [watchFaceName]
* is unrecognized, or [ServiceNotBoundException] if the WatchFaceControlService is not
* bound.
*/
public fun createHeadlessWatchFaceClient(
watchFaceName: ComponentName,
deviceConfig: DeviceConfig,
surfaceWidth: Int,
surfaceHeight: Int
): ListenableFuture<HeadlessWatchFaceClient?>
/**
* Requests either an existing [InteractiveWatchFaceWcsClient] with the specified [id] or
* schedules creation of an [InteractiveWatchFaceWcsClient] for the next time the
* WallpaperService creates an engine.
*
* NOTE that currently only one [InteractiveWatchFaceWcsClient] per process can exist at a time.
*
* @param id The ID for the requested [InteractiveWatchFaceWcsClient].
* @param deviceConfig The [DeviceConfig] for the wearable.
* @param systemState The initial [SystemState] for the wearable.
* @param userStyle The initial style map (see [UserStyle]), or null if the default should be
* used.
* @param idToComplicationData The initial complication data, or null if unavailable.
* @return a [ListenableFuture] for a [InteractiveWatchFaceWcsClient]
*/
public fun getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClient(
id: String,
deviceConfig: DeviceConfig,
systemState: SystemState,
userStyle: Map<String, String>?,
idToComplicationData: Map<Int, ComplicationData>?
): ListenableFuture<InteractiveWatchFaceWcsClient>
}
internal class WatchFaceControlClientImpl internal constructor(
private val context: Context,
serviceIntent: Intent
) : WatchFaceControlClient {
internal var serviceFuture = ResolvableFuture.create<IWatchFaceControlService?>()
private val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, binder: IBinder) {
serviceFuture.set(IWatchFaceControlService.Stub.asInterface(binder))
}
override fun onServiceDisconnected(name: ComponentName) {
serviceFuture.set(null)
}
}
init {
if (!context.bindService(serviceIntent, serviceConnection, Context.BIND_AUTO_CREATE)) {
serviceFuture.set(null)
}
}
override fun getInteractiveWatchFaceSysUiClientInstance(
instanceId: String
): ListenableFuture<InteractiveWatchFaceSysUiClient?> {
val resultFuture = ResolvableFuture.create<InteractiveWatchFaceSysUiClient>()
serviceFuture.addListener(
{
val service = serviceFuture.get()
if (service == null) {
resultFuture.setException(WatchFaceControlClient.ServiceNotBoundException())
} else {
resultFuture.set(
InteractiveWatchFaceSysUiClientImpl(
service.getInteractiveWatchFaceInstanceSysUI(instanceId)
)
)
}
},
{ runnable -> runnable.run() }
)
return resultFuture
}
override fun createHeadlessWatchFaceClient(
watchFaceName: ComponentName,
deviceConfig: DeviceConfig,
surfaceWidth: Int,
surfaceHeight: Int
): ListenableFuture<HeadlessWatchFaceClient?> {
val resultFuture = ResolvableFuture.create<HeadlessWatchFaceClient?>()
serviceFuture.addListener(
{
val service = serviceFuture.get()
if (service == null) {
resultFuture.setException(WatchFaceControlClient.ServiceNotBoundException())
} else {
resultFuture.set(
HeadlessWatchFaceClientImpl(
service.createHeadlessWatchFaceInstance(
HeadlessWatchFaceInstanceParams(
watchFaceName,
androidx.wear.watchface.data.DeviceConfig(
deviceConfig.hasLowBitAmbient,
deviceConfig.hasBurnInProtection,
deviceConfig.analogPreviewReferenceTimeMillis,
deviceConfig.digitalPreviewReferenceTimeMillis
),
surfaceWidth,
surfaceHeight
)
)
)
)
}
},
{ runnable -> runnable.run() }
)
return resultFuture
}
override fun getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClient(
id: String,
deviceConfig: DeviceConfig,
systemState: SystemState,
userStyle: Map<String, String>?,
idToComplicationData: Map<Int, ComplicationData>?
): ListenableFuture<InteractiveWatchFaceWcsClient> {
val resultFuture = ResolvableFuture.create<InteractiveWatchFaceWcsClient>()
serviceFuture.addListener(
{
val service = serviceFuture.get()
if (service == null) {
resultFuture.setException(WatchFaceControlClient.ServiceNotBoundException())
} else {
val existingInstance = service.getOrCreateInteractiveWatchFaceWCS(
WallpaperInteractiveWatchFaceInstanceParams(
id,
androidx.wear.watchface.data.DeviceConfig(
deviceConfig.hasLowBitAmbient,
deviceConfig.hasBurnInProtection,
deviceConfig.analogPreviewReferenceTimeMillis,
deviceConfig.digitalPreviewReferenceTimeMillis
),
androidx.wear.watchface.data.SystemState(
systemState.inAmbientMode,
systemState.interruptionFilter
),
UserStyleWireFormat(userStyle ?: emptyMap()),
idToComplicationData?.map {
IdAndComplicationDataWireFormat(
it.key,
it.value.asWireComplicationData()
)
}
),
object : IPendingInteractiveWatchFaceWCS.Stub() {
override fun getApiVersion() =
IPendingInteractiveWatchFaceWCS.API_VERSION
override fun onInteractiveWatchFaceWcsCreated(
iInteractiveWatchFaceWcs: IInteractiveWatchFaceWCS
) {
resultFuture.set(
InteractiveWatchFaceWcsClientImpl(iInteractiveWatchFaceWcs)
)
}
}
)
existingInstance?.let {
resultFuture.set(InteractiveWatchFaceWcsClientImpl(it))
}
}
},
{ runnable -> runnable.run() }
)
return resultFuture
}
override fun close() {
context.unbindService(serviceConnection)
serviceFuture.set(null)
}
}