Revision control
++ // Copyright (C) 2021 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
++
++ #include <QDebug>
++
++ #include <QJniEnvironment>
++ #include <QtCore>
++
++ #include "androidinapppurchasebackend.h"
++ #include "androidinappproduct.h"
++ #include "androidinapptransaction.h"
++ #include "../inapp/inappstore.h"
++
++
++ AndroidInAppPurchaseBackend::AndroidInAppPurchaseBackend(QObject *parent)
++ : InAppPurchaseBackend(parent)
++ , m_isReady(false)
++ {
++ m_javaObject = QJniObject("org/qtproject/qt/android/purchasing/InAppPurchase",
++ QNativeInterface::QAndroidApplication::context(),
++ jlong(this));
++
++ if (!m_javaObject.isValid()) {
++ qWarning("Cannot initialize IAP backend for Android due to missing dependency: InAppPurchase class");
++ return;
++ }
++ }
++
++ void AndroidInAppPurchaseBackend::initialize()
++ {
++ m_javaObject.callMethod<void>("initializeConnection");
++ }
++
++ bool AndroidInAppPurchaseBackend::isReady() const
++ {
++ QMutexLocker locker(&m_mutex);
++ return m_isReady;
++ }
++
++ void AndroidInAppPurchaseBackend::restorePurchases()
++ {
++ for (const QString &purchasedUnlockeble : purchasedUnlockebles) {
++ InAppProduct *product = store()->registeredProduct(purchasedUnlockeble);
++ Q_ASSERT(product != 0);
++
++ checkFinalizationStatus(product, InAppTransaction::PurchaseRestored);
++ }
++
++ }
++
++ void AndroidInAppPurchaseBackend::queryProducts(const QList<Product> &products)
++ {
++ QMutexLocker locker(&m_mutex);
++ QJniEnvironment environment;
++
++ QStringList newProducts;
++ for (int i = 0; i < products.size(); ++i) {
++ const Product &product = products.at(i);
++ if (m_productTypeForPendingId.contains(product.identifier)) {
++ qWarning("Product query already pending for %s", qPrintable(product.identifier));
++ continue;
++ }
++ else{
++ m_productTypeForPendingId[product.identifier] = product.productType;
++ newProducts.append(product.identifier);
++ }
++ }
++
++ if (newProducts.isEmpty())
++ return;
++
++ jclass cls = environment->FindClass("java/lang/String");
++ jobjectArray productIds = environment->NewObjectArray(newProducts.size(), cls, 0);
++ environment->DeleteLocalRef(cls);
++
++ for (int i = 0; i < newProducts.size(); ++i) {
++ QJniObject identifier = QJniObject::fromString(newProducts.at(i));
++ environment->SetObjectArrayElement(productIds, i, identifier.object());
++ }
++
++ m_javaObject.callMethod<void>("queryDetails",
++ "([Ljava/lang/String;)V",
++ productIds);
++ environment->DeleteLocalRef(productIds);
++ }
++
++ void AndroidInAppPurchaseBackend::queryProduct(InAppProduct::ProductType productType,
++ const QString &identifier)
++ {
++ queryProducts(QList<Product>() << Product(productType, identifier));
++ }
++
++ void AndroidInAppPurchaseBackend::setPlatformProperty(const QString &propertyName, const QString &value)
++ {
++ QMutexLocker locker(&m_mutex);
++ if (propertyName.compare(QStringLiteral("AndroidPublicKey"), Qt::CaseInsensitive) == 0) {
++ m_javaObject.callMethod<void>("setPublicKey",
++ "(Ljava/lang/String;)V",
++ QJniObject::fromString(value).object<jstring>());
++ }
++ }
++
++ void AndroidInAppPurchaseBackend::consumeTransaction(const QString &purchaseToken)
++ {
++ QMutexLocker locker(&m_mutex);
++ m_javaObject.callMethod<void>("consumePurchase",
++ "(Ljava/lang/String;)V",
++ QJniObject::fromString(purchaseToken).object<jstring>());
++ }
++
++ void AndroidInAppPurchaseBackend::registerFinalizedUnlockable(const QString &purchaseToken)
++ {
++ QMutexLocker locker(&m_mutex);
++ m_javaObject.callMethod<void>("acknowledgeUnlockablePurchase",
++ "(Ljava/lang/String;)V",
++ QJniObject::fromString(purchaseToken).object<jstring>());
++ }
++
++ bool AndroidInAppPurchaseBackend::transactionFinalizedForProduct(InAppProduct *product)
++ {
++ Q_ASSERT(m_infoForPurchase.contains(product->identifier()));
++
++ if (product->productType() != InAppProduct::Consumable && purchasedUnlockebles.contains(product->identifier()))
++ {
++ return true;
++ }
++ return false;
++ }
++
++ void AndroidInAppPurchaseBackend::checkFinalizationStatus(InAppProduct *product,
++ InAppTransaction::TransactionStatus status)
++ {
++ // Verifies the finalization status of an item based on the following logic:
++ // 1. If the item is not purchased yet, do nothing (it's either never been purchased, or it's a
++ // consumed consumable.
++ // 2. If the item is purchased, and it's a consumable, it's unfinalized. Emit a new transaction.
++ // Consumable items are consumed when they are finalized.
++ // 3. If the item is purchased, and it's an unlockable, check the local cache for finalized
++ // unlockable purchases. If it's not there, then the transaction is unfinalized. This means
++ // that if the cache gets deleted or corrupted, the worst-case scenario is that the transactions
++ // are republished.
++ QHash<QString, PurchaseInfo>::iterator it = m_infoForPurchase.find(product->identifier());
++
++ if (it == m_infoForPurchase.end()) {
++ return;
++ }
++
++ const PurchaseInfo &info = it.value();
++ if (transactionFinalizedForProduct(product)) {
++ AndroidInAppTransaction *transaction = new AndroidInAppTransaction(info.signature,
++ info.data,
++ info.purchaseToken,
++ info.orderId,
++ status,
++ product,
++ info.timestamp,
++ InAppTransaction::NoFailure,
++ QString(),
++ this);
++ emit transactionReady(transaction);
++ }
++ }
++
++ void AndroidInAppPurchaseBackend::registerProduct(const QString &productId,
++ const QString &price,
++ const QString &title,
++ const QString &description)
++ {
++ QMutexLocker locker(&m_mutex);
++ QHash<QString, InAppProduct::ProductType>::iterator it = m_productTypeForPendingId.find(productId);
++ Q_ASSERT(it != m_productTypeForPendingId.end());
++
++ AndroidInAppProduct *product = new AndroidInAppProduct(this, price, title, description, it.value(), it.key(), this);
++
++ emit productQueryDone(product);
++ }
++
++ void AndroidInAppPurchaseBackend::registerPurchased(const QString &identifier,
++ const QString &signature,
++ const QString &data,
++ const QString &purchaseToken,
++ const QString &orderId,
++ const QDateTime ×tamp)
++ {
++ QMutexLocker locker(&m_mutex);
++ m_infoForPurchase.insert(identifier, PurchaseInfo(signature, data, purchaseToken, orderId, timestamp));
++
++ QHash<QString, InAppProduct::ProductType>::iterator it = m_productTypeForPendingId.find(identifier);
++ if (it.value() == InAppProduct::Unlockable && !purchasedUnlockebles.contains(identifier))
++ purchasedUnlockebles.append(identifier);
++
++
++ }
++
++ void AndroidInAppPurchaseBackend::registerReady()
++ {
++ QMutexLocker locker(&m_mutex);
++ m_isReady = true;
++ emit ready();
++ }
++
++ void AndroidInAppPurchaseBackend::purchaseProduct(AndroidInAppProduct *product)
++ {
++ QMutexLocker locker(&m_mutex);
++ if (!m_javaObject.isValid()) {
++ purchaseFailed(product, InAppTransaction::ErrorOccurred, QStringLiteral("Java backend is not initialized"));
++ return;
++ }
++
++ int requestCode = 0;
++ while (m_activePurchaseRequests.contains(requestCode)) {
++ requestCode++;
++ if (requestCode == 0) {
++ qWarning("No available request code for purchase request.");
++ return;
++ }
++ }
++
++ m_activePurchaseRequests[requestCode] = product;
++
++ m_javaObject.callMethod<void>("launchBillingFlow",
++ "(Ljava/lang/String;I)V",
++ QJniObject::fromString(product->identifier()).object<jstring>(),
++ requestCode);
++ }
++
++ void AndroidInAppPurchaseBackend::purchaseFailed(int requestCode, int failureReason, const QString &errorString)
++ {
++ QMutexLocker locker(&m_mutex);
++ InAppProduct *product = m_activePurchaseRequests.take(requestCode);
++ if (product == 0) {
++ qWarning("No product registered for requestCode %d", requestCode);
++ return;
++ }
++
++ purchaseFailed(product, failureReason, errorString);
++ }
++
++ void AndroidInAppPurchaseBackend::purchaseFailed(InAppProduct *product, int failureReason, const QString &errorString)
++ {
++ InAppTransaction *transaction = new AndroidInAppTransaction(QString(),
++ QString(),
++ QString(),
++ QString(),
++ InAppTransaction::PurchaseFailed,
++ product,
++ QDateTime(),
++ InAppTransaction::FailureReason(failureReason),
++ errorString,
++ this);
++ emit transactionReady(transaction);
++ }
++
++ void AndroidInAppPurchaseBackend::purchaseSucceeded(int requestCode,
++ const QString &signature,
++ const QString &data,
++ const QString &purchaseToken,
++ const QString &orderId,
++ const QDateTime ×tamp)
++
++ {
++ QMutexLocker locker(&m_mutex);
++ InAppProduct *product = m_activePurchaseRequests.take(requestCode);
++ if (product == 0) {
++ qWarning("No product registered for requestCode %d", requestCode);
++ return;
++ }
++
++ m_infoForPurchase.insert(product->identifier(), PurchaseInfo(signature, data, purchaseToken, orderId, timestamp));
++ InAppTransaction *transaction = new AndroidInAppTransaction(signature,
++ data,
++ purchaseToken,
++ orderId,
++ InAppTransaction::PurchaseApproved,
++ product,
++ timestamp,
++ InAppTransaction::NoFailure,
++ QString(),
++ this);
++ emit transactionReady(transaction);
++ }