From 550e3b690f0ecf1b1e08a1ef3dcda17be8d659b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 11 Feb 2024 20:33:12 +0800 Subject: [PATCH] Fix sponsor not acknowledged --- .../io/nekohasekai/sfa/ui/MainActivity.kt | 2 + .../nekohasekai/sfa/vendor/VendorInterface.kt | 1 + .../java/io/nekohasekai/sfa/vendor/Vendor.kt | 200 +++++++++++------- 3 files changed, 131 insertions(+), 72 deletions(-) diff --git a/app/src/main/java/io/nekohasekai/sfa/ui/MainActivity.kt b/app/src/main/java/io/nekohasekai/sfa/ui/MainActivity.kt index 9dc612b..5a27f92 100644 --- a/app/src/main/java/io/nekohasekai/sfa/ui/MainActivity.kt +++ b/app/src/main/java/io/nekohasekai/sfa/ui/MainActivity.kt @@ -80,6 +80,8 @@ class MainActivity : AbstractActivity(), ServiceConnection.Callback { startIntegration() onNewIntent(intent) + + Vendor.initializeBillingClient(this) } override fun onNewIntent(intent: Intent) { diff --git a/app/src/main/java/io/nekohasekai/sfa/vendor/VendorInterface.kt b/app/src/main/java/io/nekohasekai/sfa/vendor/VendorInterface.kt index b7c5189..466013c 100644 --- a/app/src/main/java/io/nekohasekai/sfa/vendor/VendorInterface.kt +++ b/app/src/main/java/io/nekohasekai/sfa/vendor/VendorInterface.kt @@ -5,5 +5,6 @@ import android.app.Activity interface VendorInterface { fun checkUpdateAvailable(): Boolean fun checkUpdate(activity: Activity, byUser: Boolean) + fun initializeBillingClient(activity: Activity) fun startSponsor(activity: Activity, fallback: () -> Unit) } \ No newline at end of file diff --git a/app/src/play/java/io/nekohasekai/sfa/vendor/Vendor.kt b/app/src/play/java/io/nekohasekai/sfa/vendor/Vendor.kt index e462a3c..2b8d620 100644 --- a/app/src/play/java/io/nekohasekai/sfa/vendor/Vendor.kt +++ b/app/src/play/java/io/nekohasekai/sfa/vendor/Vendor.kt @@ -4,13 +4,17 @@ import android.app.Activity import android.app.ProgressDialog import android.content.Context import android.util.Log +import com.android.billingclient.api.AcknowledgePurchaseParams import com.android.billingclient.api.BillingClient import com.android.billingclient.api.BillingClientStateListener import com.android.billingclient.api.BillingFlowParams import com.android.billingclient.api.BillingResult import com.android.billingclient.api.ProductDetails +import com.android.billingclient.api.Purchase import com.android.billingclient.api.QueryProductDetailsParams +import com.android.billingclient.api.QueryPurchasesParams import com.android.billingclient.api.queryProductDetails +import com.android.billingclient.api.queryPurchasesAsync import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.play.core.appupdate.AppUpdateManagerFactory import com.google.android.play.core.appupdate.AppUpdateOptions @@ -20,11 +24,17 @@ import com.google.android.play.core.install.model.UpdateAvailability import io.nekohasekai.sfa.Application import io.nekohasekai.sfa.R import io.nekohasekai.sfa.ktx.errorDialogBuilder +import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.util.concurrent.atomic.AtomicInteger +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine object Vendor : VendorInterface { @@ -85,105 +95,103 @@ object Vendor : VendorInterface { } private lateinit var billingClient: BillingClient - override fun startSponsor(activity: Activity, fallback: () -> Unit) { - if (!::billingClient.isInitialized) { - billingClient = BillingClient.newBuilder(Application.application) - .setListener { _, _ -> - } - .enablePendingPurchases() - .build() + override fun initializeBillingClient(activity: Activity) { + billingClient = + BillingClient.newBuilder(Application.application).setListener { result, purchases -> + onPurchasesUpdated(activity, result, purchases) + }.enablePendingPurchases().build() + } + + private fun requireConnection(callback: (String?) -> Unit) { + when (billingClient.connectionState) { + BillingClient.ConnectionState.CONNECTED -> callback(null) + else -> { + billingClient.startConnection(object : BillingClientStateListener { + override fun onBillingSetupFinished(result: BillingResult) { + if (result.responseCode == BillingClient.BillingResponseCode.OK) { + callback(null) + } else { + callback(result.toString()) + } + } + + override fun onBillingServiceDisconnected() { + } + }) + } } + } + + override fun startSponsor(activity: Activity, fallback: () -> Unit) { val dialog = ProgressDialog(activity) dialog.setMessage(activity.getString(R.string.loading)) dialog.show() - billingClient.startConnection(object : BillingClientStateListener { - override fun onBillingSetupFinished(result: BillingResult) { - dialog.dismiss() - if (result.responseCode == BillingClient.BillingResponseCode.OK) { - GlobalScope.launch(Dispatchers.IO) { - runCatching { - startSponsor0(activity, fallback) - }.onFailure { exception -> - withContext(Dispatchers.Main) { - activity.errorDialogBuilder(exception).show() - } - } - } - } else { - GlobalScope.launch(Dispatchers.Main) { - activity.errorDialogBuilder(result.toString()).show() - } - } + requireConnection { + if (it != null) { + activity.errorDialogBuilder(it).show() + return@requireConnection } - - override fun onBillingServiceDisconnected() { + GlobalScope.launch(Dispatchers.IO) { + acknowledgeSponsor() + startSponsor0(activity, dialog, fallback) } - }) + } } - private suspend fun startSponsor0(activity: Activity, fallback: () -> Unit) { - val params = QueryProductDetailsParams.newBuilder() - .setProductList( - listOf( - QueryProductDetailsParams.Product.newBuilder() - .setProductId("sponsor_circle_1") - .setProductType(BillingClient.ProductType.SUBS) - .build(), - QueryProductDetailsParams.Product.newBuilder() - .setProductId("sponsor_circle_10") - .setProductType(BillingClient.ProductType.SUBS) - .build(), - QueryProductDetailsParams.Product.newBuilder() - .setProductId("sponsor_circle_100") - .setProductType(BillingClient.ProductType.SUBS) - .build(), - ) - ).build() + private suspend fun startSponsor0( + activity: Activity, + dialog: ProgressDialog, + fallback: () -> Unit, + ) { + val params = QueryProductDetailsParams.newBuilder().setProductList( + listOf( + QueryProductDetailsParams.Product.newBuilder().setProductId("sponsor_circle_1") + .setProductType(BillingClient.ProductType.SUBS).build(), + QueryProductDetailsParams.Product.newBuilder().setProductId("sponsor_circle_10") + .setProductType(BillingClient.ProductType.SUBS).build(), + QueryProductDetailsParams.Product.newBuilder() + .setProductId("sponsor_circle_100") + .setProductType(BillingClient.ProductType.SUBS).build(), + ) + ).build() val (result, products) = billingClient.queryProductDetails(params) - if (result.responseCode != BillingClient.BillingResponseCode.OK) { - error(result.toString()) + withContext(Dispatchers.Main) { + dialog.dismiss() } - if (products.isNullOrEmpty()) { + if (result.responseCode != BillingClient.BillingResponseCode.OK || products.isNullOrEmpty()) { withContext(Dispatchers.Main) { - fallback() + activity.errorDialogBuilder(result.toString()).show() } return } val selecting = products.sortedBy { it.productId.substringAfterLast("_").toInt() } val selected = AtomicInteger(0) withContext(Dispatchers.Main) { - MaterialAlertDialogBuilder(activity) - .setTitle(R.string.sponsor_play) - .setSingleChoiceItems( - selecting.map { it.title.removeSuffix(" (sing-box)") }.toMutableList().also { + MaterialAlertDialogBuilder(activity).setTitle(R.string.sponsor_play) + .setSingleChoiceItems(selecting.map { it.title.removeSuffix(" (sing-box)") } + .toMutableList().also { it.add(activity.getString(R.string.other_methods)) - }.toTypedArray(), - 0 + }.toTypedArray(), 0 ) { _, which -> selected.set(which) - } - .setNeutralButton(android.R.string.cancel, null) + }.setNeutralButton(android.R.string.cancel, null) .setPositiveButton(R.string.action_start) { _, _ -> if (selected.get() == selecting.size) { fallback() return@setPositiveButton } startSponsor1(activity, selecting[selected.get()]) - } - .show() + }.show() } } private fun startSponsor1(activity: Activity, product: ProductDetails) { val paramsList = listOf( - BillingFlowParams.ProductDetailsParams.newBuilder() - .setProductDetails(product) - .setOfferToken(product.subscriptionOfferDetails!![0].offerToken) - .build() + BillingFlowParams.ProductDetailsParams.newBuilder().setProductDetails(product) + .setOfferToken(product.subscriptionOfferDetails!![0].offerToken).build() ) - val flowParams = BillingFlowParams.newBuilder() - .setProductDetailsParamsList(paramsList) - .build() + val flowParams = + BillingFlowParams.newBuilder().setProductDetailsParamsList(paramsList).build() val result = billingClient.launchBillingFlow(activity, flowParams) if (result.responseCode != BillingClient.BillingResponseCode.OK) { activity.errorDialogBuilder(result.toString()).show() @@ -191,11 +199,59 @@ object Vendor : VendorInterface { } private fun Context.showNoUpdatesDialog() { - MaterialAlertDialogBuilder(this) - .setTitle(io.nekohasekai.sfa.R.string.check_update) - .setMessage(R.string.no_updates_available) - .setPositiveButton(R.string.ok, null) - .show() + MaterialAlertDialogBuilder(this).setTitle(io.nekohasekai.sfa.R.string.check_update) + .setMessage(R.string.no_updates_available).setPositiveButton(R.string.ok, null).show() + } + + private fun onPurchasesUpdated( + activity: Activity, result: BillingResult, purchases: List? + ) { + if (result.responseCode != BillingClient.BillingResponseCode.OK || purchases.isNullOrEmpty()) { + return + } + requireConnection { + if (it != null) GlobalScope.launch(Dispatchers.Main) { + val dialog = ProgressDialog(activity) + dialog.setMessage(activity.getString(R.string.loading)) + dialog.show() + val errorMessage = acknowledgeSponsor0(purchases) + dialog.dismiss() + if (errorMessage != null) { + activity.errorDialogBuilder(errorMessage).show() + } + } + } + } + + private suspend fun acknowledgeSponsor() { + val result = billingClient.queryPurchasesAsync( + QueryPurchasesParams.newBuilder().setProductType(BillingClient.ProductType.SUBS) + .build() + ) + if (result.purchasesList.isNotEmpty()) { + acknowledgeSponsor0(result.purchasesList) + } + } + + private suspend fun acknowledgeSponsor0(purchases: List): String? = coroutineScope { + val deferred = mutableListOf>() + for (purchase in purchases) { + deferred += async(Dispatchers.IO) { + suspendCoroutine { continuation -> + billingClient.acknowledgePurchase( + AcknowledgePurchaseParams.newBuilder() + .setPurchaseToken(purchase.purchaseToken).build() + ) { + if (it.responseCode == BillingClient.BillingResponseCode.OK) { + continuation.resume(null) + } else { + continuation.resume(it.toString()) + } + } + } + } + } + deferred.awaitAll().filterNotNull().firstOrNull() } } \ No newline at end of file