Add method to get the list of offers
This commit is contained in:
parent
3bd6f704e9
commit
bad9327ffa
14 changed files with 336 additions and 16 deletions
|
|
@ -12,6 +12,7 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
compileOnly(project(":utils"))
|
||||||
implementation(libs.androidx.core)
|
implementation(libs.androidx.core)
|
||||||
implementation(libs.kotlinx.coroutines)
|
implementation(libs.kotlinx.coroutines)
|
||||||
implementation(libs.android.billing)
|
implementation(libs.android.billing)
|
||||||
|
|
|
||||||
51
client/android/billing/src/main/kotlin/BillingException.kt
Normal file
51
client/android/billing/src/main/kotlin/BillingException.kt
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
import com.android.billingclient.api.BillingClient.BillingResponseCode.BILLING_UNAVAILABLE
|
||||||
|
import com.android.billingclient.api.BillingClient.BillingResponseCode.DEVELOPER_ERROR
|
||||||
|
import com.android.billingclient.api.BillingClient.BillingResponseCode.ERROR
|
||||||
|
import com.android.billingclient.api.BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED
|
||||||
|
import com.android.billingclient.api.BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED
|
||||||
|
import com.android.billingclient.api.BillingClient.BillingResponseCode.ITEM_NOT_OWNED
|
||||||
|
import com.android.billingclient.api.BillingClient.BillingResponseCode.ITEM_UNAVAILABLE
|
||||||
|
import com.android.billingclient.api.BillingClient.BillingResponseCode.NETWORK_ERROR
|
||||||
|
import com.android.billingclient.api.BillingClient.BillingResponseCode.SERVICE_DISCONNECTED
|
||||||
|
import com.android.billingclient.api.BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE
|
||||||
|
import com.android.billingclient.api.BillingClient.BillingResponseCode.USER_CANCELED
|
||||||
|
import com.android.billingclient.api.BillingResult
|
||||||
|
import org.amnezia.vpn.util.ErrorCode
|
||||||
|
|
||||||
|
internal class BillingException(billingResult: BillingResult) : Exception(billingResult.debugMessage) {
|
||||||
|
|
||||||
|
val errorCode: Int
|
||||||
|
val isCanceled = billingResult.responseCode == USER_CANCELED
|
||||||
|
|
||||||
|
init {
|
||||||
|
when (billingResult.responseCode) {
|
||||||
|
ERROR -> {
|
||||||
|
errorCode = ErrorCode.BillingGooglePlayError
|
||||||
|
}
|
||||||
|
|
||||||
|
BILLING_UNAVAILABLE, SERVICE_DISCONNECTED, SERVICE_UNAVAILABLE -> {
|
||||||
|
errorCode = ErrorCode.BillingUnavailable
|
||||||
|
}
|
||||||
|
|
||||||
|
DEVELOPER_ERROR, FEATURE_NOT_SUPPORTED, ITEM_NOT_OWNED -> {
|
||||||
|
errorCode = ErrorCode.BillingError
|
||||||
|
}
|
||||||
|
|
||||||
|
ITEM_ALREADY_OWNED -> {
|
||||||
|
errorCode = ErrorCode.SubscriptionAlreadyOwned
|
||||||
|
}
|
||||||
|
|
||||||
|
ITEM_UNAVAILABLE -> {
|
||||||
|
errorCode = ErrorCode.SubscriptionUnavailable
|
||||||
|
}
|
||||||
|
|
||||||
|
NETWORK_ERROR -> {
|
||||||
|
errorCode = ErrorCode.BillingNetworkError
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
errorCode = ErrorCode.BillingError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
187
client/android/billing/src/main/kotlin/BillingProvider.kt
Normal file
187
client/android/billing/src/main/kotlin/BillingProvider.kt
Normal file
|
|
@ -0,0 +1,187 @@
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import com.android.billingclient.api.BillingClient
|
||||||
|
import com.android.billingclient.api.BillingClient.BillingResponseCode
|
||||||
|
import com.android.billingclient.api.BillingClient.ProductType
|
||||||
|
import com.android.billingclient.api.BillingClientStateListener
|
||||||
|
import com.android.billingclient.api.BillingFlowParams
|
||||||
|
import com.android.billingclient.api.BillingResult
|
||||||
|
import com.android.billingclient.api.GetBillingConfigParams
|
||||||
|
import com.android.billingclient.api.PendingPurchasesParams
|
||||||
|
import com.android.billingclient.api.Purchase
|
||||||
|
import com.android.billingclient.api.PurchasesUpdatedListener
|
||||||
|
import com.android.billingclient.api.QueryProductDetailsParams
|
||||||
|
import com.android.billingclient.api.QueryProductDetailsParams.Product
|
||||||
|
import com.android.billingclient.api.queryProductDetails
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.amnezia.vpn.util.Log
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
private const val TAG = "BillingProvider"
|
||||||
|
private const val RESULT_OK = 1
|
||||||
|
private const val RESULT_CANCELED = 0
|
||||||
|
private const val RESULT_ERROR = -1
|
||||||
|
|
||||||
|
class BillingProvider(context: Context) : AutoCloseable {
|
||||||
|
|
||||||
|
private var billingClient: BillingClient
|
||||||
|
private var subscriptionPurchases = MutableStateFlow<Pair<BillingResult, List<Purchase>?>?>(null)
|
||||||
|
|
||||||
|
private val purchasesUpdatedListeners = PurchasesUpdatedListener { billingResult, purchases ->
|
||||||
|
Log.v(TAG, "PurchasesUpdatedListener: $billingResult")
|
||||||
|
subscriptionPurchases.value = billingResult to purchases
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
billingClient = BillingClient.newBuilder(context)
|
||||||
|
.setListener(purchasesUpdatedListeners)
|
||||||
|
.enablePendingPurchases(PendingPurchasesParams.newBuilder().enableOneTimeProducts().build())
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun connect() {
|
||||||
|
if (billingClient.isReady) return
|
||||||
|
|
||||||
|
Log.v(TAG, "Connect to Google Play")
|
||||||
|
val connection = CompletableDeferred<Unit>()
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
billingClient.startConnection(object : BillingClientStateListener {
|
||||||
|
override fun onBillingSetupFinished(billingResult: BillingResult) {
|
||||||
|
Log.v(TAG, "Billing setup finished: $billingResult")
|
||||||
|
if (billingResult.isOk) {
|
||||||
|
connection.complete(Unit)
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Billing setup failed: $billingResult")
|
||||||
|
connection.completeExceptionally(BillingException(billingResult))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBillingServiceDisconnected() {
|
||||||
|
Log.w(TAG, "Billing service disconnected")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
connection.await()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun handleBillingApiCall(block: suspend () -> JSONObject): JSONObject =
|
||||||
|
try {
|
||||||
|
block()
|
||||||
|
} catch (e: BillingException) {
|
||||||
|
if (e.isCanceled) {
|
||||||
|
Log.w(TAG, "Billing canceled")
|
||||||
|
JSONObject().put("result", RESULT_CANCELED)
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Billing error: $e")
|
||||||
|
JSONObject()
|
||||||
|
.put("result", RESULT_ERROR)
|
||||||
|
.put("errorCode", e.errorCode)
|
||||||
|
}
|
||||||
|
} catch (_: CancellationException) {
|
||||||
|
Log.w(TAG, "Billing coroutine canceled")
|
||||||
|
JSONObject().put("result", RESULT_CANCELED)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getSubscriptionPlans(): JSONObject = handleBillingApiCall {
|
||||||
|
Log.v(TAG, "Get subscription plans")
|
||||||
|
|
||||||
|
val productDetailsParams = Product.newBuilder()
|
||||||
|
.setProductId("premium")
|
||||||
|
.setProductType(ProductType.SUBS)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val queryProductDetailsParams = QueryProductDetailsParams.newBuilder()
|
||||||
|
.setProductList(listOf(productDetailsParams))
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val result = withContext(Dispatchers.IO) {
|
||||||
|
billingClient.queryProductDetails(queryProductDetailsParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.billingResult.isOk) {
|
||||||
|
Log.e(TAG, "Failed to get subscription plans: ${result.billingResult}")
|
||||||
|
throw BillingException(result.billingResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.v(TAG, "Subscription plans:\n${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 {
|
||||||
|
Log.v(TAG, "Get customer country code")
|
||||||
|
|
||||||
|
val deferred = CompletableDeferred<String>()
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
billingClient.getBillingConfigAsync(GetBillingConfigParams.newBuilder().build(),
|
||||||
|
{ billingResult, billingConfig ->
|
||||||
|
Log.v(TAG, "Billing config: $billingResult, ${billingConfig?.countryCode}")
|
||||||
|
if (billingResult.isOk) {
|
||||||
|
deferred.complete(billingConfig?.countryCode ?: "")
|
||||||
|
} else {
|
||||||
|
deferred.completeExceptionally(BillingException(billingResult))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
val countryCode = deferred.await()
|
||||||
|
|
||||||
|
JSONObject()
|
||||||
|
.put("result", RESULT_OK)
|
||||||
|
.put("countryCode", countryCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun purchaseSubscription(activity: Activity, obfuscatedAccountId: String): JSONObject =
|
||||||
|
handleBillingApiCall {
|
||||||
|
Log.v(TAG, "Purchase subscription")
|
||||||
|
billingClient.launchBillingFlow(activity, BillingFlowParams.newBuilder()
|
||||||
|
.setObfuscatedAccountId(obfuscatedAccountId)
|
||||||
|
.build())
|
||||||
|
JSONObject()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
Log.d(TAG, "Close billing client connection")
|
||||||
|
billingClient.endConnection()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
suspend fun withBillingProvider(context: Context, block: suspend BillingProvider.() -> JSONObject): String =
|
||||||
|
BillingProvider(context).use { bp ->
|
||||||
|
bp.handleBillingApiCall {
|
||||||
|
bp.connect()
|
||||||
|
bp.block()
|
||||||
|
}.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal val BillingResult.isOk: Boolean
|
||||||
|
get() = responseCode == BillingResponseCode.OK
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
package org.amnezia.vpn
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
|
||||||
|
class BillingPaymentRepository(@Suppress("UNUSED_PARAMETER") context: Context) : BillingRepository {
|
||||||
|
override suspend fun getCountryCode(): String = ""
|
||||||
|
override suspend fun getSubscriptionPlans(): String = ""
|
||||||
|
override suspend fun purchaseSubscription(activity: Activity): String = ""
|
||||||
|
}
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
package org.amnezia.vpn
|
|
||||||
|
|
||||||
class BillingProvider {
|
|
||||||
fun type(): String {
|
|
||||||
return "OSS"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package org.amnezia.vpn
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import BillingProvider.Companion.withBillingProvider
|
||||||
|
|
||||||
|
class BillingPaymentRepository(private val context: Context) : BillingRepository {
|
||||||
|
|
||||||
|
override suspend fun getCountryCode(): String = withBillingProvider(context) {
|
||||||
|
getCustomerCountryCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getSubscriptionPlans(): String = withBillingProvider(context) {
|
||||||
|
getSubscriptionPlans()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun purchaseSubscription(activity: Activity): String = withBillingProvider(context) {
|
||||||
|
purchaseSubscription(activity, "obfuscatedAccountId")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
package org.amnezia.vpn
|
|
||||||
|
|
||||||
class BillingProvider {
|
|
||||||
fun type(): String {
|
|
||||||
return "PLAY"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -70,6 +70,7 @@ class AmneziaActivity : QtActivity() {
|
||||||
private var isInBoundState = false
|
private var isInBoundState = false
|
||||||
private var notificationStateReceiver: BroadcastReceiver? = null
|
private var notificationStateReceiver: BroadcastReceiver? = null
|
||||||
private lateinit var vpnServiceMessenger: IpcMessenger
|
private lateinit var vpnServiceMessenger: IpcMessenger
|
||||||
|
private lateinit var billingRepository: BillingRepository
|
||||||
|
|
||||||
private val actionResultHandlers = mutableMapOf<Int, ActivityResultHandler>()
|
private val actionResultHandlers = mutableMapOf<Int, ActivityResultHandler>()
|
||||||
private val permissionRequestHandlers = mutableMapOf<Int, PermissionRequestHandler>()
|
private val permissionRequestHandlers = mutableMapOf<Int, PermissionRequestHandler>()
|
||||||
|
|
@ -157,7 +158,6 @@ class AmneziaActivity : QtActivity() {
|
||||||
* Activity overloaded methods
|
* Activity overloaded methods
|
||||||
*/
|
*/
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
Log.d(TAG, "Billing provider: ${BillingProvider().type()}")
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
Log.d(TAG, "Create Amnezia activity: $intent")
|
Log.d(TAG, "Create Amnezia activity: $intent")
|
||||||
loadLibs()
|
loadLibs()
|
||||||
|
|
@ -180,6 +180,7 @@ class AmneziaActivity : QtActivity() {
|
||||||
registerBroadcastReceivers()
|
registerBroadcastReceivers()
|
||||||
intent?.let(::processIntent)
|
intent?.let(::processIntent)
|
||||||
runBlocking { vpnProto = proto.await() }
|
runBlocking { vpnProto = proto.await() }
|
||||||
|
billingRepository = BillingPaymentRepository(applicationContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadLibs() {
|
private fun loadLibs() {
|
||||||
|
|
@ -724,6 +725,26 @@ class AmneziaActivity : QtActivity() {
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
fun isPlay(): Boolean = BuildConfig.FLAVOR == "play"
|
fun isPlay(): Boolean = BuildConfig.FLAVOR == "play"
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
fun getCountryCode(): String {
|
||||||
|
Log.v(TAG, "Get country code")
|
||||||
|
return runBlocking {
|
||||||
|
mainScope.async {
|
||||||
|
billingRepository.getCountryCode()
|
||||||
|
}.await()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
fun getSubscriptionPlans(): String {
|
||||||
|
Log.v(TAG, "Get subscription plans")
|
||||||
|
return runBlocking {
|
||||||
|
mainScope.async {
|
||||||
|
billingRepository.getSubscriptionPlans()
|
||||||
|
}.await()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utils methods
|
* Utils methods
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
9
client/android/src/org/amnezia/vpn/BillingRepository.kt
Normal file
9
client/android/src/org/amnezia/vpn/BillingRepository.kt
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
package org.amnezia.vpn
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
|
||||||
|
interface BillingRepository {
|
||||||
|
suspend fun getCountryCode(): String
|
||||||
|
suspend fun getSubscriptionPlans(): String
|
||||||
|
suspend fun purchaseSubscription(activity: Activity): String
|
||||||
|
}
|
||||||
11
client/android/utils/src/main/kotlin/ErrorCode.kt
Normal file
11
client/android/utils/src/main/kotlin/ErrorCode.kt
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
package org.amnezia.vpn.util
|
||||||
|
|
||||||
|
// keep synchronized with client/core/defs.h error_code_ns::ErrorCode
|
||||||
|
object ErrorCode {
|
||||||
|
const val BillingError = 1300
|
||||||
|
const val BillingGooglePlayError = 1301
|
||||||
|
const val BillingUnavailable = 1302
|
||||||
|
const val SubscriptionAlreadyOwned = 1303
|
||||||
|
const val SubscriptionUnavailable = 1304
|
||||||
|
const val BillingNetworkError = 1305
|
||||||
|
}
|
||||||
|
|
@ -114,7 +114,15 @@ namespace amnezia
|
||||||
PermissionsError = 1202,
|
PermissionsError = 1202,
|
||||||
UnspecifiedError = 1203,
|
UnspecifiedError = 1203,
|
||||||
FatalError = 1204,
|
FatalError = 1204,
|
||||||
AbortError = 1205
|
AbortError = 1205,
|
||||||
|
|
||||||
|
// Billing errors
|
||||||
|
BillingError = 1300,
|
||||||
|
BillingGooglePlayError = 1301,
|
||||||
|
BillingUnavailable = 1302,
|
||||||
|
SubscriptionAlreadyOwned = 1303,
|
||||||
|
SubscriptionUnavailable = 1304,
|
||||||
|
BillingNetworkError = 1305,
|
||||||
};
|
};
|
||||||
Q_ENUM_NS(ErrorCode)
|
Q_ENUM_NS(ErrorCode)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,14 @@ QString errorString(ErrorCode code) {
|
||||||
case(ErrorCode::FatalError): errorMessage = QObject::tr("QFile error: A fatal error occurred"); break;
|
case(ErrorCode::FatalError): errorMessage = QObject::tr("QFile error: A fatal error occurred"); break;
|
||||||
case(ErrorCode::AbortError): errorMessage = QObject::tr("QFile error: The operation was aborted"); break;
|
case(ErrorCode::AbortError): errorMessage = QObject::tr("QFile error: The operation was aborted"); break;
|
||||||
|
|
||||||
|
// Billing errors
|
||||||
|
case(ErrorCode::BillingError): errorMessage = QObject::tr("Billing error"); break;
|
||||||
|
case(ErrorCode::BillingGooglePlayError): errorMessage = QObject::tr("Internal Google Play error, please try again later"); break;
|
||||||
|
case(ErrorCode::BillingUnavailable): errorMessage = QObject::tr("Billing is unavailable, please try again later"); break;
|
||||||
|
case(ErrorCode::SubscriptionAlreadyOwned): errorMessage = QObject::tr("You already own this subscription"); break;
|
||||||
|
case(ErrorCode::SubscriptionUnavailable): errorMessage = QObject::tr("The requested subscription is not available for purchase"); break;
|
||||||
|
case(ErrorCode::BillingNetworkError): errorMessage = QObject::tr("A network error occurred during the operation, please check the Internet connection"); break;
|
||||||
|
|
||||||
case(ErrorCode::InternalError):
|
case(ErrorCode::InternalError):
|
||||||
default:
|
default:
|
||||||
errorMessage = QObject::tr("Internal error"); break;
|
errorMessage = QObject::tr("Internal error"); break;
|
||||||
|
|
|
||||||
|
|
@ -292,6 +292,13 @@ bool AndroidController::isPlay()
|
||||||
return callActivityMethod<jboolean>("isPlay", "()Z");
|
return callActivityMethod<jboolean>("isPlay", "()Z");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QJsonObject AndroidController::getSubscriptionPlans()
|
||||||
|
{
|
||||||
|
QJniObject subscriptionPlans = callActivityMethod<jstring>("getSubscriptionPlans", "()Ljava/lang/String;");
|
||||||
|
QJsonObject json = QJsonDocument::fromJson(subscriptionPlans.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;
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ public:
|
||||||
void requestNotificationPermission();
|
void requestNotificationPermission();
|
||||||
bool requestAuthentication();
|
bool requestAuthentication();
|
||||||
bool isPlay();
|
bool isPlay();
|
||||||
|
QJsonObject getSubscriptionPlans();
|
||||||
|
|
||||||
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