mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-22 12:08:34 +00:00
Add billing module and include in play implementation.
This commit is contained in:
@@ -558,6 +558,8 @@ dependencies {
|
|||||||
implementation(libs.rxjava3.rxkotlin)
|
implementation(libs.rxjava3.rxkotlin)
|
||||||
implementation(libs.rxdogtag)
|
implementation(libs.rxdogtag)
|
||||||
|
|
||||||
|
"playImplementation"(project(":billing"))
|
||||||
|
|
||||||
"spinnerImplementation"(project(":spinner"))
|
"spinnerImplementation"(project(":spinner"))
|
||||||
|
|
||||||
"canaryImplementation"(libs.square.leakcanary)
|
"canaryImplementation"(libs.square.leakcanary)
|
||||||
|
|||||||
@@ -196,6 +196,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
|||||||
.addNonBlocking(this::beginJobLoop)
|
.addNonBlocking(this::beginJobLoop)
|
||||||
.addNonBlocking(EmojiSource::refresh)
|
.addNonBlocking(EmojiSource::refresh)
|
||||||
.addNonBlocking(() -> AppDependencies.getGiphyMp4Cache().onAppStart(this))
|
.addNonBlocking(() -> AppDependencies.getGiphyMp4Cache().onAppStart(this))
|
||||||
|
.addNonBlocking(AppDependencies::getBillingApi)
|
||||||
.addNonBlocking(this::ensureProfileUploaded)
|
.addNonBlocking(this::ensureProfileUploaded)
|
||||||
.addNonBlocking(() -> AppDependencies.getExpireStoriesManager().scheduleIfNecessary())
|
.addNonBlocking(() -> AppDependencies.getExpireStoriesManager().scheduleIfNecessary())
|
||||||
.addPostRender(() -> AppDependencies.getDeletedCallEventManager().scheduleIfNecessary())
|
.addPostRender(() -> AppDependencies.getDeletedCallEventManager().scheduleIfNecessary())
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.thoughtcrime.securesms.billing
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Variant interface for the BillingApi.
|
||||||
|
*/
|
||||||
|
interface GooglePlayBillingApi {
|
||||||
|
fun isApiAvailable(): Boolean = false
|
||||||
|
suspend fun queryProducts() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Empty implementation, to be used when play services are available but
|
||||||
|
* GooglePlayBillingApi is not available.
|
||||||
|
*/
|
||||||
|
object Empty : GooglePlayBillingApi
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import org.signal.core.util.resettableLazy
|
|||||||
import org.signal.libsignal.net.Network
|
import org.signal.libsignal.net.Network
|
||||||
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations
|
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations
|
||||||
import org.signal.libsignal.zkgroup.receipts.ClientZkReceiptOperations
|
import org.signal.libsignal.zkgroup.receipts.ClientZkReceiptOperations
|
||||||
|
import org.thoughtcrime.securesms.billing.GooglePlayBillingApi
|
||||||
import org.thoughtcrime.securesms.components.TypingStatusRepository
|
import org.thoughtcrime.securesms.components.TypingStatusRepository
|
||||||
import org.thoughtcrime.securesms.components.TypingStatusSender
|
import org.thoughtcrime.securesms.components.TypingStatusSender
|
||||||
import org.thoughtcrime.securesms.crypto.storage.SignalServiceDataStoreImpl
|
import org.thoughtcrime.securesms.crypto.storage.SignalServiceDataStoreImpl
|
||||||
@@ -210,6 +211,11 @@ object AppDependencies {
|
|||||||
provider.provideAndroidCallAudioManager()
|
provider.provideAndroidCallAudioManager()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
val billingApi: GooglePlayBillingApi by lazy {
|
||||||
|
provider.provideBillingApi()
|
||||||
|
}
|
||||||
|
|
||||||
private val _webSocketObserver: Subject<WebSocketConnectionState> = BehaviorSubject.create()
|
private val _webSocketObserver: Subject<WebSocketConnectionState> = BehaviorSubject.create()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -342,5 +348,6 @@ object AppDependencies {
|
|||||||
fun provideClientZkReceiptOperations(signalServiceConfiguration: SignalServiceConfiguration): ClientZkReceiptOperations
|
fun provideClientZkReceiptOperations(signalServiceConfiguration: SignalServiceConfiguration): ClientZkReceiptOperations
|
||||||
fun provideScheduledMessageManager(): ScheduledMessageManager
|
fun provideScheduledMessageManager(): ScheduledMessageManager
|
||||||
fun provideLibsignalNetwork(config: SignalServiceConfiguration): Network
|
fun provideLibsignalNetwork(config: SignalServiceConfiguration): Network
|
||||||
|
fun provideBillingApi(): GooglePlayBillingApi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import org.signal.libsignal.net.Network;
|
|||||||
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations;
|
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations;
|
||||||
import org.signal.libsignal.zkgroup.receipts.ClientZkReceiptOperations;
|
import org.signal.libsignal.zkgroup.receipts.ClientZkReceiptOperations;
|
||||||
import org.thoughtcrime.securesms.BuildConfig;
|
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.TypingStatusRepository;
|
||||||
import org.thoughtcrime.securesms.components.TypingStatusSender;
|
import org.thoughtcrime.securesms.components.TypingStatusSender;
|
||||||
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
|
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
|
||||||
@@ -69,8 +71,8 @@ import org.thoughtcrime.securesms.util.AlarmSleepTimer;
|
|||||||
import org.thoughtcrime.securesms.util.AppForegroundObserver;
|
import org.thoughtcrime.securesms.util.AppForegroundObserver;
|
||||||
import org.thoughtcrime.securesms.util.ByteUnit;
|
import org.thoughtcrime.securesms.util.ByteUnit;
|
||||||
import org.thoughtcrime.securesms.util.EarlyMessageCache;
|
import org.thoughtcrime.securesms.util.EarlyMessageCache;
|
||||||
import org.thoughtcrime.securesms.util.RemoteConfig;
|
|
||||||
import org.thoughtcrime.securesms.util.FrameRateTracker;
|
import org.thoughtcrime.securesms.util.FrameRateTracker;
|
||||||
|
import org.thoughtcrime.securesms.util.RemoteConfig;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
import org.thoughtcrime.securesms.video.exo.GiphyMp4Cache;
|
import org.thoughtcrime.securesms.video.exo.GiphyMp4Cache;
|
||||||
import org.thoughtcrime.securesms.video.exo.SimpleExoPlayerPool;
|
import org.thoughtcrime.securesms.video.exo.SimpleExoPlayerPool;
|
||||||
@@ -92,11 +94,11 @@ import org.whispersystems.signalservice.api.util.SleepTimer;
|
|||||||
import org.whispersystems.signalservice.api.util.UptimeSleepTimer;
|
import org.whispersystems.signalservice.api.util.UptimeSleepTimer;
|
||||||
import org.whispersystems.signalservice.api.websocket.WebSocketFactory;
|
import org.whispersystems.signalservice.api.websocket.WebSocketFactory;
|
||||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
|
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
|
||||||
|
import org.whispersystems.signalservice.internal.websocket.LibSignalChatConnection;
|
||||||
import org.whispersystems.signalservice.internal.websocket.LibSignalNetworkExtensions;
|
import org.whispersystems.signalservice.internal.websocket.LibSignalNetworkExtensions;
|
||||||
|
import org.whispersystems.signalservice.internal.websocket.OkHttpWebSocketConnection;
|
||||||
import org.whispersystems.signalservice.internal.websocket.ShadowingWebSocketConnection;
|
import org.whispersystems.signalservice.internal.websocket.ShadowingWebSocketConnection;
|
||||||
import org.whispersystems.signalservice.internal.websocket.WebSocketConnection;
|
import org.whispersystems.signalservice.internal.websocket.WebSocketConnection;
|
||||||
import org.whispersystems.signalservice.internal.websocket.LibSignalChatConnection;
|
|
||||||
import org.whispersystems.signalservice.internal.websocket.OkHttpWebSocketConnection;
|
|
||||||
import org.whispersystems.signalservice.internal.websocket.WebSocketShadowingBridge;
|
import org.whispersystems.signalservice.internal.websocket.WebSocketShadowingBridge;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@@ -457,6 +459,11 @@ public class ApplicationDependencyProvider implements AppDependencies.Provider {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull GooglePlayBillingApi provideBillingApi() {
|
||||||
|
return GooglePlayBillingFactory.create(context);
|
||||||
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
static class DynamicCredentialsProvider implements CredentialsProvider {
|
static class DynamicCredentialsProvider implements CredentialsProvider {
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.thoughtcrime.securesms.maps;
|
package org.thoughtcrime.securesms.maps;
|
||||||
|
|
||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.location.Location;
|
import android.location.Location;
|
||||||
@@ -73,6 +74,7 @@ class LocationRetriever implements DefaultLifecycleObserver, LocationListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
@Override
|
@Override
|
||||||
public void onStop(@NonNull LifecycleOwner owner) {
|
public void onStop(@NonNull LifecycleOwner owner) {
|
||||||
Log.i(TAG, "Removing any possible location listeners.");
|
Log.i(TAG, "Removing any possible location listeners.");
|
||||||
|
|||||||
28
app/src/play/AndroidManifest.xml
Normal file
28
app/src/play/AndroidManifest.xml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION"/>
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
|
||||||
|
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION"/>
|
||||||
|
|
||||||
|
<application>
|
||||||
|
<receiver android:name=".apkupdate.ApkUpdateRefreshListener" android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
<receiver android:name=".apkupdate.ApkUpdateDownloadManagerReceiver" android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.DOWNLOAD_COMPLETE"/>
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".apkupdate.ApkUpdatePackageInstallerReceiver"
|
||||||
|
android:exported="true" />
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".apkupdate.ApkUpdateNotificationReceiver"
|
||||||
|
android:exported="false" />
|
||||||
|
</application>
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package org.thoughtcrime.securesms.billing
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.android.billingclient.api.ProductDetailsResult
|
||||||
|
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 billingApi: BillingApi = BillingApi.getOrCreate(context)
|
||||||
|
|
||||||
|
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}")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import org.signal.core.util.concurrent.DeadlockDetector;
|
|||||||
import org.signal.libsignal.net.Network;
|
import org.signal.libsignal.net.Network;
|
||||||
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations;
|
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations;
|
||||||
import org.signal.libsignal.zkgroup.receipts.ClientZkReceiptOperations;
|
import org.signal.libsignal.zkgroup.receipts.ClientZkReceiptOperations;
|
||||||
|
import org.thoughtcrime.securesms.billing.GooglePlayBillingApi;
|
||||||
import org.thoughtcrime.securesms.components.TypingStatusRepository;
|
import org.thoughtcrime.securesms.components.TypingStatusRepository;
|
||||||
import org.thoughtcrime.securesms.components.TypingStatusSender;
|
import org.thoughtcrime.securesms.components.TypingStatusSender;
|
||||||
import org.thoughtcrime.securesms.crypto.storage.SignalServiceDataStoreImpl;
|
import org.thoughtcrime.securesms.crypto.storage.SignalServiceDataStoreImpl;
|
||||||
@@ -238,4 +239,9 @@ public class MockApplicationDependencyProvider implements AppDependencies.Provid
|
|||||||
public @NonNull Network provideLibsignalNetwork(@NonNull SignalServiceConfiguration config) {
|
public @NonNull Network provideLibsignalNetwork(@NonNull SignalServiceConfiguration config) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull GooglePlayBillingApi provideBillingApi() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
14
billing/build.gradle.kts
Normal file
14
billing/build.gradle.kts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
plugins {
|
||||||
|
id("signal-library")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "org.signal.billing"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
lintChecks(project(":lintchecks"))
|
||||||
|
|
||||||
|
api(libs.android.billing)
|
||||||
|
implementation(project(":core-util"))
|
||||||
|
}
|
||||||
5
billing/src/main/AndroidManifest.xml
Normal file
5
billing/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
</manifest>
|
||||||
169
billing/src/main/java/org/signal/billing/BillingApi.kt
Normal file
169
billing/src/main/java/org/signal/billing/BillingApi.kt
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.billing
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.android.billingclient.api.BillingClient
|
||||||
|
import com.android.billingclient.api.BillingClient.BillingResponseCode
|
||||||
|
import com.android.billingclient.api.BillingClientStateListener
|
||||||
|
import com.android.billingclient.api.BillingResult
|
||||||
|
import com.android.billingclient.api.PendingPurchasesParams
|
||||||
|
import com.android.billingclient.api.ProductDetailsResult
|
||||||
|
import com.android.billingclient.api.PurchasesUpdatedListener
|
||||||
|
import com.android.billingclient.api.QueryProductDetailsParams
|
||||||
|
import com.android.billingclient.api.queryProductDetails
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.retry
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.signal.core.util.logging.Log
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BillingApi serves as the core location for interacting with the Google Billing API. Use of this API is required
|
||||||
|
* for remote backups paid tier, and will only be available in play store builds.
|
||||||
|
*
|
||||||
|
* Care should be taken here to ensure only one instance of this exists at a time.
|
||||||
|
*/
|
||||||
|
class BillingApi private constructor(
|
||||||
|
context: Context
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
private val TAG = Log.tag(BillingApi::class)
|
||||||
|
|
||||||
|
private var instance: BillingApi? = null
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun getOrCreate(context: Context): BillingApi {
|
||||||
|
return instance ?: BillingApi(context).let {
|
||||||
|
instance = it
|
||||||
|
it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val connectionState = MutableStateFlow<State>(State.Init)
|
||||||
|
private val coroutineScope = CoroutineScope(Dispatchers.Default)
|
||||||
|
|
||||||
|
private val purchasesUpdatedListener = PurchasesUpdatedListener { billingResult, purchases ->
|
||||||
|
Log.d(TAG, "purchasesUpdatedListener: ${billingResult.responseCode}")
|
||||||
|
Log.d(TAG, "purchasesUpdatedListener: Detected ${purchases?.size ?: 0} purchases.")
|
||||||
|
}
|
||||||
|
|
||||||
|
private val billingClient: BillingClient = BillingClient.newBuilder(context)
|
||||||
|
.setListener(purchasesUpdatedListener)
|
||||||
|
.enablePendingPurchases(
|
||||||
|
PendingPurchasesParams.newBuilder()
|
||||||
|
.enableOneTimeProducts()
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
init {
|
||||||
|
coroutineScope.launch {
|
||||||
|
createConnectionFlow()
|
||||||
|
.retry { it is RetryException } // TODO [message-backups] - consider a delay here
|
||||||
|
.collect { newState ->
|
||||||
|
Log.d(TAG, "Updating Google Play Billing connection state: $newState")
|
||||||
|
connectionState.update {
|
||||||
|
newState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun queryProducts(): ProductDetailsResult {
|
||||||
|
val productList = listOf(
|
||||||
|
QueryProductDetailsParams.Product.newBuilder()
|
||||||
|
.setProductId("") // TODO [message-backups] where does the product id come from?
|
||||||
|
.setProductType(BillingClient.ProductType.SUBS)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
|
||||||
|
val params = QueryProductDetailsParams.newBuilder()
|
||||||
|
.setProductList(productList)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
doOnConnectionReady {
|
||||||
|
billingClient.queryProductDetails(params)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 <T> doOnConnectionReady(block: suspend () -> T): T {
|
||||||
|
val state = connectionState
|
||||||
|
.filter { it == State.Connected || it is State.Failure }
|
||||||
|
.first()
|
||||||
|
|
||||||
|
return when (state) {
|
||||||
|
State.Connected -> block()
|
||||||
|
is State.Failure -> throw state.billingError
|
||||||
|
else -> error("Unexpected state: $state")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createConnectionFlow(): Flow<State> {
|
||||||
|
return callbackFlow {
|
||||||
|
Log.d(TAG, "Starting Google Play Billing connection...", true)
|
||||||
|
trySend(State.Connecting)
|
||||||
|
|
||||||
|
billingClient.startConnection(object : BillingClientStateListener {
|
||||||
|
override fun onBillingServiceDisconnected() {
|
||||||
|
Log.d(TAG, "Google Play Billing became disconnected.", true)
|
||||||
|
trySend(State.Disconnected)
|
||||||
|
cancel(CancellationException("Google Play Billing became disconnected.", RetryException()))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBillingSetupFinished(billingResult: BillingResult) {
|
||||||
|
Log.d(TAG, "onBillingSetupFinished: ${billingResult.responseCode}")
|
||||||
|
if (billingResult.responseCode == BillingResponseCode.OK) {
|
||||||
|
Log.d(TAG, "Google Play Billing is ready.", true)
|
||||||
|
trySend(State.Connected)
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "Google Play Billing failed to connect.", true)
|
||||||
|
val billingError = BillingError(
|
||||||
|
billingResponseCode = billingResult.responseCode
|
||||||
|
)
|
||||||
|
trySend(State.Failure(billingError))
|
||||||
|
cancel(CancellationException("Failed to connect to Google Play Billing", billingError))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
awaitClose {
|
||||||
|
billingClient.endConnection()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed interface State {
|
||||||
|
data object Init : State
|
||||||
|
data object Connecting : State
|
||||||
|
data object Connected : State
|
||||||
|
data object Disconnected : State
|
||||||
|
data class Failure(val billingError: BillingError) : State
|
||||||
|
}
|
||||||
|
|
||||||
|
private class RetryException : Exception()
|
||||||
|
}
|
||||||
10
billing/src/main/java/org/signal/billing/BillingError.kt
Normal file
10
billing/src/main/java/org/signal/billing/BillingError.kt
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.billing
|
||||||
|
|
||||||
|
class BillingError(
|
||||||
|
val billingResponseCode: Int
|
||||||
|
) : Exception()
|
||||||
@@ -99,6 +99,7 @@ dependencyResolutionManagement {
|
|||||||
library("androidx-asynclayoutinflater-appcompat", "androidx.asynclayoutinflater:asynclayoutinflater-appcompat:1.1.0-alpha01")
|
library("androidx-asynclayoutinflater-appcompat", "androidx.asynclayoutinflater:asynclayoutinflater-appcompat:1.1.0-alpha01")
|
||||||
library("androidx-emoji2", "androidx.emoji2:emoji2:1.4.0")
|
library("androidx-emoji2", "androidx.emoji2:emoji2:1.4.0")
|
||||||
library("androidx-documentfile", "androidx.documentfile:documentfile:1.0.0")
|
library("androidx-documentfile", "androidx.documentfile:documentfile:1.0.0")
|
||||||
|
library("android-billing", "com.android.billingclient:billing-ktx:7.0.0")
|
||||||
|
|
||||||
// Material
|
// Material
|
||||||
library("material-material", "com.google.android.material:material:1.8.0")
|
library("material-material", "com.google.android.material:material:1.8.0")
|
||||||
|
|||||||
@@ -3396,6 +3396,16 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
|||||||
<sha256 value="81dd485618a509a3235929b9eb13091d884452661de6ce5a45cc38b1c555421c" origin="Generated by Gradle"/>
|
<sha256 value="81dd485618a509a3235929b9eb13091d884452661de6ce5a45cc38b1c555421c" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</component>
|
||||||
|
<component group="com.android.billingclient" name="billing" version="7.0.0">
|
||||||
|
<artifact name="billing-7.0.0.aar">
|
||||||
|
<sha256 value="7d58671c3e56da57befe2798fa60f806a26bb724d326a0865da05bc50827ff87" origin="Generated by Gradle"/>
|
||||||
|
</artifact>
|
||||||
|
</component>
|
||||||
|
<component group="com.android.billingclient" name="billing-ktx" version="7.0.0">
|
||||||
|
<artifact name="billing-ktx-7.0.0.aar">
|
||||||
|
<sha256 value="bfb7416b270b2a15ddb09fe0c3a9f9a4edeadb348c875104c615dce9766924ba" origin="Generated by Gradle"/>
|
||||||
|
</artifact>
|
||||||
|
</component>
|
||||||
<component group="com.android.databinding" name="baseLibrary" version="8.0.2">
|
<component group="com.android.databinding" name="baseLibrary" version="8.0.2">
|
||||||
<artifact name="baseLibrary-8.0.2.jar">
|
<artifact name="baseLibrary-8.0.2.jar">
|
||||||
<sha256 value="530b2113317ff4d0f69ffdfb49387ba4b86aac169e1c77dff943405b79adcf8b" origin="Generated by Gradle"/>
|
<sha256 value="530b2113317ff4d0f69ffdfb49387ba4b86aac169e1c77dff943405b79adcf8b" origin="Generated by Gradle"/>
|
||||||
@@ -4871,6 +4881,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
|||||||
<sha256 value="2896d76f432be52167295bb9ce45ade25c310aeffc04d28cf8db6a15868e83de" origin="Generated by Gradle"/>
|
<sha256 value="2896d76f432be52167295bb9ce45ade25c310aeffc04d28cf8db6a15868e83de" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</component>
|
||||||
|
<component group="com.google.android.gms" name="play-services-base" version="18.3.0">
|
||||||
|
<artifact name="play-services-base-18.3.0.aar">
|
||||||
|
<sha256 value="94066a46047e3d593eb652383e7767e0630385ecd75133e68c21124aa96b8dc2" origin="Generated by Gradle"/>
|
||||||
|
</artifact>
|
||||||
|
</component>
|
||||||
<component group="com.google.android.gms" name="play-services-basement" version="18.0.0">
|
<component group="com.google.android.gms" name="play-services-basement" version="18.0.0">
|
||||||
<artifact name="play-services-basement-18.0.0.aar">
|
<artifact name="play-services-basement-18.0.0.aar">
|
||||||
<sha256 value="55c1777467901a2d399f3252384c4976284aa35fddfd5995466dbeacb49f9dd6" origin="Generated by Gradle"/>
|
<sha256 value="55c1777467901a2d399f3252384c4976284aa35fddfd5995466dbeacb49f9dd6" origin="Generated by Gradle"/>
|
||||||
@@ -4886,6 +4901,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
|||||||
<sha256 value="ef43ebfc641d71481543524f46d126793b4cb57bf466c4df4ce43d0cb5e11b91" origin="Generated by Gradle"/>
|
<sha256 value="ef43ebfc641d71481543524f46d126793b4cb57bf466c4df4ce43d0cb5e11b91" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</component>
|
||||||
|
<component group="com.google.android.gms" name="play-services-basement" version="18.3.0">
|
||||||
|
<artifact name="play-services-basement-18.3.0.aar">
|
||||||
|
<sha256 value="6c11ae3eb2dd7f17373f919c4c557a70e4cf891bc0c9b66926a0a6445d654352" origin="Generated by Gradle"/>
|
||||||
|
</artifact>
|
||||||
|
</component>
|
||||||
<component group="com.google.android.gms" name="play-services-cloud-messaging" version="17.0.1">
|
<component group="com.google.android.gms" name="play-services-cloud-messaging" version="17.0.1">
|
||||||
<artifact name="play-services-cloud-messaging-17.0.1.aar">
|
<artifact name="play-services-cloud-messaging-17.0.1.aar">
|
||||||
<sha256 value="1e759adcf0350731ce4dc73b035705e4c7d08bdf7db069cc0468eca3e7bb9dc2" origin="Generated by Gradle"/>
|
<sha256 value="1e759adcf0350731ce4dc73b035705e4c7d08bdf7db069cc0468eca3e7bb9dc2" origin="Generated by Gradle"/>
|
||||||
@@ -4901,6 +4921,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
|||||||
<sha256 value="4c93d05e943d29852d2b3092b467ce8517a222363eb8584b706a47292bddd18a" origin="Generated by Gradle"/>
|
<sha256 value="4c93d05e943d29852d2b3092b467ce8517a222363eb8584b706a47292bddd18a" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</component>
|
||||||
|
<component group="com.google.android.gms" name="play-services-location" version="19.0.0">
|
||||||
|
<artifact name="play-services-location-19.0.0.aar">
|
||||||
|
<sha256 value="6b205c43ba5df751eca8ce9dae7a58effafac7d637fb4fc708a7522d1b99cf80" origin="Generated by Gradle"/>
|
||||||
|
</artifact>
|
||||||
|
</component>
|
||||||
<component group="com.google.android.gms" name="play-services-maps" version="18.0.2">
|
<component group="com.google.android.gms" name="play-services-maps" version="18.0.2">
|
||||||
<artifact name="play-services-maps-18.0.2.aar">
|
<artifact name="play-services-maps-18.0.2.aar">
|
||||||
<sha256 value="442a4687f44a266051853986d8e8f694ffc62e5fe752b613b356c66f9bf55d2d" origin="Generated by Gradle"/>
|
<sha256 value="442a4687f44a266051853986d8e8f694ffc62e5fe752b613b356c66f9bf55d2d" origin="Generated by Gradle"/>
|
||||||
@@ -4911,6 +4936,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
|||||||
<sha256 value="44f50655578a21c579e1bb2dacaec4c545ec4df81393d6fd7236d664268056a9" origin="Generated by Gradle"/>
|
<sha256 value="44f50655578a21c579e1bb2dacaec4c545ec4df81393d6fd7236d664268056a9" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</component>
|
||||||
|
<component group="com.google.android.gms" name="play-services-places-placereport" version="17.0.0">
|
||||||
|
<artifact name="play-services-places-placereport-17.0.0.aar">
|
||||||
|
<sha256 value="2c7fd63ad02f28150ae4ffe4615dac7d694d790e2c4667f777aedc8ee054e929" origin="Generated by Gradle"/>
|
||||||
|
</artifact>
|
||||||
|
</component>
|
||||||
<component group="com.google.android.gms" name="play-services-stats" version="17.0.2">
|
<component group="com.google.android.gms" name="play-services-stats" version="17.0.2">
|
||||||
<artifact name="play-services-stats-17.0.2.aar">
|
<artifact name="play-services-stats-17.0.2.aar">
|
||||||
<sha256 value="dd4314a53f49a378ec146103d36232b96c75454d29526336ccbdf132941764d3" origin="Generated by Gradle"/>
|
<sha256 value="dd4314a53f49a378ec146103d36232b96c75454d29526336ccbdf132941764d3" origin="Generated by Gradle"/>
|
||||||
@@ -4926,6 +4956,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
|||||||
<sha256 value="03465cf53680da23d3d4a3995c0329b7f73b211d38791ecb36e591febb896664" origin="Generated by Gradle"/>
|
<sha256 value="03465cf53680da23d3d4a3995c0329b7f73b211d38791ecb36e591febb896664" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</component>
|
||||||
|
<component group="com.google.android.gms" name="play-services-tasks" version="18.1.0">
|
||||||
|
<artifact name="play-services-tasks-18.1.0.aar">
|
||||||
|
<sha256 value="d60575eae39350e6234858bc9d7d775375707ae82a684e6caf7f3e41a12e25a2" origin="Generated by Gradle"/>
|
||||||
|
</artifact>
|
||||||
|
</component>
|
||||||
<component group="com.google.android.gms" name="play-services-wallet" version="19.2.1">
|
<component group="com.google.android.gms" name="play-services-wallet" version="19.2.1">
|
||||||
<artifact name="play-services-wallet-19.2.1.aar">
|
<artifact name="play-services-wallet-19.2.1.aar">
|
||||||
<sha256 value="c1625a403df419d08e9da950fb72b04c9430b66bd702231941aa726b39942c1d" origin="Generated by Gradle"/>
|
<sha256 value="c1625a403df419d08e9da950fb72b04c9430b66bd702231941aa726b39942c1d" origin="Generated by Gradle"/>
|
||||||
@@ -8196,6 +8231,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
|||||||
<sha256 value="7b9f6960c6689d1a79d82ca3b00b8347bd57d15fe4070a3dd34e826e76136392" origin="Generated by Gradle"/>
|
<sha256 value="7b9f6960c6689d1a79d82ca3b00b8347bd57d15fe4070a3dd34e826e76136392" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</component>
|
||||||
|
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-core" version="1.6.0">
|
||||||
|
<artifact name="kotlinx-coroutines-core-1.6.0.module">
|
||||||
|
<sha256 value="512cfab68f00d7363461b5dd02637dd19048b364a89e1960f99784c705a27a7c" origin="Generated by Gradle"/>
|
||||||
|
</artifact>
|
||||||
|
</component>
|
||||||
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-core" version="1.6.3">
|
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-core" version="1.6.3">
|
||||||
<artifact name="kotlinx-coroutines-core-1.6.3.module">
|
<artifact name="kotlinx-coroutines-core-1.6.3.module">
|
||||||
<sha256 value="14039719f2100d91e0ab220f834f7e5f3578a81026137e4ec16a5c83ecd1740b" origin="Generated by Gradle"/>
|
<sha256 value="14039719f2100d91e0ab220f834f7e5f3578a81026137e4ec16a5c83ecd1740b" origin="Generated by Gradle"/>
|
||||||
@@ -8241,6 +8281,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
|||||||
<sha256 value="c885dd0281076c5843826de317e3cbcdc3d8859dbeef53ae1cfacd1b9c60f96e" origin="Generated by Gradle"/>
|
<sha256 value="c885dd0281076c5843826de317e3cbcdc3d8859dbeef53ae1cfacd1b9c60f96e" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</component>
|
||||||
|
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-core-jvm" version="1.6.0">
|
||||||
|
<artifact name="kotlinx-coroutines-core-jvm-1.6.0.module">
|
||||||
|
<sha256 value="2cdc60217b955ce213fca1f14ee960b85dbd30f8a6966361237410d38a602ef1" origin="Generated by Gradle"/>
|
||||||
|
</artifact>
|
||||||
|
</component>
|
||||||
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-core-jvm" version="1.6.3">
|
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-core-jvm" version="1.6.3">
|
||||||
<artifact name="kotlinx-coroutines-core-jvm-1.6.3.jar">
|
<artifact name="kotlinx-coroutines-core-jvm-1.6.3.jar">
|
||||||
<sha256 value="58a497ab595d83bbbf28892a8b34ab57d94309a8742ee0eba43cb86408d235bf" origin="Generated by Gradle"/>
|
<sha256 value="58a497ab595d83bbbf28892a8b34ab57d94309a8742ee0eba43cb86408d235bf" origin="Generated by Gradle"/>
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ include(":benchmark")
|
|||||||
include(":microbenchmark")
|
include(":microbenchmark")
|
||||||
include(":video")
|
include(":video")
|
||||||
include(":video-app")
|
include(":video-app")
|
||||||
|
include(":billing")
|
||||||
|
|
||||||
project(":app").name = "Signal-Android"
|
project(":app").name = "Signal-Android"
|
||||||
project(":paging").projectDir = file("paging/lib")
|
project(":paging").projectDir = file("paging/lib")
|
||||||
|
|||||||
Reference in New Issue
Block a user