| /**************************************************************************** |
| ** |
| ** Copyright (C) 2015 The Qt Company Ltd. |
| ** Contact: http://www.qt.io/licensing/ |
| ** |
| ** This file is part of the Purchasing module of the Qt Toolkit. |
| ** |
| ** $QT_BEGIN_LICENSE:LGPL3-COMM$ |
| ** Commercial License Usage |
| ** Licensees holding valid commercial Qt licenses may use this file in |
| ** accordance with the commercial license agreement provided with the |
| ** Software or, alternatively, in accordance with the terms contained in |
| ** a written agreement between you and The Qt Company. For licensing terms |
| ** and conditions see http://www.qt.io/terms-conditions. For further |
| ** information use the contact form at http://www.qt.io/contact-us. |
| ** |
| ** GNU Lesser General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU Lesser |
| ** General Public License version 3 as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.LGPLv3 included in the |
| ** packaging of this file. Please review the following information to |
| ** ensure the GNU Lesser General Public License version 3 requirements |
| ** will be met: https://www.gnu.org/licenses/lgpl.html. |
| ** |
| ** $QT_END_LICENSE$ |
| ** |
| ****************************************************************************/ |
| |
| #include "qandroidinapppurchasebackend_p.h" |
| #include "qandroidinappproduct_p.h" |
| #include "qandroidinapptransaction_p.h" |
| #include "qinappstore.h" |
| |
| #include <QtAndroidExtras/qandroidfunctions.h> |
| #include <QtAndroidExtras/qandroidjnienvironment.h> |
| #include <QtCore/qfile.h> |
| #include <QtCore/qfileinfo.h> |
| #include <QtCore/qdir.h> |
| #include <QtCore/qdatastream.h> |
| #include <QtCore/qstandardpaths.h> |
| |
| QT_BEGIN_NAMESPACE |
| |
| // #define QANDROIDINAPPPURCHASEBACKEND_DEBUG |
| |
| QAndroidInAppPurchaseBackend::QAndroidInAppPurchaseBackend(QObject *parent) |
| : QInAppPurchaseBackend(parent) |
| , m_isReady(false) |
| { |
| #if defined(QANDROIDINAPPPURCHASEBACKEND_DEBUG) |
| qDebug("Creating backend"); |
| #endif |
| |
| m_javaObject = QAndroidJniObject("org/qtproject/qt5/android/purchasing/QtInAppPurchase", |
| "(Landroid/content/Context;J)V", |
| QtAndroid::androidActivity().object<jobject>(), |
| reinterpret_cast<jlong>(this)); |
| if (!m_javaObject.isValid()) { |
| qWarning("Cannot initialize IAP backend for Android due to missing dependency: QtInAppPurchase class"); |
| return; |
| } |
| } |
| |
| QString QAndroidInAppPurchaseBackend::finalizedUnlockableFileName() const |
| { |
| QString path = QStandardPaths::writableLocation(QStandardPaths::DataLocation); |
| return path + QStringLiteral("/.qt-purchasing-data/iap_finalization.data"); |
| } |
| |
| void QAndroidInAppPurchaseBackend::initialize() |
| { |
| #if defined(QANDROIDINAPPPURCHASEBACKEND_DEBUG) |
| qDebug("Initializing backend"); |
| #endif |
| |
| m_javaObject.callMethod<void>("initializeConnection"); |
| |
| QFile file(finalizedUnlockableFileName()); |
| if (file.open(QIODevice::ReadOnly)) { |
| QDataStream stream(&file); |
| while (!stream.atEnd()) { |
| QString identifier; |
| stream >> identifier; |
| m_finalizedUnlockableProducts.insert(identifier); |
| |
| #if defined(QANDROIDINAPPPURCHASEBACKEND_DEBUG) |
| qDebug("Finalized unlockable: %s", qPrintable(identifier)); |
| #endif |
| |
| } |
| |
| } else if (file.exists()) { |
| qWarning("Failed to read from finalization data."); |
| } |
| } |
| |
| bool QAndroidInAppPurchaseBackend::isReady() const |
| { |
| QMutexLocker locker(&m_mutex); |
| return m_isReady; |
| } |
| |
| void QAndroidInAppPurchaseBackend::restorePurchases() |
| { |
| const QSet<QString> previouslyFinalizedUnlockables = std::move(m_finalizedUnlockableProducts); |
| m_finalizedUnlockableProducts.clear(); |
| for (const QString &previouslyFinalizedUnlockable : previouslyFinalizedUnlockables) { |
| QInAppProduct *product = store()->registeredProduct(previouslyFinalizedUnlockable); |
| Q_ASSERT(product != 0); |
| |
| checkFinalizationStatus(product, QInAppTransaction::PurchaseRestored); |
| } |
| } |
| |
| void QAndroidInAppPurchaseBackend::queryProducts(const QList<Product> &products) |
| { |
| QMutexLocker locker(&m_mutex); |
| QAndroidJniEnvironment 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; |
| } |
| |
| 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) { |
| QAndroidJniObject identifier = QAndroidJniObject::fromString(newProducts.at(i)); |
| environment->SetObjectArrayElement(productIds, i, identifier.object()); |
| } |
| |
| m_javaObject.callMethod<void>("queryDetails", |
| "([Ljava/lang/String;)V", |
| productIds); |
| environment->DeleteLocalRef(productIds); |
| } |
| |
| void QAndroidInAppPurchaseBackend::queryProduct(QInAppProduct::ProductType productType, |
| const QString &identifier) |
| { |
| #if defined(QANDROIDINAPPPURCHASEBACKEND_DEBUG) |
| qDebug("Querying product: %s (%d)", qPrintable(identifier), productType); |
| #endif |
| |
| queryProducts(QList<Product>() << Product(productType, identifier)); |
| } |
| |
| void QAndroidInAppPurchaseBackend::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", |
| QAndroidJniObject::fromString(value).object<jstring>()); |
| } |
| } |
| |
| void QAndroidInAppPurchaseBackend::registerQueryFailure(const QString &productId) |
| { |
| #if defined(QANDROIDINAPPPURCHASEBACKEND_DEBUG) |
| qDebug("Query failed for %s", qPrintable(productId)); |
| #endif |
| |
| QMutexLocker locker(&m_mutex); |
| QHash<QString, QInAppProduct::ProductType>::iterator it = m_productTypeForPendingId.find(productId); |
| Q_ASSERT(it != m_productTypeForPendingId.end()); |
| |
| QInAppProduct::ProductType productType = it.value(); |
| m_productTypeForPendingId.erase(it); |
| emit productQueryFailed(productType, productId); |
| } |
| |
| void QAndroidInAppPurchaseBackend::consumeTransaction(const QString &purchaseToken) |
| { |
| #if defined(QANDROIDINAPPPURCHASEBACKEND_DEBUG) |
| qDebug("Transaction consumed for %s", qPrintable(purchaseToken)); |
| #endif |
| |
| QMutexLocker locker(&m_mutex); |
| m_javaObject.callMethod<void>("consumePurchase", |
| "(Ljava/lang/String;)V", |
| QAndroidJniObject::fromString(purchaseToken).object<jstring>()); |
| } |
| |
| void QAndroidInAppPurchaseBackend::registerFinalizedUnlockable(const QString &identifier) |
| { |
| #if defined(QANDROIDINAPPPURCHASEBACKEND_DEBUG) |
| qDebug("Finalizing unlockable %s", qPrintable(identifier)); |
| #endif |
| |
| QMutexLocker locker(&m_mutex); |
| m_finalizedUnlockableProducts.insert(identifier); |
| |
| QString fileName = finalizedUnlockableFileName(); |
| QDir().mkpath(QFileInfo(fileName).absolutePath()); |
| |
| QFile file(fileName); |
| if (!file.open(QIODevice::WriteOnly)) { |
| qWarning("Failed to open file to store finalization info."); |
| return; |
| } |
| |
| QDataStream stream(&file); |
| for (const QString &finalizedUnlockableProduct : qAsConst(m_finalizedUnlockableProducts)) |
| stream << finalizedUnlockableProduct; |
| } |
| |
| bool QAndroidInAppPurchaseBackend::transactionFinalizedForProduct(QInAppProduct *product) |
| { |
| Q_ASSERT(m_infoForPurchase.contains(product->identifier())); |
| return product->productType() != QInAppProduct::Consumable |
| && m_finalizedUnlockableProducts.contains(product->identifier()); |
| } |
| |
| void QAndroidInAppPurchaseBackend::checkFinalizationStatus(QInAppProduct *product, |
| QInAppTransaction::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()) { |
| #if defined(QANDROIDINAPPPURCHASEBACKEND_DEBUG) |
| qDebug("Product %s not purchased", qPrintable(product->identifier())); |
| #endif |
| return; |
| } |
| |
| const PurchaseInfo &info = it.value(); |
| if (!transactionFinalizedForProduct(product)) { |
| #if defined(QANDROIDINAPPPURCHASEBACKEND_DEBUG) |
| qDebug("Product unfinalized: %s. Emitting transaction with status %d.", qPrintable(product->identifier()), status); |
| #endif |
| |
| QAndroidInAppTransaction *transaction = new QAndroidInAppTransaction(info.signature, |
| info.data, |
| info.purchaseToken, |
| info.orderId, |
| status, |
| product, |
| info.timestamp, |
| QInAppTransaction::NoFailure, |
| QString(), |
| this); |
| emit transactionReady(transaction); |
| } |
| } |
| |
| void QAndroidInAppPurchaseBackend::registerProduct(const QString &productId, |
| const QString &price, |
| const QString &title, |
| const QString &description) |
| { |
| #if defined(QANDROIDINAPPPURCHASEBACKEND_DEBUG) |
| qDebug("Registering product %s with price %s", qPrintable(productId), qPrintable(price)); |
| #endif |
| |
| QMutexLocker locker(&m_mutex); |
| QHash<QString, QInAppProduct::ProductType>::iterator it = m_productTypeForPendingId.find(productId); |
| Q_ASSERT(it != m_productTypeForPendingId.end()); |
| |
| QAndroidInAppProduct *product = new QAndroidInAppProduct(this, price, title, description, it.value(), it.key(), this); |
| checkFinalizationStatus(product); |
| |
| emit productQueryDone(product); |
| m_productTypeForPendingId.erase(it); |
| } |
| |
| void QAndroidInAppPurchaseBackend::registerPurchased(const QString &identifier, |
| const QString &signature, |
| const QString &data, |
| const QString &purchaseToken, |
| const QString &orderId, |
| const QDateTime ×tamp) |
| { |
| #if defined(QANDROIDINAPPPURCHASEBACKEND_DEBUG) |
| qDebug("Registering previously purchased product: %s", qPrintable(identifier)); |
| #endif |
| |
| QMutexLocker locker(&m_mutex); |
| m_infoForPurchase.insert(identifier, PurchaseInfo(signature, data, purchaseToken, orderId, timestamp)); |
| } |
| |
| void QAndroidInAppPurchaseBackend::registerReady() |
| { |
| QMutexLocker locker(&m_mutex); |
| m_isReady = true; |
| emit ready(); |
| } |
| |
| void QAndroidInAppPurchaseBackend::handleActivityResult(int requestCode, int resultCode, const QAndroidJniObject &data) |
| { |
| QInAppProduct *product = m_activePurchaseRequests.value(requestCode); |
| if (product == 0) { |
| qWarning("No product registered for requestCode %d", requestCode); |
| return; |
| } |
| |
| m_javaObject.callMethod<void>("handleActivityResult", "(IILandroid/content/Intent;Ljava/lang/String;)V", |
| requestCode, |
| resultCode, |
| data.object<jobject>(), |
| QAndroidJniObject::fromString(product->identifier()).object<jstring>()); |
| } |
| |
| void QAndroidInAppPurchaseBackend::purchaseProduct(QAndroidInAppProduct *product) |
| { |
| #if defined(QANDROIDINAPPPURCHASEBACKEND_DEBUG) |
| qDebug("Attempting to purchase %s", qPrintable(product->identifier())); |
| #endif |
| |
| QMutexLocker locker(&m_mutex); |
| if (!m_javaObject.isValid()) { |
| purchaseFailed(product, QInAppTransaction::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; |
| |
| QAndroidJniObject intentSender = m_javaObject.callObjectMethod("createBuyIntentSender", |
| "(Ljava/lang/String;I)Landroid/content/IntentSender;", |
| QAndroidJniObject::fromString(product->identifier()).object<jstring>(), requestCode); |
| |
| if (!intentSender.isValid()) { |
| m_activePurchaseRequests.remove(requestCode); |
| return; |
| } |
| |
| QtAndroid::startIntentSender(intentSender, requestCode, this); |
| } |
| |
| void QAndroidInAppPurchaseBackend::purchaseFailed(int requestCode, int failureReason, const QString &errorString) |
| { |
| QMutexLocker locker(&m_mutex); |
| QInAppProduct *product = m_activePurchaseRequests.take(requestCode); |
| if (product == 0) { |
| qWarning("No product registered for requestCode %d", requestCode); |
| return; |
| } |
| |
| purchaseFailed(product, failureReason, errorString); |
| } |
| |
| void QAndroidInAppPurchaseBackend::purchaseFailed(QInAppProduct *product, int failureReason, const QString &errorString) |
| { |
| #if defined(QANDROIDINAPPPURCHASEBACKEND_DEBUG) |
| qDebug("Purchase failed for %s", qPrintable(product->identifier())); |
| #endif |
| |
| QInAppTransaction *transaction = new QAndroidInAppTransaction(QString(), |
| QString(), |
| QString(), |
| QString(), |
| QInAppTransaction::PurchaseFailed, |
| product, |
| QDateTime(), |
| QInAppTransaction::FailureReason(failureReason), |
| errorString, |
| this); |
| emit transactionReady(transaction); |
| } |
| |
| void QAndroidInAppPurchaseBackend::purchaseSucceeded(int requestCode, |
| const QString &signature, |
| const QString &data, |
| const QString &purchaseToken, |
| const QString &orderId, |
| const QDateTime ×tamp) |
| |
| { |
| QMutexLocker locker(&m_mutex); |
| QInAppProduct *product = m_activePurchaseRequests.take(requestCode); |
| if (product == 0) { |
| qWarning("No product registered for requestCode %d", requestCode); |
| return; |
| } |
| |
| #if defined(QANDROIDINAPPPURCHASEBACKEND_DEBUG) |
| qDebug("Purchase succeeded for %s", qPrintable(product->identifier())); |
| #endif |
| |
| |
| m_infoForPurchase.insert(product->identifier(), PurchaseInfo(signature, data, purchaseToken, orderId, timestamp)); |
| QInAppTransaction *transaction = new QAndroidInAppTransaction(signature, |
| data, |
| purchaseToken, |
| orderId, |
| QInAppTransaction::PurchaseApproved, |
| product, |
| timestamp, |
| QInAppTransaction::NoFailure, |
| QString(), |
| this); |
| emit transactionReady(transaction); |
| } |
| |
| QT_END_NAMESPACE |