From 244a81ef245402cef05003e739de0fc3ed16fc1b Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Thu, 22 Aug 2024 15:12:24 -0300 Subject: [PATCH] Move billing code to shared module. --- app/build.gradle.kts | 1 + .../securesms/dependencies/AppDependencies.kt | 6 +- .../ApplicationDependencyProvider.java | 8 +- .../billing/GooglePlayBillingFactory.kt | 13 -- .../billing/GooglePlayBillingFactory.kt | 73 -------- .../MockApplicationDependencyProvider.kt | 4 +- .../java/org/signal/billing/BillingFactory.kt | 14 ++ .../billing/GooglePlayBillingFactory.kt | 13 -- billing/build.gradle.kts | 2 +- .../{BillingApi.kt => BillingApiImpl.kt} | 156 ++++++++++-------- .../java/org/signal/billing/BillingFactory.kt | 23 +++ .../signal/core/util/billing/BillingApi.kt | 6 +- 12 files changed, 136 insertions(+), 183 deletions(-) delete mode 100644 app/src/nightly/java/org/thoughtcrime/securesms/billing/GooglePlayBillingFactory.kt delete mode 100644 app/src/play/java/org/thoughtcrime/securesms/billing/GooglePlayBillingFactory.kt create mode 100644 app/src/website/java/org/signal/billing/BillingFactory.kt delete mode 100644 app/src/website/java/org/thoughtcrime/securesms/billing/GooglePlayBillingFactory.kt rename billing/src/main/java/org/signal/billing/{BillingApi.kt => BillingApiImpl.kt} (83%) create mode 100644 billing/src/main/java/org/signal/billing/BillingFactory.kt rename app/src/main/java/org/thoughtcrime/securesms/billing/GooglePlayBillingApi.kt => core-util/src/main/java/org/signal/core/util/billing/BillingApi.kt (84%) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2873283dda..0607e80b8e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -559,6 +559,7 @@ dependencies { implementation(libs.rxdogtag) "playImplementation"(project(":billing")) + "nightlyImplementation"(project(":billing")) "spinnerImplementation"(project(":spinner")) diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/AppDependencies.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/AppDependencies.kt index 2f1bcba50e..dff0e61889 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/AppDependencies.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/AppDependencies.kt @@ -7,12 +7,12 @@ import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.subjects.BehaviorSubject import io.reactivex.rxjava3.subjects.Subject import okhttp3.OkHttpClient +import org.signal.core.util.billing.BillingApi import org.signal.core.util.concurrent.DeadlockDetector import org.signal.core.util.resettableLazy import org.signal.libsignal.net.Network import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations import org.signal.libsignal.zkgroup.receipts.ClientZkReceiptOperations -import org.thoughtcrime.securesms.billing.GooglePlayBillingApi import org.thoughtcrime.securesms.components.TypingStatusRepository import org.thoughtcrime.securesms.components.TypingStatusSender import org.thoughtcrime.securesms.crypto.storage.SignalServiceDataStoreImpl @@ -212,7 +212,7 @@ object AppDependencies { } @JvmStatic - val billingApi: GooglePlayBillingApi by lazy { + val billingApi: BillingApi by lazy { provider.provideBillingApi() } @@ -348,6 +348,6 @@ object AppDependencies { fun provideClientZkReceiptOperations(signalServiceConfiguration: SignalServiceConfiguration): ClientZkReceiptOperations fun provideScheduledMessageManager(): ScheduledMessageManager fun provideLibsignalNetwork(config: SignalServiceConfiguration): Network - fun provideBillingApi(): GooglePlayBillingApi + fun provideBillingApi(): BillingApi } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java index 2f5dc7640e..56a19674e0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java @@ -8,15 +8,15 @@ import android.os.HandlerThread; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; +import org.signal.billing.BillingFactory; import org.signal.core.util.ThreadUtil; +import org.signal.core.util.billing.BillingApi; import org.signal.core.util.concurrent.DeadlockDetector; import org.signal.core.util.concurrent.SignalExecutors; import org.signal.libsignal.net.Network; import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations; import org.signal.libsignal.zkgroup.receipts.ClientZkReceiptOperations; import org.thoughtcrime.securesms.BuildConfig; -import org.thoughtcrime.securesms.billing.GooglePlayBillingApi; -import org.thoughtcrime.securesms.billing.GooglePlayBillingFactory; import org.thoughtcrime.securesms.components.TypingStatusRepository; import org.thoughtcrime.securesms.components.TypingStatusSender; import org.thoughtcrime.securesms.crypto.ReentrantSessionLock; @@ -460,8 +460,8 @@ public class ApplicationDependencyProvider implements AppDependencies.Provider { } @Override - public @NonNull GooglePlayBillingApi provideBillingApi() { - return GooglePlayBillingFactory.create(context); + public @NonNull BillingApi provideBillingApi() { + return BillingFactory.create(context, RemoteConfig.messageBackups()); } @VisibleForTesting diff --git a/app/src/nightly/java/org/thoughtcrime/securesms/billing/GooglePlayBillingFactory.kt b/app/src/nightly/java/org/thoughtcrime/securesms/billing/GooglePlayBillingFactory.kt deleted file mode 100644 index c71e690c00..0000000000 --- a/app/src/nightly/java/org/thoughtcrime/securesms/billing/GooglePlayBillingFactory.kt +++ /dev/null @@ -1,13 +0,0 @@ -package org.thoughtcrime.securesms.billing - -import android.content.Context - -/** - * Website builds do not support google play billing. - */ -object GooglePlayBillingFactory { - @JvmStatic - fun create(context: Context): GooglePlayBillingApi { - return GooglePlayBillingApi.Empty - } -} diff --git a/app/src/play/java/org/thoughtcrime/securesms/billing/GooglePlayBillingFactory.kt b/app/src/play/java/org/thoughtcrime/securesms/billing/GooglePlayBillingFactory.kt deleted file mode 100644 index d4994d40dc..0000000000 --- a/app/src/play/java/org/thoughtcrime/securesms/billing/GooglePlayBillingFactory.kt +++ /dev/null @@ -1,73 +0,0 @@ -package org.thoughtcrime.securesms.billing - -import android.app.Activity -import android.content.Context -import com.android.billingclient.api.BillingClient.BillingResponseCode -import com.android.billingclient.api.ProductDetailsResult -import com.android.billingclient.api.PurchasesUpdatedListener -import org.signal.billing.BillingApi -import org.signal.core.util.logging.Log -import org.thoughtcrime.securesms.util.RemoteConfig - -/** - * Play billing factory. Returns empty implementation if message backups are not enabled. - */ -object GooglePlayBillingFactory { - @JvmStatic - fun create(context: Context): GooglePlayBillingApi { - return if (RemoteConfig.messageBackups) { - GooglePlayBillingApiImpl(context) - } else { - GooglePlayBillingApi.Empty - } - } -} - -/** - * Play Store implementation - */ -private class GooglePlayBillingApiImpl(context: Context) : GooglePlayBillingApi { - - private companion object { - val TAG = Log.tag(GooglePlayBillingApiImpl::class) - } - - private val purchasesUpdatedListener = PurchasesUpdatedListener { billingResult, purchases -> - when { - billingResult.responseCode == BillingResponseCode.OK && purchases != null -> { - Log.d(TAG, "purchasesUpdatedListener: ${purchases.size} purchases.") - purchases.forEach { - // Handle purchases. - } - } - billingResult.responseCode == BillingResponseCode.USER_CANCELED -> { - // Handle user cancelled - Log.d(TAG, "purchasesUpdatedListener: User cancelled.") - } - else -> { - Log.d(TAG, "purchasesUpdatedListener: No purchases.") - } - } - } - - private val billingApi: BillingApi = BillingApi.getOrCreate(context, purchasesUpdatedListener) - - override fun isApiAvailable(): Boolean = billingApi.areSubscriptionsSupported() - - override suspend fun queryProducts() { - val products: ProductDetailsResult = billingApi.queryProducts() - - Log.d(TAG, "queryProducts: ${products.billingResult.responseCode}, ${products.billingResult.debugMessage}") - } - - override suspend fun queryPurchases() { - Log.d(TAG, "queryPurchases") - - val purchaseResult = billingApi.queryPurchases() - purchasesUpdatedListener.onPurchasesUpdated(purchaseResult.billingResult, purchaseResult.purchasesList) - } - - override suspend fun launchBillingFlow(activity: Activity) { - billingApi.launchBillingFlow(activity) - } -} diff --git a/app/src/test/java/org/thoughtcrime/securesms/dependencies/MockApplicationDependencyProvider.kt b/app/src/test/java/org/thoughtcrime/securesms/dependencies/MockApplicationDependencyProvider.kt index 31092b10ed..9dc89e94a9 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/dependencies/MockApplicationDependencyProvider.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/dependencies/MockApplicationDependencyProvider.kt @@ -2,11 +2,11 @@ package org.thoughtcrime.securesms.dependencies import io.mockk.mockk import org.mockito.Mockito +import org.signal.core.util.billing.BillingApi import org.signal.core.util.concurrent.DeadlockDetector import org.signal.libsignal.net.Network import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations import org.signal.libsignal.zkgroup.receipts.ClientZkReceiptOperations -import org.thoughtcrime.securesms.billing.GooglePlayBillingApi import org.thoughtcrime.securesms.components.TypingStatusRepository import org.thoughtcrime.securesms.components.TypingStatusSender import org.thoughtcrime.securesms.crypto.storage.SignalServiceDataStoreImpl @@ -199,7 +199,7 @@ class MockApplicationDependencyProvider : AppDependencies.Provider { return mockk() } - override fun provideBillingApi(): GooglePlayBillingApi { + override fun provideBillingApi(): BillingApi { return mockk() } } diff --git a/app/src/website/java/org/signal/billing/BillingFactory.kt b/app/src/website/java/org/signal/billing/BillingFactory.kt new file mode 100644 index 0000000000..8e7a47306e --- /dev/null +++ b/app/src/website/java/org/signal/billing/BillingFactory.kt @@ -0,0 +1,14 @@ +package org.signal.billing + +import android.content.Context +import org.signal.core.util.billing.BillingApi + +/** + * Website builds do not support google play billing. + */ +object BillingFactory { + @JvmStatic + fun create(context: Context): BillingApi { + return BillingApi.Empty + } +} diff --git a/app/src/website/java/org/thoughtcrime/securesms/billing/GooglePlayBillingFactory.kt b/app/src/website/java/org/thoughtcrime/securesms/billing/GooglePlayBillingFactory.kt deleted file mode 100644 index c71e690c00..0000000000 --- a/app/src/website/java/org/thoughtcrime/securesms/billing/GooglePlayBillingFactory.kt +++ /dev/null @@ -1,13 +0,0 @@ -package org.thoughtcrime.securesms.billing - -import android.content.Context - -/** - * Website builds do not support google play billing. - */ -object GooglePlayBillingFactory { - @JvmStatic - fun create(context: Context): GooglePlayBillingApi { - return GooglePlayBillingApi.Empty - } -} diff --git a/billing/build.gradle.kts b/billing/build.gradle.kts index de0a343141..c9005a1f3a 100644 --- a/billing/build.gradle.kts +++ b/billing/build.gradle.kts @@ -9,6 +9,6 @@ android { dependencies { lintChecks(project(":lintchecks")) - api(libs.android.billing) + implementation(libs.android.billing) implementation(project(":core-util")) } diff --git a/billing/src/main/java/org/signal/billing/BillingApi.kt b/billing/src/main/java/org/signal/billing/BillingApiImpl.kt similarity index 83% rename from billing/src/main/java/org/signal/billing/BillingApi.kt rename to billing/src/main/java/org/signal/billing/BillingApiImpl.kt index 1542810c4b..7947109691 100644 --- a/billing/src/main/java/org/signal/billing/BillingApi.kt +++ b/billing/src/main/java/org/signal/billing/BillingApiImpl.kt @@ -17,7 +17,6 @@ import com.android.billingclient.api.BillingResult import com.android.billingclient.api.PendingPurchasesParams import com.android.billingclient.api.ProductDetails import com.android.billingclient.api.ProductDetailsResult -import com.android.billingclient.api.PurchasesResult import com.android.billingclient.api.PurchasesUpdatedListener import com.android.billingclient.api.QueryProductDetailsParams import com.android.billingclient.api.QueryPurchasesParams @@ -37,6 +36,7 @@ import kotlinx.coroutines.flow.retry import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.signal.core.util.billing.BillingApi import org.signal.core.util.logging.Log /** @@ -45,29 +45,37 @@ import org.signal.core.util.logging.Log * * Care should be taken here to ensure only one instance of this exists at a time. */ -class BillingApi private constructor( - context: Context, - onPurchaseUpdateListener: PurchasesUpdatedListener -) { +internal class BillingApiImpl( + context: Context +) : BillingApi { + companion object { private val TAG = Log.tag(BillingApi::class) - - private var instance: BillingApi? = null - - @Synchronized - fun getOrCreate(context: Context, onPurchaseUpdateListener: PurchasesUpdatedListener): BillingApi { - return instance ?: BillingApi(context, onPurchaseUpdateListener).let { - instance = it - it - } - } } private val connectionState = MutableStateFlow(State.Init) private val coroutineScope = CoroutineScope(Dispatchers.Default) + private val purchasesUpdatedListener = PurchasesUpdatedListener { billingResult, purchases -> + when { + billingResult.responseCode == BillingResponseCode.OK && purchases != null -> { + Log.d(TAG, "purchasesUpdatedListener: ${purchases.size} purchases.") + purchases.forEach { + // Handle purchases. + } + } + billingResult.responseCode == BillingResponseCode.USER_CANCELED -> { + // Handle user cancelled + Log.d(TAG, "purchasesUpdatedListener: User cancelled.") + } + else -> { + Log.d(TAG, "purchasesUpdatedListener: No purchases.") + } + } + } + private val billingClient: BillingClient = BillingClient.newBuilder(context) - .setListener(onPurchaseUpdateListener) + .setListener(purchasesUpdatedListener) .enablePendingPurchases( PendingPurchasesParams.newBuilder() .enableOneTimeProducts() @@ -88,7 +96,67 @@ class BillingApi private constructor( } } - suspend fun queryProducts(): ProductDetailsResult { + override suspend fun queryProducts() { + val products = queryProductsInternal() + } + + override suspend fun queryPurchases() { + val param = QueryPurchasesParams.newBuilder() + .setProductType(ProductType.SUBS) + .build() + + val purchases = doOnConnectionReady { + billingClient.queryPurchasesAsync(param) + } + + purchasesUpdatedListener.onPurchasesUpdated(purchases.billingResult, purchases.purchasesList) + } + + /** + * Launches the Google Play billing flow. + * Returns a billing result if we launched the flow, null otherwise. + */ + override suspend fun launchBillingFlow(activity: Activity) { + val productDetails = queryProductsInternal().productDetailsList + if (productDetails.isNullOrEmpty()) { + Log.w(TAG, "No products are available! Cancelling billing flow launch.") + return + } + + val subscriptionDetails: ProductDetails = productDetails[0] + val offerToken = subscriptionDetails.subscriptionOfferDetails?.firstOrNull() + if (offerToken == null) { + Log.w(TAG, "No offer tokens available on subscription product! Cancelling billing flow launch.") + return + } + + val productDetailParamsList = listOf( + ProductDetailsParams.newBuilder() + .setProductDetails(subscriptionDetails) + .setOfferToken(offerToken.offerToken) + .build() + ) + + val billingFlowParams = BillingFlowParams.newBuilder() + .setProductDetailsParamsList(productDetailParamsList) + .build() + + doOnConnectionReady { + withContext(Dispatchers.Main) { + billingClient.launchBillingFlow(activity, billingFlowParams) + } + } + } + + /** + * Returns whether or not subscriptions are supported by a user's device. Lack of subscription support is generally due + * to out-of-date Google Play API + */ + override fun isApiAvailable(): Boolean { + return billingClient.isFeatureSupported(BillingClient.FeatureType.SUBSCRIPTIONS).responseCode == BillingResponseCode.OK + } + + private suspend fun queryProductsInternal(): ProductDetailsResult { val productList = listOf( QueryProductDetailsParams.Product.newBuilder() .setProductId("") // TODO [message-backups] where does the product id come from? @@ -107,60 +175,6 @@ class BillingApi private constructor( } } - suspend fun queryPurchases(): PurchasesResult { - val param = QueryPurchasesParams.newBuilder() - .setProductType(ProductType.SUBS) - .build() - - return doOnConnectionReady { - billingClient.queryPurchasesAsync(param) - } - } - - /** - * Launches the Google Play billing flow. - * Returns a billing result if we launched the flow, null otherwise. - */ - suspend fun launchBillingFlow(activity: Activity): BillingResult? { - val productDetails = queryProducts().productDetailsList - if (productDetails.isNullOrEmpty()) { - Log.w(TAG, "No products are available! Cancelling billing flow launch.") - return null - } - - val subscriptionDetails: ProductDetails = productDetails[0] - val offerToken = subscriptionDetails.subscriptionOfferDetails?.firstOrNull() - if (offerToken == null) { - Log.w(TAG, "No offer tokens available on subscription product! Cancelling billing flow launch.") - return null - } - - val productDetailParamsList = listOf( - ProductDetailsParams.newBuilder() - .setProductDetails(subscriptionDetails) - .setOfferToken(offerToken.offerToken) - .build() - ) - - val billingFlowParams = BillingFlowParams.newBuilder() - .setProductDetailsParamsList(productDetailParamsList) - .build() - - return doOnConnectionReady { - withContext(Dispatchers.Main) { - billingClient.launchBillingFlow(activity, billingFlowParams) - } - } - } - - /** - * Returns whether or not subscriptions are supported by a user's device. Lack of subscription support is generally due - * to out-of-date Google Play API - */ - fun areSubscriptionsSupported(): Boolean { - return billingClient.isFeatureSupported(BillingClient.FeatureType.SUBSCRIPTIONS).responseCode == BillingResponseCode.OK - } - private suspend fun doOnConnectionReady(block: suspend () -> T): T { val state = connectionState .filter { it == State.Connected || it is State.Failure } diff --git a/billing/src/main/java/org/signal/billing/BillingFactory.kt b/billing/src/main/java/org/signal/billing/BillingFactory.kt new file mode 100644 index 0000000000..925826b5ef --- /dev/null +++ b/billing/src/main/java/org/signal/billing/BillingFactory.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.billing + +import android.content.Context +import org.signal.core.util.billing.BillingApi + +/** + * Play billing factory. Returns empty implementation if message backups are not enabled. + */ +object BillingFactory { + @JvmStatic + fun create(context: Context, isBackupsAvailable: Boolean): BillingApi { + return if (isBackupsAvailable) { + BillingApiImpl(context) + } else { + BillingApi.Empty + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/billing/GooglePlayBillingApi.kt b/core-util/src/main/java/org/signal/core/util/billing/BillingApi.kt similarity index 84% rename from app/src/main/java/org/thoughtcrime/securesms/billing/GooglePlayBillingApi.kt rename to core-util/src/main/java/org/signal/core/util/billing/BillingApi.kt index 10c4236ba9..51d25f0931 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/billing/GooglePlayBillingApi.kt +++ b/core-util/src/main/java/org/signal/core/util/billing/BillingApi.kt @@ -3,14 +3,14 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.thoughtcrime.securesms.billing +package org.signal.core.util.billing import android.app.Activity /** * Variant interface for the BillingApi. */ -interface GooglePlayBillingApi { +interface BillingApi { fun isApiAvailable(): Boolean = false suspend fun queryProducts() = Unit @@ -26,5 +26,5 @@ interface GooglePlayBillingApi { * Empty implementation, to be used when play services are available but * GooglePlayBillingApi is not available. */ - object Empty : GooglePlayBillingApi + object Empty : BillingApi }