Browse Source

Android: Replaced deprecated AIDL in-app billing code with Google Play Billing library

tags/2021-05-28
ed 5 years ago
parent
commit
027e12e3a6
10 changed files with 570 additions and 682 deletions
  1. +14
    -1
      examples/Utilities/InAppPurchasesDemo.h
  2. +9
    -4
      extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Android.h
  3. +1
    -0
      modules/juce_core/system/juce_StandardHeader.h
  4. +0
    -4
      modules/juce_gui_basics/native/juce_android_Windowing.cpp
  5. +2
    -4
      modules/juce_product_unlocking/in_app_purchases/juce_InAppPurchases.cpp
  6. +21
    -9
      modules/juce_product_unlocking/in_app_purchases/juce_InAppPurchases.h
  7. +2
    -1
      modules/juce_product_unlocking/juce_product_unlocking.h
  8. +197
    -0
      modules/juce_product_unlocking/native/javacore/app/com/roli/juce/JuceBillingClient.java
  9. +323
    -658
      modules/juce_product_unlocking/native/juce_android_InAppPurchases.cpp
  10. +1
    -1
      modules/juce_product_unlocking/native/juce_ios_InAppPurchases.cpp

+ 14
- 1
examples/Utilities/InAppPurchasesDemo.h View File

@@ -52,6 +52,19 @@
#include "../Assets/DemoUtilities.h"
/*
To finish the setup of this demo, do the following in the Projucer project:
1. In the project settings, set the "Bundle Identifier" to com.roli.juceInAppPurchaseSample
2. In the Android exporter settings, change the following settings:
- "In-App Billing" - Enabled
- "Key Signing: key.store" - path to InAppPurchase.keystore file in examples/Assets/Signing
- "Key Signing: key.store.password" - amazingvoices
- "Key Signing: key-alias" - InAppPurchase
- "Key Signing: key.alias.password" - amazingvoices
3. Re-save the project
*/
//==============================================================================
class VoicePurchases : private InAppPurchases::Listener
{
@@ -108,7 +121,7 @@ public:
purchaseInProgress = true;
product.purchaseInProgress = true;
InAppPurchases::getInstance()->purchaseProduct (product.identifier, false);
InAppPurchases::getInstance()->purchaseProduct (product.identifier);
guiUpdater.triggerAsyncUpdate();
}


+ 9
- 4
extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Android.h View File

