Rework billing client integration.

This commit is contained in:
Alex Hart
2025-06-04 09:17:53 -03:00
committed by Cody Henthorne
parent c3dcdd2010
commit a85b8c49d9

View File

@@ -22,20 +22,14 @@ import com.android.billingclient.api.QueryProductDetailsParams
import com.android.billingclient.api.QueryPurchasesParams import com.android.billingclient.api.QueryPurchasesParams
import com.android.billingclient.api.queryProductDetails import com.android.billingclient.api.queryProductDetails
import com.android.billingclient.api.queryPurchasesAsync import com.android.billingclient.api.queryPurchasesAsync
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.retry
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -75,6 +69,7 @@ internal class BillingApiImpl(
private val connectionState = MutableStateFlow<State>(State.Init) private val connectionState = MutableStateFlow<State>(State.Init)
private val coroutineScope = CoroutineScope(Dispatchers.Default) private val coroutineScope = CoroutineScope(Dispatchers.Default)
private val connectionStateDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
private val internalResults = MutableSharedFlow<BillingPurchaseResult>() private val internalResults = MutableSharedFlow<BillingPurchaseResult>()
@@ -102,49 +97,61 @@ internal class BillingApiImpl(
} }
} }
} }
BillingResponseCode.BILLING_UNAVAILABLE -> { BillingResponseCode.BILLING_UNAVAILABLE -> {
Log.d(TAG, "purchasesUpdatedListener: Billing unavailable.") Log.d(TAG, "purchasesUpdatedListener: Billing unavailable.")
BillingPurchaseResult.BillingUnavailable BillingPurchaseResult.BillingUnavailable
} }
BillingResponseCode.USER_CANCELED -> { BillingResponseCode.USER_CANCELED -> {
Log.d(TAG, "purchasesUpdatedListener: User cancelled.") Log.d(TAG, "purchasesUpdatedListener: User cancelled.")
BillingPurchaseResult.UserCancelled BillingPurchaseResult.UserCancelled
} }
BillingResponseCode.ERROR -> { BillingResponseCode.ERROR -> {
Log.d(TAG, "purchasesUpdatedListener: error.") Log.d(TAG, "purchasesUpdatedListener: error.")
BillingPurchaseResult.GenericError BillingPurchaseResult.GenericError
} }
BillingResponseCode.NETWORK_ERROR -> { BillingResponseCode.NETWORK_ERROR -> {
Log.d(TAG, "purchasesUpdatedListener: Network error.") Log.d(TAG, "purchasesUpdatedListener: Network error.")
BillingPurchaseResult.NetworkError BillingPurchaseResult.NetworkError
} }
BillingResponseCode.DEVELOPER_ERROR -> { BillingResponseCode.DEVELOPER_ERROR -> {
Log.d(TAG, "purchasesUpdatedListener: Developer error.") Log.d(TAG, "purchasesUpdatedListener: Developer error.")
BillingPurchaseResult.GenericError BillingPurchaseResult.GenericError
} }
BillingResponseCode.FEATURE_NOT_SUPPORTED -> { BillingResponseCode.FEATURE_NOT_SUPPORTED -> {
Log.d(TAG, "purchasesUpdatedListener: Feature not supported.") Log.d(TAG, "purchasesUpdatedListener: Feature not supported.")
BillingPurchaseResult.FeatureNotSupported BillingPurchaseResult.FeatureNotSupported
} }
BillingResponseCode.ITEM_ALREADY_OWNED -> { BillingResponseCode.ITEM_ALREADY_OWNED -> {
Log.d(TAG, "purchasesUpdatedListener: Already owned.") Log.d(TAG, "purchasesUpdatedListener: Already owned.")
BillingPurchaseResult.AlreadySubscribed BillingPurchaseResult.AlreadySubscribed
} }
BillingResponseCode.ITEM_NOT_OWNED -> { BillingResponseCode.ITEM_NOT_OWNED -> {
error("This shouldn't happen during the purchase process") error("This shouldn't happen during the purchase process")
} }
BillingResponseCode.ITEM_UNAVAILABLE -> { BillingResponseCode.ITEM_UNAVAILABLE -> {
Log.d(TAG, "purchasesUpdatedListener: Item is unavailable") Log.d(TAG, "purchasesUpdatedListener: Item is unavailable")
BillingPurchaseResult.TryAgainLater BillingPurchaseResult.TryAgainLater
} }
BillingResponseCode.SERVICE_UNAVAILABLE -> { BillingResponseCode.SERVICE_UNAVAILABLE -> {
Log.d(TAG, "purchasesUpdatedListener: Service is unavailable.") Log.d(TAG, "purchasesUpdatedListener: Service is unavailable.")
BillingPurchaseResult.TryAgainLater BillingPurchaseResult.TryAgainLater
} }
BillingResponseCode.SERVICE_DISCONNECTED -> { BillingResponseCode.SERVICE_DISCONNECTED -> {
Log.d(TAG, "purchasesUpdatedListener: Service is disconnected.") Log.d(TAG, "purchasesUpdatedListener: Service is disconnected.")
BillingPurchaseResult.TryAgainLater BillingPurchaseResult.TryAgainLater
} }
else -> { else -> {
Log.d(TAG, "purchasesUpdatedListener: No purchases.") Log.d(TAG, "purchasesUpdatedListener: No purchases.")
BillingPurchaseResult.None BillingPurchaseResult.None
@@ -163,19 +170,6 @@ internal class BillingApiImpl(
) )
.build() .build()
init {
coroutineScope.launch {
createConnectionFlow()
.retry { it is RetryException }
.collect { newState ->
Log.d(TAG, "Updating Google Play Billing connection state: $newState", true)
connectionState.update {
newState
}
}
}
}
override fun getBillingPurchaseResults(): Flow<BillingPurchaseResult> { override fun getBillingPurchaseResults(): Flow<BillingPurchaseResult> {
return internalResults return internalResults
} }
@@ -318,6 +312,7 @@ internal class BillingApiImpl(
private suspend fun <T> doOnConnectionReady(caller: String, block: suspend () -> T): T { private suspend fun <T> doOnConnectionReady(caller: String, block: suspend () -> T): T {
Log.d(TAG, "Awaiting connection from $caller... (current state: ${connectionState.value})", true) Log.d(TAG, "Awaiting connection from $caller... (current state: ${connectionState.value})", true)
startBillingClientConnectionIfNecessary()
val state = connectionState val state = connectionState
.filter { it == State.Connected || it is State.Failure } .filter { it == State.Connected || it is State.Failure }
@@ -331,37 +326,59 @@ internal class BillingApiImpl(
} }
} }
private fun createConnectionFlow(): Flow<State> { private suspend fun startBillingClientConnectionIfNecessary() {
return callbackFlow { withContext(connectionStateDispatcher) {
Log.d(TAG, "Starting Google Play Billing connection...", true) val billingConnectionState = billingClient.connectionState
send(State.Connecting) when (billingConnectionState) {
BillingClient.ConnectionState.DISCONNECTED -> {
billingClient.startConnection(object : BillingClientStateListener { Log.d(TAG, "BillingClient is disconnected. Starting connection attempt.", true)
override fun onBillingServiceDisconnected() { connectionState.update { State.Connecting }
Log.d(TAG, "Google Play Billing became disconnected.", true) billingClient.startConnection(
trySendBlocking(State.Disconnected) BillingListener(
cancel(CancellationException("Google Play Billing became disconnected.", RetryException())) onStateUpdate = { new ->
} connectionState.update { old ->
Log.d(TAG, "Moving from state $old -> $new", true)
override fun onBillingSetupFinished(billingResult: BillingResult) { new
Log.d(TAG, "onBillingSetupFinished: ${billingResult.responseCode}", true) }
if (billingResult.responseCode == BillingResponseCode.OK) { }
Log.d(TAG, "Google Play Billing is ready.", true)
trySendBlocking(State.Connected)
} else {
Log.d(TAG, "Google Play Billing failed to connect.", true)
val billingError = BillingError(
billingResponseCode = billingResult.responseCode
) )
trySendBlocking(State.Failure(billingError)) )
channel.close()
}
} }
})
awaitClose { BillingClient.ConnectionState.CONNECTING -> {
Log.d(TAG, "Ending Google Play Billing connection.", true) Log.d(TAG, "BillingClient is already connecting. Nothing to do.", true)
billingClient.endConnection() }
BillingClient.ConnectionState.CONNECTED -> {
Log.d(TAG, "BillingClient is already connected. Nothing to do.", true)
}
BillingClient.ConnectionState.CLOSED -> {
Log.w(TAG, "BillingClient was permanently closed. Cannot proceed.", true)
}
}
}
}
private class BillingListener(
private val onStateUpdate: (State) -> Unit
) : BillingClientStateListener {
override fun onBillingServiceDisconnected() {
Log.d(TAG, "BillingListener#onBillingServiceDisconnected", true)
onStateUpdate(State.Disconnected)
}
override fun onBillingSetupFinished(billingResult: BillingResult) {
Log.d(TAG, "BillingListener#onBillingSetupFinished: ${billingResult.responseCode}", true)
if (billingResult.responseCode == BillingResponseCode.OK) {
Log.d(TAG, "BillingListener#onBillingSetupFinished: ready", true)
onStateUpdate(State.Connected)
} else {
Log.d(TAG, "BillingListener#onBillingSetupFinished: failure", true)
val billingError = BillingError(
billingResponseCode = billingResult.responseCode
)
onStateUpdate(State.Failure(billingError))
} }
} }
} }