TestTileClient.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.tiles.testing
import android.app.Application
import android.app.Service
import android.content.ComponentName
import android.content.Intent
import androidx.test.core.app.ApplicationProvider.getApplicationContext
import androidx.wear.tiles.RequestBuilders
import androidx.wear.tiles.ResourceBuilders
import androidx.wear.tiles.TileBuilders
import androidx.wear.tiles.TileService
import androidx.wear.tiles.client.TileClient
import androidx.wear.tiles.connection.DefaultTileClient
import com.google.common.util.concurrent.ListenableFuture
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import org.robolectric.Shadows.shadowOf
import org.robolectric.android.controller.ServiceController
import java.util.concurrent.Executor
/**
* [TileClient] for testing purposes. This will pass calls through to the given instance of
* [TileService], handling all the service binding (via Robolectric), and
* serialization/deserialization.
*
* Note that this class will not drive the full service lifecycle for the passed service instance.
* On the first call to any of these methods, it will call your service's [Service.onCreate] method,
* however, it will never call [Service.onDestroy]. Equally, where [DefaultTileClient] will
* unbind after a period of time, potentially destroying the service, this class wil Client will
* unbind, but not destroy the service. If you wish to test service destruction, you can instead
* call [Service.onDestroy] on the passed in `service` instance.
*/
public class TestTileClient<T : TileService> :
TileClient {
private val controller: ServiceController<T>
private val componentName: ComponentName
private val innerTileService: DefaultTileClient
private var hasBound = false
/**
* Build a [TestTileClient] for use with a coroutine dispatcher.
*
* @param service An instance of the [TileService] class to bind to.
* @param coroutineScope A [CoroutineScope] to use when dispatching calls to the
* [TileService]. Cancelling the passed [CoroutineScope] will also cancel any pending
* work in this class.
* @param coroutineDispatcher A [CoroutineDispatcher] to use when dispatching work from this
* class.
*/
public constructor(
service: T,
coroutineScope: CoroutineScope,
coroutineDispatcher: CoroutineDispatcher
) {
val bindIntent = Intent(TileService.ACTION_BIND_TILE_PROVIDER)
this.componentName = ComponentName(getApplicationContext(), service.javaClass)
bindIntent.component = componentName
this.controller = ServiceController.of(service, bindIntent)
this.innerTileService = DefaultTileClient(
getApplicationContext(),
componentName,
coroutineScope,
coroutineDispatcher
)
}
/**
* Build a [TestTileClient] for use with a given [Executor]
*
* @param service An instance of the [TileService] class to bind to.
* @param executor An [Executor] to use when dispatching calls to the [TileService].
*/
public constructor(service: T, executor: Executor) {
val bindIntent = Intent(TileService.ACTION_BIND_TILE_PROVIDER)
this.componentName = ComponentName(getApplicationContext(), service.javaClass)
bindIntent.component = componentName
this.controller = ServiceController.of(service, bindIntent)
this.innerTileService = DefaultTileClient(
getApplicationContext(),
componentName,
executor
)
}
override fun requestApiVersion(): ListenableFuture<Int> {
maybeBind()
return innerTileService.requestApiVersion()
}
override fun requestTile(
requestParams: RequestBuilders.TileRequest
): ListenableFuture<TileBuilders.Tile> {
maybeBind()
return innerTileService.requestTile(requestParams)
}
override fun requestResources(
requestParams: RequestBuilders.ResourcesRequest
): ListenableFuture<ResourceBuilders.Resources> {
maybeBind()
return innerTileService.requestResources(requestParams)
}
override fun sendOnTileAddedEvent(): ListenableFuture<Void?> {
maybeBind()
return innerTileService.sendOnTileAddedEvent()
}
override fun sendOnTileRemovedEvent(): ListenableFuture<Void?> {
maybeBind()
return innerTileService.sendOnTileRemovedEvent()
}
override fun sendOnTileEnterEvent(): ListenableFuture<Void?> {
maybeBind()
return innerTileService.sendOnTileEnterEvent()
}
override fun sendOnTileLeaveEvent(): ListenableFuture<Void?> {
maybeBind()
return innerTileService.sendOnTileLeaveEvent()
}
private fun maybeBind() {
if (!hasBound) {
val binder = controller.create().get().onBind(controller.intent)
shadowOf(getApplicationContext<Application>())
.setComponentNameAndServiceForBindServiceForIntent(
controller.intent,
componentName,
binder
)
}
}
}