@@ -140,9 +140,9 @@ public:
androidKeyStorePass (settings, Ids::androidKeyStorePass, getUndoManager(), "android"),
androidKeyAlias (settings, Ids::androidKeyAlias, getUndoManager(), "androiddebugkey"),
androidKeyAliasPass (settings, Ids::androidKeyAliasPass, getUndoManager(), "android"),
gradleVersion (settings, Ids::gradleVersion, getUndoManager(), "4.10"),
gradleVersion (settings, Ids::gradleVersion, getUndoManager(), "5.4.1"),
gradleToolchain (settings, Ids::gradleToolchain, getUndoManager(), "clang"),
androidPluginVersion (settings, Ids::androidPluginVersion, getUndoManager(), "3.2.1"),
androidPluginVersion (settings, Ids::androidPluginVersion, getUndoManager(), "3.5.3"),
AndroidExecutable (getAppSettings().getStoredPath (Ids::androidStudioExePath, TargetOS::getThisOS()).get().toString())
{
name = getName();
@@ -840,6 +840,9 @@ private:
for (auto& d : StringArray::fromLines (androidJavaLibs.get().toString()))
mo << " implementation files('libs/" << File (d).getFileName() << "')" << newLine;
if (isInAppBillingEnabled())
mo << " implementation 'com.android.billingclient:billing:2.1.0'" << newLine;
if (areRemoteNotificationsEnabled())
{
mo << " implementation 'com.google.firebase:firebase-core:16.0.1'" << newLine;
@@ -1215,6 +1218,8 @@ private:
bool arePushNotificationsEnabled() const { return androidPushNotifications.get(); }
bool areRemoteNotificationsEnabled() const { return arePushNotificationsEnabled() && androidEnableRemoteNotifications.get(); }
bool isInAppBillingEnabled() const { return androidInAppBillingPermission.get(); }
String getJNIActivityClassName() const
{
return getActivityClassString().replaceCharacter ('.', '/');
@@ -1436,7 +1441,7 @@ private:
defines.set ("JUCE_PUSH_NOTIFICATIONS_ACTIVITY", getJNIActivityClassName().quoted());
}
if (androidInAppBillingPermission.get())
if (isInAppBillingEnabled())
defines.set ("JUCE_IN_APP_PURCHASES", "1");
if (supportsGLv3())
@@ -1820,7 +1825,7 @@ private:
if (androidExternalWritePermission.get())
s.add ("android.permission.WRITE_EXTERNAL_STORAGE");
if (androidInAppBillingPermission.get())
if (isInAppBillingEnabled())
s.add ("com.android.vending.BILLING");
if (androidVibratePermission.get())


+ 1
- 0
modules/juce_core/system/juce_StandardHeader.h View File

@@ -58,6 +58,7 @@
#include <unordered_set>
#include <mutex>
#include <condition_variable>
#include <queue>
//==============================================================================
#include "juce_CompilerSupport.h"


+ 0
- 4
modules/juce_gui_basics/native/juce_android_Windowing.cpp View File

@@ -196,10 +196,6 @@ static const uint8 javaComponentPeerView[] =
extern void juce_firebaseRemoteMessageSendError (void*, void*);
#endif
#if JUCE_IN_APP_PURCHASES && JUCE_MODULE_AVAILABLE_juce_product_unlocking
extern void juce_inAppPurchaseCompleted (void*);
#endif
extern void juce_contentSharingCompleted (int);
//==============================================================================


+ 2
- 4
modules/juce_product_unlocking/in_app_purchases/juce_InAppPurchases.cpp View File

@@ -61,13 +61,11 @@ void InAppPurchases::getProductsInformation (const StringArray& productIdentifie
}
void InAppPurchases::purchaseProduct (const String& productIdentifier,
bool isSubscription,
const StringArray& upgradeProductIdentifiers,
const String& upgradeProductIdentifier,
bool creditForUnusedSubscription)
{
#if JUCE_ANDROID || JUCE_IOS || JUCE_MAC
pimpl->purchaseProduct (productIdentifier, isSubscription,
upgradeProductIdentifiers, creditForUnusedSubscription);
pimpl->purchaseProduct (productIdentifier, upgradeProductIdentifier, creditForUnusedSubscription);
#else
Listener::PurchaseInfo purchaseInfo { Purchase { "", productIdentifier, {}, {}, {} }, {} };


+ 21
- 9
modules/juce_product_unlocking/in_app_purchases/juce_InAppPurchases.h View File

@@ -200,19 +200,15 @@ public:
@param productIdentifier The product identifier.
@param isSubscription (Android only) defines if a product a user wants to buy is a subscription or a one-time purchase.
On iOS, type of the product is derived implicitly.
@param upgradeOrDowngradeFromSubscriptionsWithProductIdentifiers (Android only) specifies subscriptions that will be replaced by the
one being purchased now. Used only when buying a subscription
that is an upgrade or downgrade from other ones.
@param upgradeOrDowngradeFromSubscriptionsWithProductIdentifier (Android only) specifies the subscription that will be replaced by
the one being purchased now. Used only when buying a subscription
that is an upgrade or downgrade from another.
@param creditForUnusedSubscription (Android only) controls whether a user should be credited for any unused subscription time on
the products that are being upgraded or downgraded.
the product that is being upgraded or downgraded.
*/
void purchaseProduct (const String& productIdentifier,
bool isSubscription,
const StringArray& upgradeOrDowngradeFromSubscriptionsWithProductIdentifiers = {},
const String& upgradeOrDowngradeFromSubscriptionWithProductIdentifier = {},
bool creditForUnusedSubscription = true);
/** Asynchronously asks about a list of products that a user has already bought. Upon completion, Listener::purchasesListReceived()
@@ -260,6 +256,22 @@ public:
/** iOS only: Cancels downloads of hosted content from the store. */
void cancelDownloads (const Array<Download*>& downloads);
//==============================================================================
// On Android, it is no longer necessary to specify whether the product being purchased is a subscription
// and only a single subscription can be upgraded/downgraded. Use the updated purchaseProduct() method
// which takes a single String argument.
JUCE_DEPRECATED_WITH_BODY (void purchaseProduct (const String& productIdentifier,
bool isSubscription,
const StringArray& upgradeOrDowngradeFromSubscriptionsWithProductIdentifiers = {},
bool creditForUnusedSubscription = true),
{
ignoreUnused (isSubscription);
purchaseProduct (productIdentifier,
upgradeOrDowngradeFromSubscriptionsWithProductIdentifiers[0],
creditForUnusedSubscription);
})
private:
//==============================================================================
#ifndef DOXYGEN


+ 2
- 1
modules/juce_product_unlocking/juce_product_unlocking.h View File

@@ -41,7 +41,7 @@
website: http://www.juce.com/juce
license: GPL/Commercial
dependencies: juce_cryptography juce_core
dependencies: juce_cryptography juce_core, juce_events
END_JUCE_MODULE_DECLARATION
@@ -68,6 +68,7 @@
//==============================================================================
#include <juce_core/juce_core.h>
#include <juce_cryptography/juce_cryptography.h>
#include <juce_events/juce_events.h>
#if JUCE_MODULE_AVAILABLE_juce_data_structures
#include <juce_data_structures/juce_data_structures.h>


+ 197
- 0
modules/juce_product_unlocking/native/javacore/app/com/roli/juce/JuceBillingClient.java View File

@@ -0,0 +1,197 @@
package com.roli.juce;
import android.content.Context;
import android.app.Activity;
import android.util.Log;
import com.android.billingclient.api.BillingClient;
import com.android.billingclient.api.BillingFlowParams;
import com.android.billingclient.api.BillingResult;
import com.android.billingclient.api.BillingClientStateListener;
import com.android.billingclient.api.ConsumeResponseListener;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.PurchasesUpdatedListener;
import com.android.billingclient.api.SkuDetails;
import com.android.billingclient.api.SkuDetailsParams;
import com.android.billingclient.api.SkuDetailsResponseListener;
import com.android.billingclient.api.AcknowledgePurchaseParams;
import com.android.billingclient.api.AcknowledgePurchaseResponseListener;
import com.android.billingclient.api.ConsumeParams;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class JuceBillingClient implements PurchasesUpdatedListener {
private native void skuDetailsQueryCallback(long host, List<SkuDetails> skuDetails);
private native void purchasesListQueryCallback(long host, List<Purchase> purchases);
private native void purchaseCompletedCallback(long host, Purchase purchase, int responseCode);
private native void purchaseConsumedCallback(long host, String productIdentifier, int responseCode);
public JuceBillingClient(Context context, long hostToUse) {
host = hostToUse;
billingClient = BillingClient.newBuilder(context)
.enablePendingPurchases()
.setListener(this)
.build();
billingClient.startConnection(null);
}
public void endConnection() {
billingClient.endConnection();
}
public boolean isReady() {
return billingClient.isReady();
}
public boolean isBillingSupported() {
return billingClient.isFeatureSupported(BillingClient.FeatureType.SUBSCRIPTIONS).getResponseCode()
== BillingClient.BillingResponseCode.OK;
}
public void querySkuDetails(final String[] skusToQuery) {
executeOnBillingClientConnection(new Runnable() {
@Override
public void run() {
final List<String> skuList = Arrays.asList(skusToQuery);
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder()
.setSkusList(skuList)
.setType(BillingClient.SkuType.INAPP);
billingClient.querySkuDetailsAsync(params.build(), new SkuDetailsResponseListener() {
@Override
public void onSkuDetailsResponse(BillingResult billingResult, final List<SkuDetails> inAppSkuDetails) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder()
.setSkusList(skuList)
.setType(BillingClient.SkuType.SUBS);
billingClient.querySkuDetailsAsync(params.build(), new SkuDetailsResponseListener() {
@Override
public void onSkuDetailsResponse(BillingResult billingResult, List<SkuDetails> subsSkuDetails) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
subsSkuDetails.addAll(inAppSkuDetails);
skuDetailsQueryCallback(host, subsSkuDetails);
}
}
});
}
}
});
}
});
}
public void launchBillingFlow(final Activity activity, final BillingFlowParams params) {
executeOnBillingClientConnection(new Runnable() {
@Override
public void run() {
BillingResult r = billingClient.launchBillingFlow(activity, params);
}
});
}
public void queryPurchases() {
executeOnBillingClientConnection(new Runnable() {
@Override
public void run() {
Purchase.PurchasesResult inAppPurchases = billingClient.queryPurchases(BillingClient.SkuType.INAPP);
Purchase.PurchasesResult subsPurchases = billingClient.queryPurchases(BillingClient.SkuType.SUBS);
if (inAppPurchases.getResponseCode() == BillingClient.BillingResponseCode.OK
&& subsPurchases.getResponseCode() == BillingClient.BillingResponseCode.OK) {
List<Purchase> purchaseList = inAppPurchases.getPurchasesList();
purchaseList.addAll(subsPurchases.getPurchasesList());
purchasesListQueryCallback(host, purchaseList);
return;
}
purchasesListQueryCallback(host, null);
}
});
}
public void consumePurchase(final String productIdentifier, final String purchaseToken) {
executeOnBillingClientConnection(new Runnable() {
@Override
public void run() {
ConsumeParams consumeParams = ConsumeParams.newBuilder()
.setPurchaseToken(purchaseToken)
.build();
billingClient.consumeAsync(consumeParams, new ConsumeResponseListener() {
@Override
public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
purchaseConsumedCallback(host, productIdentifier, billingResult.getResponseCode());
}
});
}
});
}
@Override
public void onPurchasesUpdated(BillingResult result, List<Purchase> purchases) {
int responseCode = result.getResponseCode();
if (purchases != null) {
for (Purchase purchase : purchases) {
handlePurchase(purchase, responseCode);
}
} else {
purchaseCompletedCallback(host, null, responseCode);
}
}
private void executeOnBillingClientConnection(Runnable runnable) {
if (billingClient.isReady()) {
runnable.run();
} else {
connectAndExecute(runnable);
}
}
private void connectAndExecute(final Runnable executeOnSuccess) {
billingClient.startConnection(new BillingClientStateListener() {
@Override
public void onBillingSetupFinished(BillingResult billingResponse) {
if (billingResponse.getResponseCode() == BillingClient.BillingResponseCode.OK) {
if (executeOnSuccess != null) {
executeOnSuccess.run();
}
}
}
@Override
public void onBillingServiceDisconnected() {
}
});
}
private void handlePurchase(final Purchase purchase, final int responseCode) {
purchaseCompletedCallback(host, purchase, responseCode);
if (responseCode == BillingClient.BillingResponseCode.OK
&& purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED
&& !purchase.isAcknowledged()) {
executeOnBillingClientConnection(new Runnable() {
@Override
public void run() {
AcknowledgePurchaseParams acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder().setPurchaseToken(purchase.getPurchaseToken()).build();
billingClient.acknowledgePurchase(acknowledgePurchaseParams, null);
}
});
}
}
private long host = 0;
private BillingClient billingClient;
}

+ 323
- 658
modules/juce_product_unlocking/native/juce_android_InAppPurchases.cpp
File diff suppressed because it is too large
View File


+ 1
- 1
modules/juce_product_unlocking/native/juce_ios_InAppPurchases.cpp View File

@@ -196,7 +196,7 @@ struct InAppPurchases::Pimpl : public SKDelegateAndPaymentObserver
[productsRequest start];
}
void purchaseProduct (const String& productIdentifier, bool, const StringArray&, bool)
void purchaseProduct (const String& productIdentifier, const String&, bool)
{
if (! [SKPaymentQueue canMakePayments])
{


Loading…
Cancel
Save