I'm following the google billing integration instructions and have got stuck with how to await for the billing client connection result.
Whenever I need to query sku details or purchases I need to make sure that the billing client is initialized and connected. There are querySkuDetails and queryPurchasesAsync awaitable kotlin extension functions, but startConnection is based on listeners instead. Here are code samples from the docs.
private var billingClient = BillingClient.newBuilder(activity)
.setListener(purchasesUpdatedListener)
.enablePendingPurchases()
.build()
billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(billingResult: BillingResult) {
if (billingResult.responseCode == BillingResponseCode.OK) {
// The BillingClient is ready. You can query purchases here.
}
}
override fun onBillingServiceDisconnected() {
// Try to restart the connection on the next request to
// Google Play by calling the startConnection() method.
}
})
suspend fun querySkuDetails() {
// prepare params
// leverage querySkuDetails Kotlin extension function
val skuDetailsResult = withContext(Dispatchers.IO) {
billingClient.querySkuDetails(params.build())
}
// Process the result.
}
How to put all this together using suspend functions?
One way to create a suspending version of startConnection is the following:
/**
* Returns immediately if this BillingClient is already connected, otherwise
* initiates the connection and suspends until this client is connected.
*/
suspend fun BillingClient.ensureReady() {
if (isReady) {
return // fast path if already connected
}
return suspendCoroutine { cont ->
startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(billingResult: BillingResult) {
if (billingResult.responseCode == BillingResponseCode.OK) {
cont.resume(Unit)
} else {
// you could also use a custom, more precise exception
cont.resumeWithException(RuntimeException("Billing setup failed: ${billingResult.debugMessage} (code ${billingResult.responseCode})"))
}
}
override fun onBillingServiceDisconnected() {
// no need to setup reconnection logic here, call ensureReady()
// before each purchase to reconnect as necessary
}
})
}
}
This will fail if another coroutine already initiated a connection.
If you want to deal with potential concurrent calls to this method, you can use a mutex to protect the connection part:
val billingConnectionMutex = Mutex()
/**
* Returns immediately if this BillingClient is already connected, otherwise
* initiates the connection and suspends until this client is connected.
* If a connection is already in the process of being established, this
* method just suspends until the billing client is ready.
*/
suspend fun BillingClient.ensureReady() {
billingConnectionMutex.withLock {
// fast path: avoid suspension if another coroutine already connected
if (isReady) {
return
}
connectOrThrow()
}
}
private suspend fun BillingClient.connectOrThrow() = suspendCoroutine<Unit> { cont ->
startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(billingResult: BillingResult) {
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
cont.resume(Unit)
} else {
cont.resumeWithException(RuntimeException("Billing setup failed: ${billingResult.debugMessage} (code ${billingResult.responseCode})"))
}
}
override fun onBillingServiceDisconnected() {
// no need to setup reconnection logic here, call ensureReady()
// before each purchase to reconnect as necessary
}
})
}
Here, the release of the mutex corresponds to the end of connectOrThrow() for whichever other coroutine was holding it, so it's released as soon as the connection succeeds. If that other connection fails, this method will attempt the connection itself, and will succeed or fail on its own so the caller will be notified in case of error.
If you prefer to deal with result codes directly in if statements, you can return results instead of throwing:
private val billingConnectionMutex = Mutex()
private val resultAlreadyConnected = BillingResult.newBuilder()
.setResponseCode(BillingClient.BillingResponseCode.OK)
.setDebugMessage("Billing client is already connected")
.build()
/**
* Returns immediately if this BillingClient is already connected, otherwise
* initiates the connection and suspends until this client is connected.
* If a connection is already in the process of being established, this
* method just suspends until the billing client is ready.
*/
suspend fun BillingClient.connect(): BillingResult = billingConnectionMutex.withLock {
if (isReady) {
// fast path: avoid suspension if already connected
resultAlreadyConnected
} else {
unsafeConnect()
}
}
private suspend fun BillingClient.unsafeConnect() = suspendCoroutine<BillingResult> { cont ->
startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(billingResult: BillingResult) {
cont.resume(billingResult)
}
override fun onBillingServiceDisconnected() {
// no need to setup reconnection logic here, call ensureReady()
// before each purchase to reconnect as necessary
}
})
}
As discussed in BillingClient.BillingClientStateListener.onBillingSetupFinished is called multiple times, it is not possible to translate startConnection into the Coroutine world, because its callback will be called multiple times.
Thanks to Joffrey's answer and comments I have managed to construct a solution with BillingResult returned
val billingConnectionMutex = Mutex()
suspend fun BillingClient.connect(): BillingResult =
billingConnectionMutex.withLock {
suspendCoroutine { cont ->
startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(billingResult: BillingResult) {
if (billingResult.responseCode != BillingResponseCode.OK)
Log.d(TAG, "Billing setup failed: ${billingResult.debugMessage} (code ${billingResult.responseCode})")
cont.resume(billingResult)
}
override fun onBillingServiceDisconnected() {}
})
}
}
and then call it like this within the billing flow
if(billingClient!!.connect().responseCode != BillingResponseCode.OK)
return
If billingClient is already connected it will return responseCode.OK, so no need to check for isReady.
Related
I have my code below
interface Listener {
fun onGetData(data: Int)
fun onClose()
}
class MyEmitter {
var listener: Listener? = null
fun sendData(data: Int) = listener?.onGetData(data)
fun close() = listener?.onClose()
}
fun handleInput(myEmitter: MyEmitter) = channelFlow {
myEmitter.listener = object:Listener {
override fun onGetData(data: Int) { trySend(data) }
override fun onClose() { close() }
}
}
fun main(): Unit = runBlocking {
val myEmitter = MyEmitter()
handleInput(myEmitter).collect {
println(it)
}
myEmitter.sendData(1)
myEmitter.sendData(2)
myEmitter.close()
}
Whenever I send the data e.g. myEmitter.sendData(1), it does get into trySend(data), but the result is closed.
Why is it closed? How can I keep it open?
I think it's not documented terribly clearly, but just like the flow builder, the channelFlow's Flow is considered complete once the suspend lambda returns. Since all you are doing is setting a listener and not waiting around, it will return almost immediately. When a channel Flow is completed, it's channel is also closed.
If you want your channelFlow to stay open until the Flow is canceled, call awaitClose() at the end. This function suspends until the channel is closed, so it will hold your Flow open until it's canceled or the event in your listener closes the Channel.
fun handleInput(myEmitter: MyEmitter) = channelFlow {
myEmitter.listener = object:Listener {
override fun onGetData(data: Int) { trySend(data) }
override fun onClose() { close() }
}
awaitClose()
}
If you are familiar with callbackFlow, it is a specialized version of channelFlow and it enforces the awaitClose() call because it is meant for waiting for a listener, so there's no reason you would ever not want to await. It's also where you can deregister any listener you created inside the flow builder.
To get this working, I did 3 things
Add awaitClose to ensure the flow is not terminated
Move the entire flow behind launch, so that it is not blocking the main() function flow
Add a little delay before myEmitter.sendData(1), so that to ensure the launch get triggered first before doing the external sendData.
Full changed code as below
interface Listener {
fun onGetData(data: Int)
fun onClose()
}
class MyEmitter {
var listener: Listener? = null
fun sendData(data: Int) = listener?.onGetData(data)
fun close() = listener?.onClose()
}
fun handleInput(myEmitter: MyEmitter) = channelFlow {
myEmitter.listener = object:Listener {
override fun onGetData(data: Int) { trySend(data) }
override fun onClose() { close() }
}
awaitClose { myEmitter.listener = null } // Need awaitClose to keep the flow alive
}
fun main(): Unit = runBlocking {
val myEmitter = MyEmitter()
launch { // Need to run to avoid it from blocking the main() function flow due to having `awaitClose` there
handleInput(myEmitter).collect {
println(it)
}
}
delay(100) // Add some delay to get this triggered after the launch run
myEmitter.sendData(1)
myEmitter.sendData(2)
myEmitter.close()
}
The 3rd step is a little hack I think.
I am trying to create a polling mechanism with kotlin coroutines using sharedFlow and want to stop when there are no subscribers and active when there is at least one subscriber. My question is, is sharedFlow the right choice in this scenario or should I use channel. I tried using channelFlow but I am unaware how to close the channel (not cancel the job) outside the block body. Can someone help? Here's the snippet.
fun poll(id: String) = channelFlow {
while (!isClosedForSend) {
try {
send(repository.getDetails(id))
delay(MIN_REFRESH_TIME_MS)
} catch (throwable: Throwable) {
Timber.e("error -> ${throwable.message}")
}
invokeOnClose { Timber.e("channel flow closed.") }
}
}
You can use SharedFlow which emits values in a broadcast fashion (won't emit new value until the previous one is consumed by all the collectors).
val sharedFlow = MutableSharedFlow<String>()
val scope = CoroutineScope(Job() + Dispatchers.IO)
var producer: Job()
scope.launch {
val producer = launch() {
sharedFlow.emit(...)
}
sharedFlow.subscriptionCount
.map {count -> count > 0}
.distinctUntilChanged()
.collect { isActive -> if (isActive) stopProducing() else startProducing()
}
fun CoroutineScope.startProducing() {
producer = launch() {
sharedFlow.emit(...)
}
}
fun stopProducing() {
producer.cancel()
}
First of all, when you call channelFlow(block), there is no need to close the channel manually. The channel will be closed automatically after the execution of block is done.
I think the "produce" coroutine builder function may be what you need. But unfortunately, it's still an experimental api.
fun poll(id: String) = someScope.produce {
invokeOnClose { Timber.e("channel flow closed.") }
while (true) {
try {
send(repository.getDetails(id))
// delay(MIN_REFRESH_TIME_MS) //no need
} catch (throwable: Throwable) {
Timber.e("error -> ${throwable.message}")
}
}
}
fun main() = runBlocking {
val channel = poll("hello")
channel.receive()
channel.cancel()
}
The produce function will suspended when you don't call the returned channel's receive() method, so there is no need to delay.
UPDATE: Use broadcast for sharing values across multiple ReceiveChannel.
fun poll(id: String) = someScope.broadcast {
invokeOnClose { Timber.e("channel flow closed.") }
while (true) {
try {
send(repository.getDetails(id))
// delay(MIN_REFRESH_TIME_MS) //no need
} catch (throwable: Throwable) {
Timber.e("error -> ${throwable.message}")
}
}
}
fun main() = runBlocking {
val broadcast = poll("hello")
val channel1 = broadcast.openSubscription()
val channel2 = broadcast.openSubscription()
channel1.receive()
channel2.receive()
broadcast.cancel()
}
Consider an asynchronous API that reports progress on its operations:
suspend fun operationWithIO(input: String, progressUpdate: (String) -> Unit): String {
withContext(Dispatchers.IO) {
// ...
}
}
Is it possible to implement calls to progressUpdate such that callbacks are handled on the caller's dispatcher? Or is there a better way to deliver status updates back to the caller?
You should send progress updates on a channel. That will allow the caller to listen to the channel using whatever dispatcher it wants.
suspend fun operationWithIO(input: String, progressChannel: Channel<String>): String {
withContext(Dispatchers.IO) {
// ...
progressChannel.send("Done!")
progressChannel.close()
}
}
The caller can use it by doing something like this:
val progressChannel = Channel<String>()
someScope.launch {
operationWithIO(input, progressChannel)
}
// Remember the call to progressChannel.close(), so that this iteration stops.
for (progressUpdate in progressChannel) {
println(progressUpdate)
}
How about wrapping the callback function and calling the wrapped function:
/** Return a new callback that invokes this callback on the current context. */
suspend fun <T> ((T) -> Unit).onCurrentContext(): (T) -> Unit =
coroutineContext.let { context ->
{ value: T ->
runBlocking {
launch(context) {
this#onCurrentContext.invoke(value)
}
}
}
}
/** Perform a background operation, delivering status updates on the caller's context. */
suspend fun operationWithIO(statusUpdate: (String) -> Unit): String {
val cb = statusUpdate.onCurrentContext()
return withContext(Dispatchers.IO) {
cb("Phase 1")
delay(150)
cb("Phase 2")
delay(150)
"Result"
}
}
// In use
runBlocking {
val result = operationWithIO {
println("received callback status $it")
}
println("result is $result")
}
I need to make a sync call to reauthenticate the user and get a new token, but I haven't found a way that works. The code below blocks the thread and it is never unblocked, ie. I have an infinite loop
class ApolloAuthenticator(private val authenticated: Boolean) : Authenticator {
#Throws(IOException::class)
override fun authenticate(route: Route, response: Response): Request? {
// Refresh your access_token using a synchronous api request
if (response.request().header(HEADER_KEY_APOLLO_AUTHORIZATION) != null) {
return null //if you've tried to authorize and failed, give up
}
synchronized(this) {
refreshTokenSync() // This is blocked and never unblocked
val newToken = getApolloTokenFromSharedPreference()
return response.request().newBuilder()
.header(HEADER_KEY_APOLLO_AUTHORIZATION, newToken)
.build()
}
private fun refreshTokenSync(): EmptyResult {
//Refresh token, synchronously
val repository = Injection.provideSignInRepository()
return repository
.signInGraphQL()
.toBlocking()
.first()
}
fun signInGraphQL() : Observable<EmptyResult> =
sharedPreferencesDataSource.identifier
.flatMap { result -> graphqlAuthenticationDataSource.getAuth(result) }
.flatMap { result -> sharedPreferencesDataSource.saveApolloToken(result) }
.onErrorReturn { EmptyResult() }
}
---------- Use of it
val apollAuthenticator = ApolloAuthenticator(authenticated)
val okHttpBuilder =
OkHttpClient.Builder()
.authenticator(apollAuthenticator)
I haven't found a way to make a sync call using RxJava, but I can make it by using kotlin coutorine runBlocking, which will block the thread until the request is finished:
synchronized(this) {
runBlocking {
val subscription = ApolloReauthenticator.signInGraphQl() // await until it's finished
subscription.unsubscribe()
}
}
fun signInGraphQl(): Subscription {
return repository.refreshToken()
.subscribe(
{ Observable.just(EmptyResult()) },
{ Observable.just(EmptyResult()) }
)
}
So I have a method that binds to the service.
fun bindService() {
val intent = Intent(this, BluetoothService::class.java)
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
}
Inside onCreate method I use this code:
bindService()
launch {
delay(500L)
service = serviceConnection.serviceBinder?.getService() as BluetoothService
}
Is there more elegant way to wait for the service to be bound than using delay()?
I wrote this just now, and haven't tried it, but hopefully something like it could work. The magic is in suspendCoroutine, which pauses the current coroutine and then gives you a continuation thingy you can use to resume it later. In our case we resume it when the onServiceConnected is called.
// helper class which holds data
class BoundService(
private val context: Context,
val name: ComponentName?,
val service: IBinder?,
val conn: ServiceConnection) {
fun unbind() {
context.unbindService(conn)
}
}
// call within a coroutine to bind service, waiting for onServiceConnected
// before the coroutine resumes
suspend fun bindServiceAndWait(context: Context, intent: Intent, flags: Int) = suspendCoroutine<BoundService> { continuation ->
val conn = object: ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
continuation.resume(BoundService(context, name, service, this))
}
override fun onServiceDisconnected(name: ComponentName?) {
// ignore, not much we can do
}
}
context.bindService(intent, conn, flags)
}
// just an example
suspend fun exampleUsage() {
val bs = bindServiceAndWait(context, intent, Context.BIND_AUTO_CREATE)
try {
// ...do something with bs.service...
} finally {
bs.unbind()
}
}