Add methods to verify and purchase subscriptions
This commit is contained in:
parent
12eee2cd40
commit
2c6ae4214d
8 changed files with 306 additions and 79 deletions
|
|
@ -12,10 +12,24 @@ import com.android.billingclient.api.BillingClient.BillingResponseCode.USER_CANC
|
||||||
import com.android.billingclient.api.BillingResult
|
import com.android.billingclient.api.BillingResult
|
||||||
import org.amnezia.vpn.util.ErrorCode
|
import org.amnezia.vpn.util.ErrorCode
|
||||||
|
|
||||||
internal class BillingException(billingResult: BillingResult) : Exception(billingResult.debugMessage) {
|
internal class BillingException(
|
||||||
|
billingResult: BillingResult,
|
||||||
|
retryable: Boolean = false
|
||||||
|
) : Exception(billingResult.toString()) {
|
||||||
|
|
||||||
|
constructor(msg: String) : this(BillingResult.newBuilder()
|
||||||
|
.setResponseCode(9999)
|
||||||
|
.setDebugMessage(msg)
|
||||||
|
.build())
|
||||||
|
|
||||||
val errorCode: Int
|
val errorCode: Int
|
||||||
val isCanceled = billingResult.responseCode == USER_CANCELED
|
val isCanceled = billingResult.responseCode == USER_CANCELED
|
||||||
|
val isRetryable = retryable || billingResult.responseCode in setOf(
|
||||||
|
NETWORK_ERROR,
|
||||||
|
SERVICE_DISCONNECTED,
|
||||||
|
SERVICE_UNAVAILABLE,
|
||||||
|
ERROR
|
||||||
|
)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
when (billingResult.responseCode) {
|
when (billingResult.responseCode) {
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,30 @@
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import com.android.billingclient.api.AcknowledgePurchaseParams
|
||||||
import com.android.billingclient.api.BillingClient
|
import com.android.billingclient.api.BillingClient
|
||||||
import com.android.billingclient.api.BillingClient.BillingResponseCode
|
import com.android.billingclient.api.BillingClient.BillingResponseCode
|
||||||
import com.android.billingclient.api.BillingClient.ProductType
|
import com.android.billingclient.api.BillingClient.ProductType
|
||||||
import com.android.billingclient.api.BillingClientStateListener
|
import com.android.billingclient.api.BillingClientStateListener
|
||||||
import com.android.billingclient.api.BillingFlowParams
|
import com.android.billingclient.api.BillingFlowParams
|
||||||
|
import com.android.billingclient.api.BillingFlowParams.SubscriptionUpdateParams.ReplacementMode
|
||||||
import com.android.billingclient.api.BillingResult
|
import com.android.billingclient.api.BillingResult
|
||||||
import com.android.billingclient.api.GetBillingConfigParams
|
import com.android.billingclient.api.GetBillingConfigParams
|
||||||
import com.android.billingclient.api.PendingPurchasesParams
|
import com.android.billingclient.api.PendingPurchasesParams
|
||||||
|
import com.android.billingclient.api.ProductDetails
|
||||||
import com.android.billingclient.api.Purchase
|
import com.android.billingclient.api.Purchase
|
||||||
import com.android.billingclient.api.PurchasesUpdatedListener
|
import com.android.billingclient.api.PurchasesUpdatedListener
|
||||||
import com.android.billingclient.api.QueryProductDetailsParams
|
import com.android.billingclient.api.QueryProductDetailsParams
|
||||||
import com.android.billingclient.api.QueryProductDetailsParams.Product
|
import com.android.billingclient.api.QueryProductDetailsParams.Product
|
||||||
|
import com.android.billingclient.api.QueryPurchasesParams
|
||||||
|
import com.android.billingclient.api.acknowledgePurchase
|
||||||
import com.android.billingclient.api.queryProductDetails
|
import com.android.billingclient.api.queryProductDetails
|
||||||
|
import com.android.billingclient.api.queryPurchasesAsync
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.amnezia.vpn.util.Log
|
import org.amnezia.vpn.util.Log
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
|
|
@ -26,6 +34,7 @@ private const val TAG = "BillingProvider"
|
||||||
private const val RESULT_OK = 1
|
private const val RESULT_OK = 1
|
||||||
private const val RESULT_CANCELED = 0
|
private const val RESULT_CANCELED = 0
|
||||||
private const val RESULT_ERROR = -1
|
private const val RESULT_ERROR = -1
|
||||||
|
private const val PRODUCT_ID = "premium"
|
||||||
|
|
||||||
class BillingProvider(context: Context) : AutoCloseable {
|
class BillingProvider(context: Context) : AutoCloseable {
|
||||||
|
|
||||||
|
|
@ -33,7 +42,7 @@ class BillingProvider(context: Context) : AutoCloseable {
|
||||||
private var subscriptionPurchases = MutableStateFlow<Pair<BillingResult, List<Purchase>?>?>(null)
|
private var subscriptionPurchases = MutableStateFlow<Pair<BillingResult, List<Purchase>?>?>(null)
|
||||||
|
|
||||||
private val purchasesUpdatedListeners = PurchasesUpdatedListener { billingResult, purchases ->
|
private val purchasesUpdatedListeners = PurchasesUpdatedListener { billingResult, purchases ->
|
||||||
Log.v(TAG, "PurchasesUpdatedListener: $billingResult")
|
Log.v(TAG, "Purchases updated: $billingResult")
|
||||||
subscriptionPurchases.value = billingResult to purchases
|
subscriptionPurchases.value = billingResult to purchases
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -47,7 +56,7 @@ class BillingProvider(context: Context) : AutoCloseable {
|
||||||
private suspend fun connect() {
|
private suspend fun connect() {
|
||||||
if (billingClient.isReady) return
|
if (billingClient.isReady) return
|
||||||
|
|
||||||
Log.v(TAG, "Connect to Google Play")
|
Log.v(TAG, "Billing client connection")
|
||||||
val connection = CompletableDeferred<Unit>()
|
val connection = CompletableDeferred<Unit>()
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
billingClient.startConnection(object : BillingClientStateListener {
|
billingClient.startConnection(object : BillingClientStateListener {
|
||||||
|
|
@ -69,29 +78,67 @@ class BillingProvider(context: Context) : AutoCloseable {
|
||||||
connection.await()
|
connection.await()
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun handleBillingApiCall(block: suspend () -> JSONObject): JSONObject =
|
private suspend fun handleBillingApiCall(block: suspend () -> JSONObject): JSONObject {
|
||||||
try {
|
val numberAttempts = 3
|
||||||
block()
|
var attemptCount = 0
|
||||||
} catch (e: BillingException) {
|
while (true) {
|
||||||
if (e.isCanceled) {
|
try {
|
||||||
Log.w(TAG, "Billing canceled")
|
return block()
|
||||||
JSONObject().put("result", RESULT_CANCELED)
|
} catch (e: BillingException) {
|
||||||
} else {
|
if (e.isCanceled) {
|
||||||
Log.e(TAG, "Billing error: $e")
|
Log.w(TAG, "Billing canceled")
|
||||||
JSONObject()
|
return JSONObject().put("result", RESULT_CANCELED)
|
||||||
.put("result", RESULT_ERROR)
|
} else if (e.isRetryable && attemptCount < numberAttempts) {
|
||||||
.put("errorCode", e.errorCode)
|
Log.d(TAG, "Retryable error: $e")
|
||||||
|
++attemptCount
|
||||||
|
delay(1000)
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Billing error: $e")
|
||||||
|
return JSONObject()
|
||||||
|
.put("result", RESULT_ERROR)
|
||||||
|
.put("errorCode", e.errorCode)
|
||||||
|
}
|
||||||
|
} catch (_: CancellationException) {
|
||||||
|
Log.w(TAG, "Billing coroutine canceled")
|
||||||
|
return JSONObject().put("result", RESULT_CANCELED)
|
||||||
}
|
}
|
||||||
} catch (_: CancellationException) {
|
|
||||||
Log.w(TAG, "Billing coroutine canceled")
|
|
||||||
JSONObject().put("result", RESULT_CANCELED)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun getSubscriptionPlans(): JSONObject = handleBillingApiCall {
|
suspend fun getSubscriptionPlans(): JSONObject {
|
||||||
Log.v(TAG, "Get subscription plans")
|
Log.v(TAG, "Get subscription plans")
|
||||||
|
|
||||||
|
val productDetailsList = getProductDetails()
|
||||||
|
val resultJson = JSONObject().put("result", RESULT_OK)
|
||||||
|
val productArray = JSONArray().also { resultJson.put("products", it) }
|
||||||
|
productDetailsList?.forEach { productDetails ->
|
||||||
|
val product = JSONObject().also { productArray.put(it) }
|
||||||
|
.put("productId", productDetails.productId)
|
||||||
|
.put("name", productDetails.name)
|
||||||
|
val offers = JSONArray().also { product.put("offers", it) }
|
||||||
|
productDetails.subscriptionOfferDetails?.forEach { offerDetails ->
|
||||||
|
val offer = JSONObject().also { offers.put(it) }
|
||||||
|
.put("basePlanId", offerDetails.basePlanId)
|
||||||
|
.put("offerId", offerDetails.offerId)
|
||||||
|
.put("offerToken", offerDetails.offerToken)
|
||||||
|
val pricingPhases = JSONArray().also { offer.put("pricingPhases", it) }
|
||||||
|
offerDetails.pricingPhases.pricingPhaseList.forEach { phase ->
|
||||||
|
JSONObject().also { pricingPhases.put(it) }
|
||||||
|
.put("billingCycleCount", phase.billingCycleCount)
|
||||||
|
.put("billingPeriod", phase.billingPeriod)
|
||||||
|
.put("formatedPrice", phase.formattedPrice)
|
||||||
|
.put("recurrenceMode", phase.recurrenceMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resultJson
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getProductDetails(): List<ProductDetails>? {
|
||||||
|
Log.v(TAG, "Get product details")
|
||||||
|
|
||||||
val productDetailsParams = Product.newBuilder()
|
val productDetailsParams = Product.newBuilder()
|
||||||
.setProductId("premium")
|
.setProductId(PRODUCT_ID)
|
||||||
.setProductType(ProductType.SUBS)
|
.setProductType(ProductType.SUBS)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
|
@ -103,40 +150,17 @@ class BillingProvider(context: Context) : AutoCloseable {
|
||||||
billingClient.queryProductDetails(queryProductDetailsParams)
|
billingClient.queryProductDetails(queryProductDetailsParams)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Log.v(TAG, "Query product details result: ${result.billingResult}")
|
||||||
|
|
||||||
if (!result.billingResult.isOk) {
|
if (!result.billingResult.isOk) {
|
||||||
Log.e(TAG, "Failed to get subscription plans: ${result.billingResult}")
|
Log.e(TAG, "Failed to get product details: ${result.billingResult}")
|
||||||
throw BillingException(result.billingResult)
|
throw BillingException(result.billingResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.v(TAG, "Subscription plans:\n${result.productDetailsList}")
|
return result.productDetailsList
|
||||||
|
|
||||||
val resultJson = JSONObject().put("result", RESULT_OK)
|
|
||||||
|
|
||||||
val productArray = JSONArray().also { resultJson.put("products", it) }
|
|
||||||
result.productDetailsList?.forEach {
|
|
||||||
val product = JSONObject().also { productArray.put(it) }
|
|
||||||
product.put("productId", it.productId)
|
|
||||||
product.put("name", it.name)
|
|
||||||
val offers = JSONArray().also { product.put("offers", it) }
|
|
||||||
it.subscriptionOfferDetails?.forEach {
|
|
||||||
val offer = JSONObject().also { offers.put(it) }
|
|
||||||
offer.put("basePlanId", it.basePlanId)
|
|
||||||
offer.put("offerId", it.offerId)
|
|
||||||
offer.put("offerToken", it.offerToken)
|
|
||||||
val pricingPhases = JSONArray().also { offer.put("pricingPhases", it) }
|
|
||||||
it.pricingPhases.pricingPhaseList.forEach {
|
|
||||||
val pricingPhase = JSONObject().also { pricingPhases.put(it) }
|
|
||||||
pricingPhase.put("billingCycleCount", it.billingCycleCount)
|
|
||||||
pricingPhase.put("billingPeriod", it.billingPeriod)
|
|
||||||
pricingPhase.put("formatedPrice", it.formattedPrice)
|
|
||||||
pricingPhase.put("recurrenceMode", it.recurrenceMode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resultJson
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getCustomerCountryCode(): JSONObject = handleBillingApiCall {
|
suspend fun getCustomerCountryCode(): JSONObject {
|
||||||
Log.v(TAG, "Get customer country code")
|
Log.v(TAG, "Get customer country code")
|
||||||
|
|
||||||
val deferred = CompletableDeferred<String>()
|
val deferred = CompletableDeferred<String>()
|
||||||
|
|
@ -153,22 +177,135 @@ class BillingProvider(context: Context) : AutoCloseable {
|
||||||
}
|
}
|
||||||
val countryCode = deferred.await()
|
val countryCode = deferred.await()
|
||||||
|
|
||||||
JSONObject()
|
return JSONObject()
|
||||||
.put("result", RESULT_OK)
|
.put("result", RESULT_OK)
|
||||||
.put("countryCode", countryCode)
|
.put("countryCode", countryCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun purchaseSubscription(activity: Activity, obfuscatedAccountId: String): JSONObject =
|
suspend fun purchaseSubscription(
|
||||||
handleBillingApiCall {
|
activity: Activity,
|
||||||
Log.v(TAG, "Purchase subscription")
|
offerToken: String,
|
||||||
billingClient.launchBillingFlow(activity, BillingFlowParams.newBuilder()
|
oldPurchaseToken: String? = null
|
||||||
.setObfuscatedAccountId(obfuscatedAccountId)
|
): JSONObject {
|
||||||
.build())
|
Log.v(TAG, "Purchase subscription")
|
||||||
JSONObject()
|
Log.v(TAG, "Offer token: $offerToken")
|
||||||
|
oldPurchaseToken?.let { Log.v(TAG, "Old purchase token: $it") }
|
||||||
|
|
||||||
|
if (offerToken.isBlank()) throw BillingException("offerToken can not be empty")
|
||||||
|
|
||||||
|
val productDetails = getProductDetails()?.let {
|
||||||
|
it.filter { it.productId == PRODUCT_ID }
|
||||||
|
}?.firstOrNull() ?: throw BillingException("Product details not found")
|
||||||
|
|
||||||
|
Log.v(TAG, "Filtered product details:\n$productDetails")
|
||||||
|
|
||||||
|
val productDetail = BillingFlowParams.ProductDetailsParams.newBuilder()
|
||||||
|
.setProductDetails(productDetails)
|
||||||
|
.setOfferToken(offerToken)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val subscriptionUpdateParams = oldPurchaseToken?.let {
|
||||||
|
BillingFlowParams.SubscriptionUpdateParams.newBuilder()
|
||||||
|
.setOldPurchaseToken(oldPurchaseToken)
|
||||||
|
.setSubscriptionReplacementMode(ReplacementMode.WITHOUT_PRORATION)
|
||||||
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val billingResult = billingClient.launchBillingFlow(activity, BillingFlowParams.newBuilder()
|
||||||
|
.setProductDetailsParamsList(listOf(productDetail))
|
||||||
|
.apply { subscriptionUpdateParams?.let { setSubscriptionUpdateParams(it) } }
|
||||||
|
.build())
|
||||||
|
|
||||||
|
Log.v(TAG, "Start billing flow result: $billingResult")
|
||||||
|
|
||||||
|
if (billingResult.responseCode == BillingResponseCode.ITEM_ALREADY_OWNED) {
|
||||||
|
Log.w(TAG, "Attempting to purchase already owned product")
|
||||||
|
val purchases = queryPurchases()
|
||||||
|
if (purchases.any { PRODUCT_ID in it.products }) throw BillingException(billingResult)
|
||||||
|
else throw BillingException(billingResult, retryable = true)
|
||||||
|
} else if (billingResult.responseCode == BillingResponseCode.ITEM_NOT_OWNED) {
|
||||||
|
Log.w(TAG, "Attempting to replace not owned product")
|
||||||
|
val purchases = queryPurchases()
|
||||||
|
if (purchases.all { PRODUCT_ID !in it.products }) throw BillingException(billingResult)
|
||||||
|
else throw BillingException(billingResult, retryable = true)
|
||||||
|
} else if (!billingResult.isOk) throw BillingException(billingResult)
|
||||||
|
|
||||||
|
subscriptionPurchases.firstOrNull { it != null }?.let { (billingResult, purchases) ->
|
||||||
|
if (!billingResult.isOk) throw BillingException(billingResult)
|
||||||
|
return JSONObject()
|
||||||
|
.put("result", RESULT_OK)
|
||||||
|
.put("purchases", processPurchases(purchases))
|
||||||
|
} ?: throw BillingException("Purchase failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processPurchases(purchases: List<Purchase>?): JSONArray {
|
||||||
|
val purchaseArray = JSONArray()
|
||||||
|
purchases?.forEach { purchase ->
|
||||||
|
val purchaseJson = JSONObject().also { purchaseArray.put(it) }
|
||||||
|
.put("purchaseToken", purchase.purchaseToken)
|
||||||
|
.put("purchaseTime", purchase.purchaseTime)
|
||||||
|
.put("purchaseState", purchase.purchaseState)
|
||||||
|
.put("isAcknowledged", purchase.isAcknowledged)
|
||||||
|
.put("isAutoRenewing", purchase.isAutoRenewing)
|
||||||
|
.put("orderId", purchase.orderId)
|
||||||
|
.put("productIds", JSONArray(purchase.products))
|
||||||
|
|
||||||
|
purchase.pendingPurchaseUpdate?.let { purchaseUpdate ->
|
||||||
|
JSONObject()
|
||||||
|
.put("purchaseToken", purchaseUpdate.purchaseToken)
|
||||||
|
.put("productIds", JSONArray(purchaseUpdate.products))
|
||||||
|
}.also { purchaseJson.put("pendingPurchaseUpdate", it) }
|
||||||
|
}
|
||||||
|
return purchaseArray
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun acknowledge(purchaseToken: String): JSONObject {
|
||||||
|
Log.v(TAG, "Acknowledge purchase: $purchaseToken")
|
||||||
|
|
||||||
|
val result = withContext(Dispatchers.IO) {
|
||||||
|
billingClient.acknowledgePurchase(
|
||||||
|
AcknowledgePurchaseParams.newBuilder()
|
||||||
|
.setPurchaseToken(purchaseToken)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.v(TAG, "Acknowledge purchase result: $result")
|
||||||
|
|
||||||
|
if (result.responseCode == BillingResponseCode.ITEM_NOT_OWNED) {
|
||||||
|
Log.w(TAG, "Attempting to acknowledge not owned product")
|
||||||
|
val purchases = queryPurchases()
|
||||||
|
if (purchases.all { PRODUCT_ID !in it.products }) throw BillingException(result)
|
||||||
|
else throw BillingException(result, retryable = true)
|
||||||
|
} else if (!result.isOk && result.responseCode != BillingResponseCode.ITEM_ALREADY_OWNED) {
|
||||||
|
throw BillingException(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSONObject().put("result", RESULT_OK)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getPurchases(): JSONObject {
|
||||||
|
Log.v(TAG, "Get purchases")
|
||||||
|
val purchases = queryPurchases()
|
||||||
|
return JSONObject()
|
||||||
|
.put("result", RESULT_OK)
|
||||||
|
.put("purchases", processPurchases(purchases))
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun queryPurchases(): List<Purchase> {
|
||||||
|
Log.v(TAG, "Query purchases")
|
||||||
|
val result = withContext(Dispatchers.IO) {
|
||||||
|
billingClient.queryPurchasesAsync(
|
||||||
|
QueryPurchasesParams.newBuilder().setProductType(ProductType.SUBS).build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Log.v(TAG, "Query purchases result: ${result.billingResult}")
|
||||||
|
if (!result.billingResult.isOk) throw BillingException(result.billingResult)
|
||||||
|
return result.purchasesList
|
||||||
|
}
|
||||||
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
Log.d(TAG, "Close billing client connection")
|
Log.v(TAG, "Close billing client connection")
|
||||||
billingClient.endConnection()
|
billingClient.endConnection()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,5 +6,8 @@ import android.content.Context
|
||||||
class BillingPaymentRepository(@Suppress("UNUSED_PARAMETER") context: Context) : BillingRepository {
|
class BillingPaymentRepository(@Suppress("UNUSED_PARAMETER") context: Context) : BillingRepository {
|
||||||
override suspend fun getCountryCode(): String = ""
|
override suspend fun getCountryCode(): String = ""
|
||||||
override suspend fun getSubscriptionPlans(): String = ""
|
override suspend fun getSubscriptionPlans(): String = ""
|
||||||
override suspend fun purchaseSubscription(activity: Activity): String = ""
|
override suspend fun purchaseSubscription(activity: Activity, offerToken: String): String = ""
|
||||||
|
override suspend fun upgradeSubscription(activity: Activity, offerToken: String, oldPurchaseToken: String): String = ""
|
||||||
|
override suspend fun acknowledge(purchaseToken: String): String = ""
|
||||||
|
override suspend fun queryPurchases(): String = ""
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,21 @@ class BillingPaymentRepository(private val context: Context) : BillingRepository
|
||||||
getSubscriptionPlans()
|
getSubscriptionPlans()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun purchaseSubscription(activity: Activity): String = withBillingProvider(context) {
|
override suspend fun purchaseSubscription(activity: Activity, offerToken: String): String =
|
||||||
purchaseSubscription(activity, "obfuscatedAccountId")
|
withBillingProvider(context) {
|
||||||
|
purchaseSubscription(activity, offerToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun upgradeSubscription(activity: Activity, offerToken: String, oldPurchaseToken: String): String =
|
||||||
|
withBillingProvider(context) {
|
||||||
|
purchaseSubscription(activity, offerToken, oldPurchaseToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun acknowledge(purchaseToken: String): String = withBillingProvider(context) {
|
||||||
|
acknowledge(purchaseToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun queryPurchases(): String = withBillingProvider(context) {
|
||||||
|
getPurchases()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ import androidx.annotation.RequiresApi
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import kotlin.LazyThreadSafetyMode.NONE
|
import kotlin.LazyThreadSafetyMode.NONE
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
import kotlin.text.RegexOption.IGNORE_CASE
|
import kotlin.text.RegexOption.IGNORE_CASE
|
||||||
import AppListProvider
|
import AppListProvider
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
|
@ -39,7 +40,6 @@ import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.amnezia.vpn.protocol.getStatistics
|
import org.amnezia.vpn.protocol.getStatistics
|
||||||
import org.amnezia.vpn.protocol.getStatus
|
import org.amnezia.vpn.protocol.getStatus
|
||||||
import org.amnezia.vpn.qt.QtAndroidController
|
import org.amnezia.vpn.qt.QtAndroidController
|
||||||
|
|
@ -648,15 +648,9 @@ class AmneziaActivity : QtActivity() {
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
fun getAppList(): String {
|
fun getAppList(): String {
|
||||||
Log.v(TAG, "Get app list")
|
Log.v(TAG, "Get app list")
|
||||||
var appList = ""
|
return blockingCall(Dispatchers.IO) {
|
||||||
runBlocking {
|
AppListProvider.getAppList(packageManager, packageName)
|
||||||
mainScope.launch {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
appList = AppListProvider.getAppList(packageManager, packageName)
|
|
||||||
}
|
|
||||||
}.join()
|
|
||||||
}
|
}
|
||||||
return appList
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
|
|
@ -728,26 +722,51 @@ class AmneziaActivity : QtActivity() {
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
fun getCountryCode(): String {
|
fun getCountryCode(): String {
|
||||||
Log.v(TAG, "Get country code")
|
Log.v(TAG, "Get country code")
|
||||||
return runBlocking {
|
return blockingCall { billingRepository.getCountryCode() }
|
||||||
mainScope.async {
|
|
||||||
billingRepository.getCountryCode()
|
|
||||||
}.await()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
fun getSubscriptionPlans(): String {
|
fun getSubscriptionPlans(): String {
|
||||||
Log.v(TAG, "Get subscription plans")
|
Log.v(TAG, "Get subscription plans")
|
||||||
return runBlocking {
|
return blockingCall { billingRepository.getSubscriptionPlans() }
|
||||||
mainScope.async {
|
}
|
||||||
billingRepository.getSubscriptionPlans()
|
|
||||||
}.await()
|
@Suppress("unused")
|
||||||
|
fun purchaseSubscription(offerToken: String): String {
|
||||||
|
Log.v(TAG, "Purchase subscription")
|
||||||
|
return blockingCall { billingRepository.purchaseSubscription(this@AmneziaActivity, offerToken) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
fun upgradeSubscription(offerToken: String, oldPurchaseToken: String): String {
|
||||||
|
Log.v(TAG, "Upgrade subscription")
|
||||||
|
return blockingCall {
|
||||||
|
billingRepository.upgradeSubscription(this@AmneziaActivity, offerToken, oldPurchaseToken)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
fun acknowledgePurchase(purchaseToken: String): String {
|
||||||
|
Log.v(TAG, "Acknowledge purchase")
|
||||||
|
return blockingCall { billingRepository.acknowledge(purchaseToken) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
fun queryPurchases(): String {
|
||||||
|
Log.v(TAG, "Query purchases")
|
||||||
|
return blockingCall { billingRepository.queryPurchases() }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utils methods
|
* Utils methods
|
||||||
*/
|
*/
|
||||||
|
private fun <T> blockingCall(
|
||||||
|
context: CoroutineContext = Dispatchers.Default,
|
||||||
|
block: suspend () -> T
|
||||||
|
) = runBlocking {
|
||||||
|
mainScope.async(context) { block() }.await()
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private fun actionCodeToString(actionCode: Int): String =
|
private fun actionCodeToString(actionCode: Int): String =
|
||||||
when (actionCode) {
|
when (actionCode) {
|
||||||
|
|
|
||||||
|
|
@ -5,5 +5,8 @@ import android.app.Activity
|
||||||
interface BillingRepository {
|
interface BillingRepository {
|
||||||
suspend fun getCountryCode(): String
|
suspend fun getCountryCode(): String
|
||||||
suspend fun getSubscriptionPlans(): String
|
suspend fun getSubscriptionPlans(): String
|
||||||
suspend fun purchaseSubscription(activity: Activity): String
|
suspend fun purchaseSubscription(activity: Activity, offerToken: String): String
|
||||||
|
suspend fun upgradeSubscription(activity: Activity, offerToken: String, oldPurchaseToken: String): String
|
||||||
|
suspend fun acknowledge(purchaseToken: String): String
|
||||||
|
suspend fun queryPurchases(): String
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -299,6 +299,39 @@ QJsonObject AndroidController::getSubscriptionPlans()
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QJsonObject AndroidController::purchaseSubscription(const QString &offerToken)
|
||||||
|
{
|
||||||
|
QJniObject result = callActivityMethod<jstring, jstring>("purchaseSubscription", "(Ljava/lang/String;)Ljava/lang/String;",
|
||||||
|
QJniObject::fromString(offerToken).object<jstring>());
|
||||||
|
QJsonObject json = QJsonDocument::fromJson(result.toString().toUtf8()).object();
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject AndroidController::upgradeSubscription(const QString &offerToken, const QString &oldPurchaseToken)
|
||||||
|
{
|
||||||
|
QJniObject result = callActivityMethod<jstring, jstring, jstring>("upgradeSubscription",
|
||||||
|
"(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;",
|
||||||
|
QJniObject::fromString(offerToken).object<jstring>(),
|
||||||
|
QJniObject::fromString(oldPurchaseToken).object<jstring>());
|
||||||
|
QJsonObject json = QJsonDocument::fromJson(result.toString().toUtf8()).object();
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject AndroidController::acknowledgePurchase(const QString &purchaseToken)
|
||||||
|
{
|
||||||
|
QJniObject result = callActivityMethod<jstring, jstring>("acknowledgePurchase", "(Ljava/lang/String;)Ljava/lang/String;",
|
||||||
|
QJniObject::fromString(purchaseToken).object<jstring>());
|
||||||
|
QJsonObject json = QJsonDocument::fromJson(result.toString().toUtf8()).object();
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject AndroidController::queryPurchases()
|
||||||
|
{
|
||||||
|
QJniObject result = callActivityMethod<jstring>("queryPurchases", "()Ljava/lang/String;");
|
||||||
|
QJsonObject json = QJsonDocument::fromJson(result.toString().toUtf8()).object();
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
// Moving log processing to the Android side
|
// Moving log processing to the Android side
|
||||||
jclass AndroidController::log;
|
jclass AndroidController::log;
|
||||||
jmethodID AndroidController::logDebug;
|
jmethodID AndroidController::logDebug;
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,10 @@ public:
|
||||||
bool requestAuthentication();
|
bool requestAuthentication();
|
||||||
bool isPlay();
|
bool isPlay();
|
||||||
QJsonObject getSubscriptionPlans();
|
QJsonObject getSubscriptionPlans();
|
||||||
|
QJsonObject purchaseSubscription(const QString &offerToken);
|
||||||
|
QJsonObject upgradeSubscription(const QString &offerToken, const QString &oldPurchaseToken);
|
||||||
|
QJsonObject acknowledgePurchase(const QString &purchaseToken);
|
||||||
|
QJsonObject queryPurchases();
|
||||||
|
|
||||||
static bool initLogging();
|
static bool initLogging();
|
||||||
static void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message);
|
static void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue