| @@ -101,7 +101,8 @@ public: | |||
| androidSharedLibraries, androidStaticLibraries, androidExtraAssetsFolder; | |||
| CachedValue<bool> androidInternetNeeded, androidMicNeeded, androidBluetoothNeeded, | |||
| androidExternalReadPermission, androidExternalWritePermission; | |||
| androidExternalReadPermission, androidExternalWritePermission, | |||
| androidInAppBillingPermission; | |||
| CachedValue<String> androidOtherPermissions; | |||
| CachedValue<String> androidKeyStore, androidKeyStorePass, androidKeyAlias, androidKeyAliasPass; | |||
| @@ -128,6 +129,7 @@ public: | |||
| androidBluetoothNeeded (settings, Ids::androidBluetoothNeeded, nullptr, true), | |||
| androidExternalReadPermission (settings, Ids::androidExternalReadNeeded, nullptr, true), | |||
| androidExternalWritePermission (settings, Ids::androidExternalWriteNeeded, nullptr, true), | |||
| androidInAppBillingPermission (settings, Ids::androidInAppBilling, nullptr, false), | |||
| androidOtherPermissions (settings, Ids::androidOtherPermissions, nullptr), | |||
| androidKeyStore (settings, Ids::androidKeyStore, nullptr, "${user.home}/.android/debug.keystore"), | |||
| androidKeyStorePass (settings, Ids::androidKeyStorePass, nullptr, "android"), | |||
| @@ -201,14 +203,8 @@ public: | |||
| removeOldFiles (targetFolder); | |||
| { | |||
| const String package (getActivityClassPackage()); | |||
| const String path (package.replaceCharacter ('.', File::separator)); | |||
| const File javaTarget (targetFolder.getChildFile ("app/src/main/java").getChildFile (path)); | |||
| if (! isLibrary()) | |||
| copyActivityJavaFiles (modules, javaTarget, package); | |||
| } | |||
| if (! isLibrary()) | |||
| copyJavaFiles (modules); | |||
| copyExtraResourceFiles(); | |||
| @@ -827,6 +823,9 @@ private: | |||
| props.add (new BooleanPropertyComponent (androidExternalWritePermission.getPropertyAsValue(), "Write to external storage", "Specify permissions to write to external storage"), | |||
| "If enabled, this will set the android.permission.WRITE_EXTERNAL_STORAGE flag in the manifest."); | |||
| props.add (new BooleanPropertyComponent (androidInAppBillingPermission.getPropertyAsValue(), "In-App Billing", "Specify In-App Billing permission in the manifest"), | |||
| "If enabled, this will set the com.android.vending.BILLING flag in the manifest."); | |||
| props.add (new TextPropertyComponent (androidOtherPermissions.getPropertyAsValue(), "Custom permissions", 2048, false), | |||
| "A space-separated list of other permission flags that should be added to the manifest."); | |||
| @@ -879,78 +878,106 @@ private: | |||
| } | |||
| //============================================================================== | |||
| void copyActivityJavaFiles (const OwnedArray<LibraryModule>& modules, const File& targetFolder, const String& package) const | |||
| void copyJavaFiles (const OwnedArray<LibraryModule>& modules) const | |||
| { | |||
| if (auto* coreModule = getCoreModule (modules)) | |||
| { | |||
| auto package = getActivityClassPackage(); | |||
| auto targetFolder = getTargetFolder(); | |||
| auto inAppBillingPath = String ("com.android.vending.billing").replaceCharacter ('.', File::separator); | |||
| auto javaSourceFolder = coreModule->getFolder().getChildFile ("native").getChildFile ("java"); | |||
| auto javaInAppBillingTarget = targetFolder.getChildFile ("app/src/main/java").getChildFile (inAppBillingPath); | |||
| auto javaActivityTarget = targetFolder.getChildFile ("app/src/main/java") | |||
| .getChildFile (package.replaceCharacter ('.', File::separator)); | |||
| copyActivityJavaFiles (javaSourceFolder, javaActivityTarget, package); | |||
| copyAdditionalJavaFiles (javaSourceFolder, javaInAppBillingTarget); | |||
| } | |||
| } | |||
| void copyActivityJavaFiles (const File& javaSourceFolder, const File& targetFolder, const String& package) const | |||
| { | |||
| if (androidActivityClass.get().contains ("_")) | |||
| throw SaveError ("Your Android activity class name or path may not contain any underscores! Try a project name without underscores."); | |||
| const String className (getActivityName()); | |||
| auto className = getActivityName(); | |||
| if (className.isEmpty()) | |||
| throw SaveError ("Invalid Android Activity class name: " + androidActivityClass.get()); | |||
| createDirectoryOrThrow (targetFolder); | |||
| if (auto* coreModule = getCoreModule (modules)) | |||
| { | |||
| File javaDestFile (targetFolder.getChildFile (className + ".java")); | |||
| auto javaDestFile = targetFolder.getChildFile (className + ".java"); | |||
| File javaSourceFolder (coreModule->getFolder().getChildFile ("native") | |||
| .getChildFile ("java")); | |||
| String juceMidiCode, juceMidiImports, juceRuntimePermissionsCode; | |||
| String juceMidiCode, juceMidiImports, juceRuntimePermissionsCode; | |||
| juceMidiImports << newLine; | |||
| juceMidiImports << newLine; | |||
| if (androidMinimumSDK.get().getIntValue() >= 23) | |||
| { | |||
| File javaAndroidMidi (javaSourceFolder.getChildFile ("AndroidMidi.java")); | |||
| File javaRuntimePermissions (javaSourceFolder.getChildFile ("AndroidRuntimePermissions.java")); | |||
| if (androidMinimumSDK.get().getIntValue() >= 23) | |||
| { | |||
| auto javaAndroidMidi = javaSourceFolder.getChildFile ("AndroidMidi.java"); | |||
| auto javaRuntimePermissions = javaSourceFolder.getChildFile ("AndroidRuntimePermissions.java"); | |||
| juceMidiImports << "import android.media.midi.*;" << newLine | |||
| << "import android.bluetooth.*;" << newLine | |||
| << "import android.bluetooth.le.*;" << newLine; | |||
| juceMidiImports << "import android.media.midi.*;" << newLine | |||
| << "import android.bluetooth.*;" << newLine | |||
| << "import android.bluetooth.le.*;" << newLine; | |||
| juceMidiCode = javaAndroidMidi.loadFileAsString().replace ("JuceAppActivity", className); | |||
| juceMidiCode = javaAndroidMidi.loadFileAsString().replace ("JuceAppActivity", className); | |||
| juceRuntimePermissionsCode = javaRuntimePermissions.loadFileAsString().replace ("JuceAppActivity", className); | |||
| } | |||
| else | |||
| juceRuntimePermissionsCode = javaRuntimePermissions.loadFileAsString().replace ("JuceAppActivity", className); | |||
| } | |||
| else | |||
| { | |||
| juceMidiCode = javaSourceFolder.getChildFile ("AndroidMidiFallback.java") | |||
| .loadFileAsString() | |||
| .replace ("JuceAppActivity", className); | |||
| } | |||
| auto javaSourceFile = javaSourceFolder.getChildFile ("JuceAppActivity.java"); | |||
| auto javaSourceLines = StringArray::fromLines (javaSourceFile.loadFileAsString()); | |||
| { | |||
| MemoryOutputStream newFile; | |||
| for (const auto& line : javaSourceLines) | |||
| { | |||
| juceMidiCode = javaSourceFolder.getChildFile ("AndroidMidiFallback.java") | |||
| .loadFileAsString() | |||
| .replace ("JuceAppActivity", className); | |||
| if (line.contains ("$$JuceAndroidMidiImports$$")) | |||
| newFile << juceMidiImports; | |||
| else if (line.contains ("$$JuceAndroidMidiCode$$")) | |||
| newFile << juceMidiCode; | |||
| else if (line.contains ("$$JuceAndroidRuntimePermissionsCode$$")) | |||
| newFile << juceRuntimePermissionsCode; | |||
| else | |||
| newFile << line.replace ("JuceAppActivity", className) | |||
| .replace ("package com.juce;", "package " + package + ";") << newLine; | |||
| } | |||
| auto javaSourceFile = javaSourceFolder.getChildFile ("JuceAppActivity.java"); | |||
| auto javaSourceLines = StringArray::fromLines (javaSourceFile.loadFileAsString()); | |||
| javaSourceLines = StringArray::fromLines (newFile.toString()); | |||
| } | |||
| { | |||
| MemoryOutputStream newFile; | |||
| while (javaSourceLines.size() > 2 | |||
| && javaSourceLines[javaSourceLines.size() - 1].trim().isEmpty() | |||
| && javaSourceLines[javaSourceLines.size() - 2].trim().isEmpty()) | |||
| javaSourceLines.remove (javaSourceLines.size() - 1); | |||
| for (const auto& line : javaSourceLines) | |||
| { | |||
| if (line.contains ("$$JuceAndroidMidiImports$$")) | |||
| newFile << juceMidiImports; | |||
| else if (line.contains ("$$JuceAndroidMidiCode$$")) | |||
| newFile << juceMidiCode; | |||
| else if (line.contains ("$$JuceAndroidRuntimePermissionsCode$$")) | |||
| newFile << juceRuntimePermissionsCode; | |||
| else | |||
| newFile << line.replace ("JuceAppActivity", className) | |||
| .replace ("package com.juce;", "package " + package + ";") << newLine; | |||
| } | |||
| overwriteFileIfDifferentOrThrow (javaDestFile, javaSourceLines.joinIntoString (newLine)); | |||
| } | |||
| javaSourceLines = StringArray::fromLines (newFile.toString()); | |||
| } | |||
| void copyAdditionalJavaFiles (const File& sourceFolder, const File& targetFolder) const | |||
| { | |||
| auto inAppBillingJavaFileName = String ("IInAppBillingService.java"); | |||
| while (javaSourceLines.size() > 2 | |||
| && javaSourceLines[javaSourceLines.size() - 1].trim().isEmpty() | |||
| && javaSourceLines[javaSourceLines.size() - 2].trim().isEmpty()) | |||
| javaSourceLines.remove (javaSourceLines.size() - 1); | |||
| auto inAppBillingJavaSrcFile = sourceFolder.getChildFile (inAppBillingJavaFileName); | |||
| auto inAppBillingJavaDestFile = targetFolder.getChildFile (inAppBillingJavaFileName); | |||
| overwriteFileIfDifferentOrThrow (javaDestFile, javaSourceLines.joinIntoString (newLine)); | |||
| } | |||
| createDirectoryOrThrow (targetFolder); | |||
| jassert (inAppBillingJavaSrcFile.existsAsFile()); | |||
| if (inAppBillingJavaSrcFile.existsAsFile()) | |||
| inAppBillingJavaSrcFile.copyFileTo (inAppBillingJavaDestFile); | |||
| } | |||
| void copyExtraResourceFiles() const | |||
| @@ -1422,6 +1449,9 @@ private: | |||
| if (androidExternalWritePermission.get()) | |||
| s.add ("android.permission.WRITE_EXTERNAL_STORAGE"); | |||
| if (androidInAppBillingPermission.get()) | |||
| s.add ("com.android.vending.BILLING"); | |||
| return getCleanedStringArray (s); | |||
| } | |||
| @@ -180,6 +180,7 @@ namespace Ids | |||
| DECLARE_ID (androidBluetoothNeeded); | |||
| DECLARE_ID (androidExternalReadNeeded); | |||
| DECLARE_ID (androidExternalWriteNeeded); | |||
| DECLARE_ID (androidInAppBilling); | |||
| DECLARE_ID (androidMinimumSDK); | |||
| DECLARE_ID (androidOtherPermissions); | |||
| DECLARE_ID (androidKeyStore); | |||
| @@ -0,0 +1,971 @@ | |||
| /* | |||
| ============================================================================== | |||
| This file is part of the JUCE library. | |||
| Copyright (c) 2017 - ROLI Ltd. | |||
| JUCE is an open source library subject to commercial or open-source | |||
| licensing. | |||
| The code included in this file is provided under the terms of the ISC license | |||
| http://www.isc.org/downloads/software-support-policy/isc-license. Permission | |||
| To use, copy, modify, and/or distribute this software for any purpose with or | |||
| without fee is hereby granted provided that the above copyright notice and | |||
| this permission notice appear in all copies. | |||
| JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
| EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
| DISCLAIMED. | |||
| ============================================================================== | |||
| */ | |||
| package com.android.vending.billing; | |||
| /** | |||
| * InAppBillingService is the service that provides in-app billing version 3 and beyond. | |||
| * This service provides the following features: | |||
| * 1. Provides a new API to get details of in-app items published for the app including | |||
| * price, type, title and description. | |||
| * 2. The purchase flow is synchronous and purchase information is available immediately | |||
| * after it completes. | |||
| * 3. Purchase information of in-app purchases is maintained within the Google Play system | |||
| * till the purchase is consumed. | |||
| * 4. An API to consume a purchase of an inapp item. All purchases of one-time | |||
| * in-app items are consumable and thereafter can be purchased again. | |||
| * 5. An API to get current purchases of the user immediately. This will not contain any | |||
| * consumed purchases. | |||
| * | |||
| * All calls will give a response code with the following possible values | |||
| * RESULT_OK = 0 - success | |||
| * RESULT_USER_CANCELED = 1 - User pressed back or canceled a dialog | |||
| * RESULT_SERVICE_UNAVAILABLE = 2 - The network connection is down | |||
| * RESULT_BILLING_UNAVAILABLE = 3 - This billing API version is not supported for the type requested | |||
| * RESULT_ITEM_UNAVAILABLE = 4 - Requested SKU is not available for purchase | |||
| * RESULT_DEVELOPER_ERROR = 5 - Invalid arguments provided to the API | |||
| * RESULT_ERROR = 6 - Fatal error during the API action | |||
| * RESULT_ITEM_ALREADY_OWNED = 7 - Failure to purchase since item is already owned | |||
| * RESULT_ITEM_NOT_OWNED = 8 - Failure to consume since item is not owned | |||
| */ | |||
| public interface IInAppBillingService extends android.os.IInterface | |||
| { | |||
| /** Local-side IPC implementation stub class. */ | |||
| public static abstract class Stub extends android.os.Binder implements com.android.vending.billing.IInAppBillingService | |||
| { | |||
| private static final java.lang.String DESCRIPTOR = "com.android.vending.billing.IInAppBillingService"; | |||
| /** Construct the stub at attach it to the interface. */ | |||
| public Stub() | |||
| { | |||
| this.attachInterface(this, DESCRIPTOR); | |||
| } | |||
| /** | |||
| * Cast an IBinder object into an com.android.vending.billing.IInAppBillingService interface, | |||
| * generating a proxy if needed. | |||
| */ | |||
| public static com.android.vending.billing.IInAppBillingService asInterface(android.os.IBinder obj) | |||
| { | |||
| if ((obj==null)) { | |||
| return null; | |||
| } | |||
| android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR); | |||
| if (((iin!=null)&&(iin instanceof com.android.vending.billing.IInAppBillingService))) { | |||
| return ((com.android.vending.billing.IInAppBillingService)iin); | |||
| } | |||
| return new com.android.vending.billing.IInAppBillingService.Stub.Proxy(obj); | |||
| } | |||
| @Override public android.os.IBinder asBinder() | |||
| { | |||
| return this; | |||
| } | |||
| @Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException | |||
| { | |||
| switch (code) | |||
| { | |||
| case INTERFACE_TRANSACTION: | |||
| { | |||
| reply.writeString(DESCRIPTOR); | |||
| return true; | |||
| } | |||
| case TRANSACTION_isBillingSupported: | |||
| { | |||
| data.enforceInterface(DESCRIPTOR); | |||
| int _arg0; | |||
| _arg0 = data.readInt(); | |||
| java.lang.String _arg1; | |||
| _arg1 = data.readString(); | |||
| java.lang.String _arg2; | |||
| _arg2 = data.readString(); | |||
| int _result = this.isBillingSupported(_arg0, _arg1, _arg2); | |||
| reply.writeNoException(); | |||
| reply.writeInt(_result); | |||
| return true; | |||
| } | |||
| case TRANSACTION_getSkuDetails: | |||
| { | |||
| data.enforceInterface(DESCRIPTOR); | |||
| int _arg0; | |||
| _arg0 = data.readInt(); | |||
| java.lang.String _arg1; | |||
| _arg1 = data.readString(); | |||
| java.lang.String _arg2; | |||
| _arg2 = data.readString(); | |||
| android.os.Bundle _arg3; | |||
| if ((0!=data.readInt())) { | |||
| _arg3 = android.os.Bundle.CREATOR.createFromParcel(data); | |||
| } | |||
| else { | |||
| _arg3 = null; | |||
| } | |||
| android.os.Bundle _result = this.getSkuDetails(_arg0, _arg1, _arg2, _arg3); | |||
| reply.writeNoException(); | |||
| if ((_result!=null)) { | |||
| reply.writeInt(1); | |||
| _result.writeToParcel(reply, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE); | |||
| } | |||
| else { | |||
| reply.writeInt(0); | |||
| } | |||
| return true; | |||
| } | |||
| case TRANSACTION_getBuyIntent: | |||
| { | |||
| data.enforceInterface(DESCRIPTOR); | |||
| int _arg0; | |||
| _arg0 = data.readInt(); | |||
| java.lang.String _arg1; | |||
| _arg1 = data.readString(); | |||
| java.lang.String _arg2; | |||
| _arg2 = data.readString(); | |||
| java.lang.String _arg3; | |||
| _arg3 = data.readString(); | |||
| java.lang.String _arg4; | |||
| _arg4 = data.readString(); | |||
| android.os.Bundle _result = this.getBuyIntent(_arg0, _arg1, _arg2, _arg3, _arg4); | |||
| reply.writeNoException(); | |||
| if ((_result!=null)) { | |||
| reply.writeInt(1); | |||
| _result.writeToParcel(reply, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE); | |||
| } | |||
| else { | |||
| reply.writeInt(0); | |||
| } | |||
| return true; | |||
| } | |||
| case TRANSACTION_getPurchases: | |||
| { | |||
| data.enforceInterface(DESCRIPTOR); | |||
| int _arg0; | |||
| _arg0 = data.readInt(); | |||
| java.lang.String _arg1; | |||
| _arg1 = data.readString(); | |||
| java.lang.String _arg2; | |||
| _arg2 = data.readString(); | |||
| java.lang.String _arg3; | |||
| _arg3 = data.readString(); | |||
| android.os.Bundle _result = this.getPurchases(_arg0, _arg1, _arg2, _arg3); | |||
| reply.writeNoException(); | |||
| if ((_result!=null)) { | |||
| reply.writeInt(1); | |||
| _result.writeToParcel(reply, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE); | |||
| } | |||
| else { | |||
| reply.writeInt(0); | |||
| } | |||
| return true; | |||
| } | |||
| case TRANSACTION_consumePurchase: | |||
| { | |||
| data.enforceInterface(DESCRIPTOR); | |||
| int _arg0; | |||
| _arg0 = data.readInt(); | |||
| java.lang.String _arg1; | |||
| _arg1 = data.readString(); | |||
| java.lang.String _arg2; | |||
| _arg2 = data.readString(); | |||
| int _result = this.consumePurchase(_arg0, _arg1, _arg2); | |||
| reply.writeNoException(); | |||
| reply.writeInt(_result); | |||
| return true; | |||
| } | |||
| case TRANSACTION_stub: | |||
| { | |||
| data.enforceInterface(DESCRIPTOR); | |||
| int _arg0; | |||
| _arg0 = data.readInt(); | |||
| java.lang.String _arg1; | |||
| _arg1 = data.readString(); | |||
| java.lang.String _arg2; | |||
| _arg2 = data.readString(); | |||
| int _result = this.stub(_arg0, _arg1, _arg2); | |||
| reply.writeNoException(); | |||
| reply.writeInt(_result); | |||
| return true; | |||
| } | |||
| case TRANSACTION_getBuyIntentToReplaceSkus: | |||
| { | |||
| data.enforceInterface(DESCRIPTOR); | |||
| int _arg0; | |||
| _arg0 = data.readInt(); | |||
| java.lang.String _arg1; | |||
| _arg1 = data.readString(); | |||
| java.util.List<java.lang.String> _arg2; | |||
| _arg2 = data.createStringArrayList(); | |||
| java.lang.String _arg3; | |||
| _arg3 = data.readString(); | |||
| java.lang.String _arg4; | |||
| _arg4 = data.readString(); | |||
| java.lang.String _arg5; | |||
| _arg5 = data.readString(); | |||
| android.os.Bundle _result = this.getBuyIntentToReplaceSkus(_arg0, _arg1, _arg2, _arg3, _arg4, _arg5); | |||
| reply.writeNoException(); | |||
| if ((_result!=null)) { | |||
| reply.writeInt(1); | |||
| _result.writeToParcel(reply, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE); | |||
| } | |||
| else { | |||
| reply.writeInt(0); | |||
| } | |||
| return true; | |||
| } | |||
| case TRANSACTION_getBuyIntentExtraParams: | |||
| { | |||
| data.enforceInterface(DESCRIPTOR); | |||
| int _arg0; | |||
| _arg0 = data.readInt(); | |||
| java.lang.String _arg1; | |||
| _arg1 = data.readString(); | |||
| java.lang.String _arg2; | |||
| _arg2 = data.readString(); | |||
| java.lang.String _arg3; | |||
| _arg3 = data.readString(); | |||
| java.lang.String _arg4; | |||
| _arg4 = data.readString(); | |||
| android.os.Bundle _arg5; | |||
| if ((0!=data.readInt())) { | |||
| _arg5 = android.os.Bundle.CREATOR.createFromParcel(data); | |||
| } | |||
| else { | |||
| _arg5 = null; | |||
| } | |||
| android.os.Bundle _result = this.getBuyIntentExtraParams(_arg0, _arg1, _arg2, _arg3, _arg4, _arg5); | |||
| reply.writeNoException(); | |||
| if ((_result!=null)) { | |||
| reply.writeInt(1); | |||
| _result.writeToParcel(reply, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE); | |||
| } | |||
| else { | |||
| reply.writeInt(0); | |||
| } | |||
| return true; | |||
| } | |||
| case TRANSACTION_getPurchaseHistory: | |||
| { | |||
| data.enforceInterface(DESCRIPTOR); | |||
| int _arg0; | |||
| _arg0 = data.readInt(); | |||
| java.lang.String _arg1; | |||
| _arg1 = data.readString(); | |||
| java.lang.String _arg2; | |||
| _arg2 = data.readString(); | |||
| java.lang.String _arg3; | |||
| _arg3 = data.readString(); | |||
| android.os.Bundle _arg4; | |||
| if ((0!=data.readInt())) { | |||
| _arg4 = android.os.Bundle.CREATOR.createFromParcel(data); | |||
| } | |||
| else { | |||
| _arg4 = null; | |||
| } | |||
| android.os.Bundle _result = this.getPurchaseHistory(_arg0, _arg1, _arg2, _arg3, _arg4); | |||
| reply.writeNoException(); | |||
| if ((_result!=null)) { | |||
| reply.writeInt(1); | |||
| _result.writeToParcel(reply, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE); | |||
| } | |||
| else { | |||
| reply.writeInt(0); | |||
| } | |||
| return true; | |||
| } | |||
| case TRANSACTION_isBillingSupportedExtraParams: | |||
| { | |||
| data.enforceInterface(DESCRIPTOR); | |||
| int _arg0; | |||
| _arg0 = data.readInt(); | |||
| java.lang.String _arg1; | |||
| _arg1 = data.readString(); | |||
| java.lang.String _arg2; | |||
| _arg2 = data.readString(); | |||
| android.os.Bundle _arg3; | |||
| if ((0!=data.readInt())) { | |||
| _arg3 = android.os.Bundle.CREATOR.createFromParcel(data); | |||
| } | |||
| else { | |||
| _arg3 = null; | |||
| } | |||
| int _result = this.isBillingSupportedExtraParams(_arg0, _arg1, _arg2, _arg3); | |||
| reply.writeNoException(); | |||
| reply.writeInt(_result); | |||
| return true; | |||
| } | |||
| } | |||
| return super.onTransact(code, data, reply, flags); | |||
| } | |||
| private static class Proxy implements com.android.vending.billing.IInAppBillingService | |||
| { | |||
| private android.os.IBinder mRemote; | |||
| Proxy(android.os.IBinder remote) | |||
| { | |||
| mRemote = remote; | |||
| } | |||
| @Override public android.os.IBinder asBinder() | |||
| { | |||
| return mRemote; | |||
| } | |||
| public java.lang.String getInterfaceDescriptor() | |||
| { | |||
| return DESCRIPTOR; | |||
| } | |||
| @Override public int isBillingSupported(int apiVersion, java.lang.String packageName, java.lang.String type) throws android.os.RemoteException | |||
| { | |||
| android.os.Parcel _data = android.os.Parcel.obtain(); | |||
| android.os.Parcel _reply = android.os.Parcel.obtain(); | |||
| int _result; | |||
| try { | |||
| _data.writeInterfaceToken(DESCRIPTOR); | |||
| _data.writeInt(apiVersion); | |||
| _data.writeString(packageName); | |||
| _data.writeString(type); | |||
| mRemote.transact(Stub.TRANSACTION_isBillingSupported, _data, _reply, 0); | |||
| _reply.readException(); | |||
| _result = _reply.readInt(); | |||
| } | |||
| finally { | |||
| _reply.recycle(); | |||
| _data.recycle(); | |||
| } | |||
| return _result; | |||
| } | |||
| /** | |||
| * Provides details of a list of SKUs | |||
| * Given a list of SKUs of a valid type in the skusBundle, this returns a bundle | |||
| * with a list JSON strings containing the productId, price, title and description. | |||
| * This API can be called with a maximum of 20 SKUs. | |||
| * @param apiVersion billing API version that the app is using | |||
| * @param packageName the package name of the calling app | |||
| * @param type of the in-app items ("inapp" for one-time purchases | |||
| * and "subs" for subscriptions) | |||
| * @param skusBundle bundle containing a StringArrayList of SKUs with key "ITEM_ID_LIST" | |||
| * @return Bundle containing the following key-value pairs | |||
| * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes | |||
| * on failures. | |||
| * "DETAILS_LIST" with a StringArrayList containing purchase information | |||
| * in JSON format similar to: | |||
| * '{ "productId" : "exampleSku", | |||
| * "type" : "inapp", | |||
| * "price" : "$5.00", | |||
| * "price_currency": "USD", | |||
| * "price_amount_micros": 5000000, | |||
| * "title : "Example Title", | |||
| * "description" : "This is an example description" }' | |||
| */ | |||
| @Override public android.os.Bundle getSkuDetails(int apiVersion, java.lang.String packageName, java.lang.String type, android.os.Bundle skusBundle) throws android.os.RemoteException | |||
| { | |||
| android.os.Parcel _data = android.os.Parcel.obtain(); | |||
| android.os.Parcel _reply = android.os.Parcel.obtain(); | |||
| android.os.Bundle _result; | |||
| try { | |||
| _data.writeInterfaceToken(DESCRIPTOR); | |||
| _data.writeInt(apiVersion); | |||
| _data.writeString(packageName); | |||
| _data.writeString(type); | |||
| if ((skusBundle!=null)) { | |||
| _data.writeInt(1); | |||
| skusBundle.writeToParcel(_data, 0); | |||
| } | |||
| else { | |||
| _data.writeInt(0); | |||
| } | |||
| mRemote.transact(Stub.TRANSACTION_getSkuDetails, _data, _reply, 0); | |||
| _reply.readException(); | |||
| if ((0!=_reply.readInt())) { | |||
| _result = android.os.Bundle.CREATOR.createFromParcel(_reply); | |||
| } | |||
| else { | |||
| _result = null; | |||
| } | |||
| } | |||
| finally { | |||
| _reply.recycle(); | |||
| _data.recycle(); | |||
| } | |||
| return _result; | |||
| } | |||
| /** | |||
| * Returns a pending intent to launch the purchase flow for an in-app item by providing a SKU, | |||
| * the type, a unique purchase token and an optional developer payload. | |||
| * @param apiVersion billing API version that the app is using | |||
| * @param packageName package name of the calling app | |||
| * @param sku the SKU of the in-app item as published in the developer console | |||
| * @param type of the in-app item being purchased ("inapp" for one-time purchases | |||
| * and "subs" for subscriptions) | |||
| * @param developerPayload optional argument to be sent back with the purchase information | |||
| * @return Bundle containing the following key-value pairs | |||
| * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes | |||
| * on failures. | |||
| * "BUY_INTENT" - PendingIntent to start the purchase flow | |||
| * | |||
| * The Pending intent should be launched with startIntentSenderForResult. When purchase flow | |||
| * has completed, the onActivityResult() will give a resultCode of OK or CANCELED. | |||
| * If the purchase is successful, the result data will contain the following key-value pairs | |||
| * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response | |||
| * codes on failures. | |||
| * "INAPP_PURCHASE_DATA" - String in JSON format similar to | |||
| * '{"orderId":"12999763169054705758.1371079406387615", | |||
| * "packageName":"com.example.app", | |||
| * "productId":"exampleSku", | |||
| * "purchaseTime":1345678900000, | |||
| * "purchaseToken" : "122333444455555", | |||
| * "developerPayload":"example developer payload" }' | |||
| * "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that | |||
| * was signed with the private key of the developer | |||
| */ | |||
| @Override public android.os.Bundle getBuyIntent(int apiVersion, java.lang.String packageName, java.lang.String sku, java.lang.String type, java.lang.String developerPayload) throws android.os.RemoteException | |||
| { | |||
| android.os.Parcel _data = android.os.Parcel.obtain(); | |||
| android.os.Parcel _reply = android.os.Parcel.obtain(); | |||
| android.os.Bundle _result; | |||
| try { | |||
| _data.writeInterfaceToken(DESCRIPTOR); | |||
| _data.writeInt(apiVersion); | |||
| _data.writeString(packageName); | |||
| _data.writeString(sku); | |||
| _data.writeString(type); | |||
| _data.writeString(developerPayload); | |||
| mRemote.transact(Stub.TRANSACTION_getBuyIntent, _data, _reply, 0); | |||
| _reply.readException(); | |||
| if ((0!=_reply.readInt())) { | |||
| _result = android.os.Bundle.CREATOR.createFromParcel(_reply); | |||
| } | |||
| else { | |||
| _result = null; | |||
| } | |||
| } | |||
| finally { | |||
| _reply.recycle(); | |||
| _data.recycle(); | |||
| } | |||
| return _result; | |||
| } | |||
| /** | |||
| * Returns the current SKUs owned by the user of the type and package name specified along with | |||
| * purchase information and a signature of the data to be validated. | |||
| * This will return all SKUs that have been purchased in V3 and managed items purchased using | |||
| * V1 and V2 that have not been consumed. | |||
| * @param apiVersion billing API version that the app is using | |||
| * @param packageName package name of the calling app | |||
| * @param type of the in-app items being requested ("inapp" for one-time purchases | |||
| * and "subs" for subscriptions) | |||
| * @param continuationToken to be set as null for the first call, if the number of owned | |||
| * skus are too many, a continuationToken is returned in the response bundle. | |||
| * This method can be called again with the continuation token to get the next set of | |||
| * owned skus. | |||
| * @return Bundle containing the following key-value pairs | |||
| * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes | |||
| on failures. | |||
| * "INAPP_PURCHASE_ITEM_LIST" - StringArrayList containing the list of SKUs | |||
| * "INAPP_PURCHASE_DATA_LIST" - StringArrayList containing the purchase information | |||
| * "INAPP_DATA_SIGNATURE_LIST"- StringArrayList containing the signatures | |||
| * of the purchase information | |||
| * "INAPP_CONTINUATION_TOKEN" - String containing a continuation token for the | |||
| * next set of in-app purchases. Only set if the | |||
| * user has more owned skus than the current list. | |||
| */ | |||
| @Override public android.os.Bundle getPurchases(int apiVersion, java.lang.String packageName, java.lang.String type, java.lang.String continuationToken) throws android.os.RemoteException | |||
| { | |||
| android.os.Parcel _data = android.os.Parcel.obtain(); | |||
| android.os.Parcel _reply = android.os.Parcel.obtain(); | |||
| android.os.Bundle _result; | |||
| try { | |||
| _data.writeInterfaceToken(DESCRIPTOR); | |||
| _data.writeInt(apiVersion); | |||
| _data.writeString(packageName); | |||
| _data.writeString(type); | |||
| _data.writeString(continuationToken); | |||
| mRemote.transact(Stub.TRANSACTION_getPurchases, _data, _reply, 0); | |||
| _reply.readException(); | |||
| if ((0!=_reply.readInt())) { | |||
| _result = android.os.Bundle.CREATOR.createFromParcel(_reply); | |||
| } | |||
| else { | |||
| _result = null; | |||
| } | |||
| } | |||
| finally { | |||
| _reply.recycle(); | |||
| _data.recycle(); | |||
| } | |||
| return _result; | |||
| } | |||
| @Override public int consumePurchase(int apiVersion, java.lang.String packageName, java.lang.String purchaseToken) throws android.os.RemoteException | |||
| { | |||
| android.os.Parcel _data = android.os.Parcel.obtain(); | |||
| android.os.Parcel _reply = android.os.Parcel.obtain(); | |||
| int _result; | |||
| try { | |||
| _data.writeInterfaceToken(DESCRIPTOR); | |||
| _data.writeInt(apiVersion); | |||
| _data.writeString(packageName); | |||
| _data.writeString(purchaseToken); | |||
| mRemote.transact(Stub.TRANSACTION_consumePurchase, _data, _reply, 0); | |||
| _reply.readException(); | |||
| _result = _reply.readInt(); | |||
| } | |||
| finally { | |||
| _reply.recycle(); | |||
| _data.recycle(); | |||
| } | |||
| return _result; | |||
| } | |||
| @Override public int stub(int apiVersion, java.lang.String packageName, java.lang.String type) throws android.os.RemoteException | |||
| { | |||
| android.os.Parcel _data = android.os.Parcel.obtain(); | |||
| android.os.Parcel _reply = android.os.Parcel.obtain(); | |||
| int _result; | |||
| try { | |||
| _data.writeInterfaceToken(DESCRIPTOR); | |||
| _data.writeInt(apiVersion); | |||
| _data.writeString(packageName); | |||
| _data.writeString(type); | |||
| mRemote.transact(Stub.TRANSACTION_stub, _data, _reply, 0); | |||
| _reply.readException(); | |||
| _result = _reply.readInt(); | |||
| } | |||
| finally { | |||
| _reply.recycle(); | |||
| _data.recycle(); | |||
| } | |||
| return _result; | |||
| } | |||
| /** | |||
| * Returns a pending intent to launch the purchase flow for upgrading or downgrading a | |||
| * subscription. The existing owned SKU(s) should be provided along with the new SKU that | |||
| * the user is upgrading or downgrading to. | |||
| * @param apiVersion billing API version that the app is using, must be 5 or later | |||
| * @param packageName package name of the calling app | |||
| * @param oldSkus the SKU(s) that the user is upgrading or downgrading from, | |||
| * if null or empty this method will behave like {@link #getBuyIntent} | |||
| * @param newSku the SKU that the user is upgrading or downgrading to | |||
| * @param type of the item being purchased, currently must be "subs" | |||
| * @param developerPayload optional argument to be sent back with the purchase information | |||
| * @return Bundle containing the following key-value pairs | |||
| * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes | |||
| * on failures. | |||
| * "BUY_INTENT" - PendingIntent to start the purchase flow | |||
| * | |||
| * The Pending intent should be launched with startIntentSenderForResult. When purchase flow | |||
| * has completed, the onActivityResult() will give a resultCode of OK or CANCELED. | |||
| * If the purchase is successful, the result data will contain the following key-value pairs | |||
| * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response | |||
| * codes on failures. | |||
| * "INAPP_PURCHASE_DATA" - String in JSON format similar to | |||
| * '{"orderId":"12999763169054705758.1371079406387615", | |||
| * "packageName":"com.example.app", | |||
| * "productId":"exampleSku", | |||
| * "purchaseTime":1345678900000, | |||
| * "purchaseToken" : "122333444455555", | |||
| * "developerPayload":"example developer payload" }' | |||
| * "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that | |||
| * was signed with the private key of the developer | |||
| */ | |||
| @Override public android.os.Bundle getBuyIntentToReplaceSkus(int apiVersion, java.lang.String packageName, java.util.List<java.lang.String> oldSkus, java.lang.String newSku, java.lang.String type, java.lang.String developerPayload) throws android.os.RemoteException | |||
| { | |||
| android.os.Parcel _data = android.os.Parcel.obtain(); | |||
| android.os.Parcel _reply = android.os.Parcel.obtain(); | |||
| android.os.Bundle _result; | |||
| try { | |||
| _data.writeInterfaceToken(DESCRIPTOR); | |||
| _data.writeInt(apiVersion); | |||
| _data.writeString(packageName); | |||
| _data.writeStringList(oldSkus); | |||
| _data.writeString(newSku); | |||
| _data.writeString(type); | |||
| _data.writeString(developerPayload); | |||
| mRemote.transact(Stub.TRANSACTION_getBuyIntentToReplaceSkus, _data, _reply, 0); | |||
| _reply.readException(); | |||
| if ((0!=_reply.readInt())) { | |||
| _result = android.os.Bundle.CREATOR.createFromParcel(_reply); | |||
| } | |||
| else { | |||
| _result = null; | |||
| } | |||
| } | |||
| finally { | |||
| _reply.recycle(); | |||
| _data.recycle(); | |||
| } | |||
| return _result; | |||
| } | |||
| /** | |||
| * Returns a pending intent to launch the purchase flow for an in-app item. This method is | |||
| * a variant of the {@link #getBuyIntent} method and takes an additional {@code extraParams} | |||
| * parameter. This parameter is a Bundle of optional keys and values that affect the | |||
| * operation of the method. | |||
| * @param apiVersion billing API version that the app is using, must be 6 or later | |||
| * @param packageName package name of the calling app | |||
| * @param sku the SKU of the in-app item as published in the developer console | |||
| * @param type of the in-app item being purchased ("inapp" for one-time purchases | |||
| * and "subs" for subscriptions) | |||
| * @param developerPayload optional argument to be sent back with the purchase information | |||
| * @extraParams a Bundle with the following optional keys: | |||
| * "skusToReplace" - List<String> - an optional list of SKUs that the user is | |||
| * upgrading or downgrading from. | |||
| * Pass this field if the purchase is upgrading or downgrading | |||
| * existing subscriptions. | |||
| * The specified SKUs are replaced with the SKUs that the user is | |||
| * purchasing. Google Play replaces the specified SKUs at the start of | |||
| * the next billing cycle. | |||
| * "replaceSkusProration" - Boolean - whether the user should be credited for any unused | |||
| * subscription time on the SKUs they are upgrading or downgrading. | |||
| * If you set this field to true, Google Play swaps out the old SKUs | |||
| * and credits the user with the unused value of their subscription | |||
| * time on a pro-rated basis. | |||
| * Google Play applies this credit to the new subscription, and does | |||
| * not begin billing the user for the new subscription until after | |||
| * the credit is used up. | |||
| * If you set this field to false, the user does not receive credit for | |||
| * any unused subscription time and the recurrence date does not | |||
| * change. | |||
| * Default value is true. Ignored if you do not pass skusToReplace. | |||
| * "accountId" - String - an optional obfuscated string that is uniquely | |||
| * associated with the user's account in your app. | |||
| * If you pass this value, Google Play can use it to detect irregular | |||
| * activity, such as many devices making purchases on the same | |||
| * account in a short period of time. | |||
| * Do not use the developer ID or the user's Google ID for this field. | |||
| * In addition, this field should not contain the user's ID in | |||
| * cleartext. | |||
| * We recommend that you use a one-way hash to generate a string from | |||
| * the user's ID, and store the hashed string in this field. | |||
| * "vr" - Boolean - an optional flag indicating whether the returned intent | |||
| * should start a VR purchase flow. The apiVersion must also be 7 or | |||
| * later to use this flag. | |||
| */ | |||
| @Override public android.os.Bundle getBuyIntentExtraParams(int apiVersion, java.lang.String packageName, java.lang.String sku, java.lang.String type, java.lang.String developerPayload, android.os.Bundle extraParams) throws android.os.RemoteException | |||
| { | |||
| android.os.Parcel _data = android.os.Parcel.obtain(); | |||
| android.os.Parcel _reply = android.os.Parcel.obtain(); | |||
| android.os.Bundle _result; | |||
| try { | |||
| _data.writeInterfaceToken(DESCRIPTOR); | |||
| _data.writeInt(apiVersion); | |||
| _data.writeString(packageName); | |||
| _data.writeString(sku); | |||
| _data.writeString(type); | |||
| _data.writeString(developerPayload); | |||
| if ((extraParams!=null)) { | |||
| _data.writeInt(1); | |||
| extraParams.writeToParcel(_data, 0); | |||
| } | |||
| else { | |||
| _data.writeInt(0); | |||
| } | |||
| mRemote.transact(Stub.TRANSACTION_getBuyIntentExtraParams, _data, _reply, 0); | |||
| _reply.readException(); | |||
| if ((0!=_reply.readInt())) { | |||
| _result = android.os.Bundle.CREATOR.createFromParcel(_reply); | |||
| } | |||
| else { | |||
| _result = null; | |||
| } | |||
| } | |||
| finally { | |||
| _reply.recycle(); | |||
| _data.recycle(); | |||
| } | |||
| return _result; | |||
| } | |||
| /** | |||
| * Returns the most recent purchase made by the user for each SKU, even if that purchase is | |||
| * expired, canceled, or consumed. | |||
| * @param apiVersion billing API version that the app is using, must be 6 or later | |||
| * @param packageName package name of the calling app | |||
| * @param type of the in-app items being requested ("inapp" for one-time purchases | |||
| * and "subs" for subscriptions) | |||
| * @param continuationToken to be set as null for the first call, if the number of owned | |||
| * skus is too large, a continuationToken is returned in the response bundle. | |||
| * This method can be called again with the continuation token to get the next set of | |||
| * owned skus. | |||
| * @param extraParams a Bundle with extra params that would be appended into http request | |||
| * query string. Not used at this moment. Reserved for future functionality. | |||
| * @return Bundle containing the following key-value pairs | |||
| * "RESPONSE_CODE" with int value: RESULT_OK(0) if success, | |||
| * {@link IabHelper#BILLING_RESPONSE_RESULT_*} response codes on failures. | |||
| * | |||
| * "INAPP_PURCHASE_ITEM_LIST" - ArrayList<String> containing the list of SKUs | |||
| * "INAPP_PURCHASE_DATA_LIST" - ArrayList<String> containing the purchase information | |||
| * "INAPP_DATA_SIGNATURE_LIST"- ArrayList<String> containing the signatures | |||
| * of the purchase information | |||
| * "INAPP_CONTINUATION_TOKEN" - String containing a continuation token for the | |||
| * next set of in-app purchases. Only set if the | |||
| * user has more owned skus than the current list. | |||
| */ | |||
| @Override public android.os.Bundle getPurchaseHistory(int apiVersion, java.lang.String packageName, java.lang.String type, java.lang.String continuationToken, android.os.Bundle extraParams) throws android.os.RemoteException | |||
| { | |||
| android.os.Parcel _data = android.os.Parcel.obtain(); | |||
| android.os.Parcel _reply = android.os.Parcel.obtain(); | |||
| android.os.Bundle _result; | |||
| try { | |||
| _data.writeInterfaceToken(DESCRIPTOR); | |||
| _data.writeInt(apiVersion); | |||
| _data.writeString(packageName); | |||
| _data.writeString(type); | |||
| _data.writeString(continuationToken); | |||
| if ((extraParams!=null)) { | |||
| _data.writeInt(1); | |||
| extraParams.writeToParcel(_data, 0); | |||
| } | |||
| else { | |||
| _data.writeInt(0); | |||
| } | |||
| mRemote.transact(Stub.TRANSACTION_getPurchaseHistory, _data, _reply, 0); | |||
| _reply.readException(); | |||
| if ((0!=_reply.readInt())) { | |||
| _result = android.os.Bundle.CREATOR.createFromParcel(_reply); | |||
| } | |||
| else { | |||
| _result = null; | |||
| } | |||
| } | |||
| finally { | |||
| _reply.recycle(); | |||
| _data.recycle(); | |||
| } | |||
| return _result; | |||
| } | |||
| @Override public int isBillingSupportedExtraParams(int apiVersion, java.lang.String packageName, java.lang.String type, android.os.Bundle extraParams) throws android.os.RemoteException | |||
| { | |||
| android.os.Parcel _data = android.os.Parcel.obtain(); | |||
| android.os.Parcel _reply = android.os.Parcel.obtain(); | |||
| int _result; | |||
| try { | |||
| _data.writeInterfaceToken(DESCRIPTOR); | |||
| _data.writeInt(apiVersion); | |||
| _data.writeString(packageName); | |||
| _data.writeString(type); | |||
| if ((extraParams!=null)) { | |||
| _data.writeInt(1); | |||
| extraParams.writeToParcel(_data, 0); | |||
| } | |||
| else { | |||
| _data.writeInt(0); | |||
| } | |||
| mRemote.transact(Stub.TRANSACTION_isBillingSupportedExtraParams, _data, _reply, 0); | |||
| _reply.readException(); | |||
| _result = _reply.readInt(); | |||
| } | |||
| finally { | |||
| _reply.recycle(); | |||
| _data.recycle(); | |||
| } | |||
| return _result; | |||
| } | |||
| } | |||
| static final int TRANSACTION_isBillingSupported = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0); | |||
| static final int TRANSACTION_getSkuDetails = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1); | |||
| static final int TRANSACTION_getBuyIntent = (android.os.IBinder.FIRST_CALL_TRANSACTION + 2); | |||
| static final int TRANSACTION_getPurchases = (android.os.IBinder.FIRST_CALL_TRANSACTION + 3); | |||
| static final int TRANSACTION_consumePurchase = (android.os.IBinder.FIRST_CALL_TRANSACTION + 4); | |||
| static final int TRANSACTION_stub = (android.os.IBinder.FIRST_CALL_TRANSACTION + 5); | |||
| static final int TRANSACTION_getBuyIntentToReplaceSkus = (android.os.IBinder.FIRST_CALL_TRANSACTION + 6); | |||
| static final int TRANSACTION_getBuyIntentExtraParams = (android.os.IBinder.FIRST_CALL_TRANSACTION + 7); | |||
| static final int TRANSACTION_getPurchaseHistory = (android.os.IBinder.FIRST_CALL_TRANSACTION + 8); | |||
| static final int TRANSACTION_isBillingSupportedExtraParams = (android.os.IBinder.FIRST_CALL_TRANSACTION + 9); | |||
| } | |||
| public int isBillingSupported(int apiVersion, java.lang.String packageName, java.lang.String type) throws android.os.RemoteException; | |||
| /** | |||
| * Provides details of a list of SKUs | |||
| * Given a list of SKUs of a valid type in the skusBundle, this returns a bundle | |||
| * with a list JSON strings containing the productId, price, title and description. | |||
| * This API can be called with a maximum of 20 SKUs. | |||
| * @param apiVersion billing API version that the app is using | |||
| * @param packageName the package name of the calling app | |||
| * @param type of the in-app items ("inapp" for one-time purchases | |||
| * and "subs" for subscriptions) | |||
| * @param skusBundle bundle containing a StringArrayList of SKUs with key "ITEM_ID_LIST" | |||
| * @return Bundle containing the following key-value pairs | |||
| * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes | |||
| * on failures. | |||
| * "DETAILS_LIST" with a StringArrayList containing purchase information | |||
| * in JSON format similar to: | |||
| * '{ "productId" : "exampleSku", | |||
| * "type" : "inapp", | |||
| * "price" : "$5.00", | |||
| * "price_currency": "USD", | |||
| * "price_amount_micros": 5000000, | |||
| * "title : "Example Title", | |||
| * "description" : "This is an example description" }' | |||
| */ | |||
| public android.os.Bundle getSkuDetails(int apiVersion, java.lang.String packageName, java.lang.String type, android.os.Bundle skusBundle) throws android.os.RemoteException; | |||
| /** | |||
| * Returns a pending intent to launch the purchase flow for an in-app item by providing a SKU, | |||
| * the type, a unique purchase token and an optional developer payload. | |||
| * @param apiVersion billing API version that the app is using | |||
| * @param packageName package name of the calling app | |||
| * @param sku the SKU of the in-app item as published in the developer console | |||
| * @param type of the in-app item being purchased ("inapp" for one-time purchases | |||
| * and "subs" for subscriptions) | |||
| * @param developerPayload optional argument to be sent back with the purchase information | |||
| * @return Bundle containing the following key-value pairs | |||
| * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes | |||
| * on failures. | |||
| * "BUY_INTENT" - PendingIntent to start the purchase flow | |||
| * | |||
| * The Pending intent should be launched with startIntentSenderForResult. When purchase flow | |||
| * has completed, the onActivityResult() will give a resultCode of OK or CANCELED. | |||
| * If the purchase is successful, the result data will contain the following key-value pairs | |||
| * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response | |||
| * codes on failures. | |||
| * "INAPP_PURCHASE_DATA" - String in JSON format similar to | |||
| * '{"orderId":"12999763169054705758.1371079406387615", | |||
| * "packageName":"com.example.app", | |||
| * "productId":"exampleSku", | |||
| * "purchaseTime":1345678900000, | |||
| * "purchaseToken" : "122333444455555", | |||
| * "developerPayload":"example developer payload" }' | |||
| * "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that | |||
| * was signed with the private key of the developer | |||
| */ | |||
| public android.os.Bundle getBuyIntent(int apiVersion, java.lang.String packageName, java.lang.String sku, java.lang.String type, java.lang.String developerPayload) throws android.os.RemoteException; | |||
| /** | |||
| * Returns the current SKUs owned by the user of the type and package name specified along with | |||
| * purchase information and a signature of the data to be validated. | |||
| * This will return all SKUs that have been purchased in V3 and managed items purchased using | |||
| * V1 and V2 that have not been consumed. | |||
| * @param apiVersion billing API version that the app is using | |||
| * @param packageName package name of the calling app | |||
| * @param type of the in-app items being requested ("inapp" for one-time purchases | |||
| * and "subs" for subscriptions) | |||
| * @param continuationToken to be set as null for the first call, if the number of owned | |||
| * skus are too many, a continuationToken is returned in the response bundle. | |||
| * This method can be called again with the continuation token to get the next set of | |||
| * owned skus. | |||
| * @return Bundle containing the following key-value pairs | |||
| * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes | |||
| on failures. | |||
| * "INAPP_PURCHASE_ITEM_LIST" - StringArrayList containing the list of SKUs | |||
| * "INAPP_PURCHASE_DATA_LIST" - StringArrayList containing the purchase information | |||
| * "INAPP_DATA_SIGNATURE_LIST"- StringArrayList containing the signatures | |||
| * of the purchase information | |||
| * "INAPP_CONTINUATION_TOKEN" - String containing a continuation token for the | |||
| * next set of in-app purchases. Only set if the | |||
| * user has more owned skus than the current list. | |||
| */ | |||
| public android.os.Bundle getPurchases(int apiVersion, java.lang.String packageName, java.lang.String type, java.lang.String continuationToken) throws android.os.RemoteException; | |||
| public int consumePurchase(int apiVersion, java.lang.String packageName, java.lang.String purchaseToken) throws android.os.RemoteException; | |||
| public int stub(int apiVersion, java.lang.String packageName, java.lang.String type) throws android.os.RemoteException; | |||
| /** | |||
| * Returns a pending intent to launch the purchase flow for upgrading or downgrading a | |||
| * subscription. The existing owned SKU(s) should be provided along with the new SKU that | |||
| * the user is upgrading or downgrading to. | |||
| * @param apiVersion billing API version that the app is using, must be 5 or later | |||
| * @param packageName package name of the calling app | |||
| * @param oldSkus the SKU(s) that the user is upgrading or downgrading from, | |||
| * if null or empty this method will behave like {@link #getBuyIntent} | |||
| * @param newSku the SKU that the user is upgrading or downgrading to | |||
| * @param type of the item being purchased, currently must be "subs" | |||
| * @param developerPayload optional argument to be sent back with the purchase information | |||
| * @return Bundle containing the following key-value pairs | |||
| * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes | |||
| * on failures. | |||
| * "BUY_INTENT" - PendingIntent to start the purchase flow | |||
| * | |||
| * The Pending intent should be launched with startIntentSenderForResult. When purchase flow | |||
| * has completed, the onActivityResult() will give a resultCode of OK or CANCELED. | |||
| * If the purchase is successful, the result data will contain the following key-value pairs | |||
| * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response | |||
| * codes on failures. | |||
| * "INAPP_PURCHASE_DATA" - String in JSON format similar to | |||
| * '{"orderId":"12999763169054705758.1371079406387615", | |||
| * "packageName":"com.example.app", | |||
| * "productId":"exampleSku", | |||
| * "purchaseTime":1345678900000, | |||
| * "purchaseToken" : "122333444455555", | |||
| * "developerPayload":"example developer payload" }' | |||
| * "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that | |||
| * was signed with the private key of the developer | |||
| */ | |||
| public android.os.Bundle getBuyIntentToReplaceSkus(int apiVersion, java.lang.String packageName, java.util.List<java.lang.String> oldSkus, java.lang.String newSku, java.lang.String type, java.lang.String developerPayload) throws android.os.RemoteException; | |||
| /** | |||
| * Returns a pending intent to launch the purchase flow for an in-app item. This method is | |||
| * a variant of the {@link #getBuyIntent} method and takes an additional {@code extraParams} | |||
| * parameter. This parameter is a Bundle of optional keys and values that affect the | |||
| * operation of the method. | |||
| * @param apiVersion billing API version that the app is using, must be 6 or later | |||
| * @param packageName package name of the calling app | |||
| * @param sku the SKU of the in-app item as published in the developer console | |||
| * @param type of the in-app item being purchased ("inapp" for one-time purchases | |||
| * and "subs" for subscriptions) | |||
| * @param developerPayload optional argument to be sent back with the purchase information | |||
| * @extraParams a Bundle with the following optional keys: | |||
| * "skusToReplace" - List<String> - an optional list of SKUs that the user is | |||
| * upgrading or downgrading from. | |||
| * Pass this field if the purchase is upgrading or downgrading | |||
| * existing subscriptions. | |||
| * The specified SKUs are replaced with the SKUs that the user is | |||
| * purchasing. Google Play replaces the specified SKUs at the start of | |||
| * the next billing cycle. | |||
| * "replaceSkusProration" - Boolean - whether the user should be credited for any unused | |||
| * subscription time on the SKUs they are upgrading or downgrading. | |||
| * If you set this field to true, Google Play swaps out the old SKUs | |||
| * and credits the user with the unused value of their subscription | |||
| * time on a pro-rated basis. | |||
| * Google Play applies this credit to the new subscription, and does | |||
| * not begin billing the user for the new subscription until after | |||
| * the credit is used up. | |||
| * If you set this field to false, the user does not receive credit for | |||
| * any unused subscription time and the recurrence date does not | |||
| * change. | |||
| * Default value is true. Ignored if you do not pass skusToReplace. | |||
| * "accountId" - String - an optional obfuscated string that is uniquely | |||
| * associated with the user's account in your app. | |||
| * If you pass this value, Google Play can use it to detect irregular | |||
| * activity, such as many devices making purchases on the same | |||
| * account in a short period of time. | |||
| * Do not use the developer ID or the user's Google ID for this field. | |||
| * In addition, this field should not contain the user's ID in | |||
| * cleartext. | |||
| * We recommend that you use a one-way hash to generate a string from | |||
| * the user's ID, and store the hashed string in this field. | |||
| * "vr" - Boolean - an optional flag indicating whether the returned intent | |||
| * should start a VR purchase flow. The apiVersion must also be 7 or | |||
| * later to use this flag. | |||
| */ | |||
| public android.os.Bundle getBuyIntentExtraParams(int apiVersion, java.lang.String packageName, java.lang.String sku, java.lang.String type, java.lang.String developerPayload, android.os.Bundle extraParams) throws android.os.RemoteException; | |||
| /** | |||
| * Returns the most recent purchase made by the user for each SKU, even if that purchase is | |||
| * expired, canceled, or consumed. | |||
| * @param apiVersion billing API version that the app is using, must be 6 or later | |||
| * @param packageName package name of the calling app | |||
| * @param type of the in-app items being requested ("inapp" for one-time purchases | |||
| * and "subs" for subscriptions) | |||
| * @param continuationToken to be set as null for the first call, if the number of owned | |||
| * skus is too large, a continuationToken is returned in the response bundle. | |||
| * This method can be called again with the continuation token to get the next set of | |||
| * owned skus. | |||
| * @param extraParams a Bundle with extra params that would be appended into http request | |||
| * query string. Not used at this moment. Reserved for future functionality. | |||
| * @return Bundle containing the following key-value pairs | |||
| * "RESPONSE_CODE" with int value: RESULT_OK(0) if success, | |||
| * {@link IabHelper#BILLING_RESPONSE_RESULT_*} response codes on failures. | |||
| * | |||
| * "INAPP_PURCHASE_ITEM_LIST" - ArrayList<String> containing the list of SKUs | |||
| * "INAPP_PURCHASE_DATA_LIST" - ArrayList<String> containing the purchase information | |||
| * "INAPP_DATA_SIGNATURE_LIST"- ArrayList<String> containing the signatures | |||
| * of the purchase information | |||
| * "INAPP_CONTINUATION_TOKEN" - String containing a continuation token for the | |||
| * next set of in-app purchases. Only set if the | |||
| * user has more owned skus than the current list. | |||
| */ | |||
| public android.os.Bundle getPurchaseHistory(int apiVersion, java.lang.String packageName, java.lang.String type, java.lang.String continuationToken, android.os.Bundle extraParams) throws android.os.RemoteException; | |||
| public int isBillingSupportedExtraParams(int apiVersion, java.lang.String packageName, java.lang.String type, android.os.Bundle extraParams) throws android.os.RemoteException; | |||
| } | |||
| @@ -66,7 +66,7 @@ import java.util.concurrent.Callable; | |||
| import java.util.concurrent.TimeoutException; | |||
| import java.util.concurrent.locks.ReentrantLock; | |||
| import java.util.concurrent.atomic.*; | |||
| $$JuceAndroidMidiImports$$ // If you get an error here, you need to re-save your project with the Projucer! | |||
| $$JuceAndroidMidiImports$$ // If you get an error here, you need to re-save your project with the Projucer! | |||
| //============================================================================== | |||
| @@ -299,6 +299,7 @@ public class JuceAppActivity extends Activity | |||
| private native void suspendApp(); | |||
| private native void resumeApp(); | |||
| private native void setScreenSize (int screenWidth, int screenHeight, int dpi); | |||
| private native void appActivityResult (int requestCode, int resultCode, Intent data); | |||
| //============================================================================== | |||
| private ViewHolder viewHolder; | |||
| @@ -1197,6 +1198,13 @@ public class JuceAppActivity extends Activity | |||
| public static final String getMoviesFolder() { return getFileLocation (Environment.DIRECTORY_MOVIES); } | |||
| public static final String getDownloadsFolder() { return getFileLocation (Environment.DIRECTORY_DOWNLOADS); } | |||
| //============================================================================== | |||
| @Override | |||
| protected void onActivityResult (int requestCode, int resultCode, Intent data) | |||
| { | |||
| appActivityResult (requestCode, resultCode, data); | |||
| } | |||
| //============================================================================== | |||
| public final Typeface getTypeFaceFromAsset (String assetName) | |||
| { | |||
| @@ -279,27 +279,27 @@ extern AndroidSystem android; | |||
| //============================================================================== | |||
| #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ | |||
| METHOD (createNewView, "createNewView", "(ZJ)L" JUCE_ANDROID_ACTIVITY_CLASSPATH "$ComponentPeerView;") \ | |||
| METHOD (deleteView, "deleteView", "(L" JUCE_ANDROID_ACTIVITY_CLASSPATH "$ComponentPeerView;)V") \ | |||
| METHOD (createNewView, "createNewView", "(ZJ)L" JUCE_ANDROID_ACTIVITY_CLASSPATH "$ComponentPeerView;") \ | |||
| METHOD (deleteView, "deleteView", "(L" JUCE_ANDROID_ACTIVITY_CLASSPATH "$ComponentPeerView;)V") \ | |||
| METHOD (createNativeSurfaceView, "createNativeSurfaceView", "(J)L" JUCE_ANDROID_ACTIVITY_CLASSPATH "$NativeSurfaceView;") \ | |||
| METHOD (finish, "finish", "()V") \ | |||
| METHOD (finish, "finish", "()V") \ | |||
| METHOD (setRequestedOrientation,"setRequestedOrientation", "(I)V") \ | |||
| METHOD (getClipboardContent, "getClipboardContent", "()Ljava/lang/String;") \ | |||
| METHOD (setClipboardContent, "setClipboardContent", "(Ljava/lang/String;)V") \ | |||
| METHOD (excludeClipRegion, "excludeClipRegion", "(Landroid/graphics/Canvas;FFFF)V") \ | |||
| METHOD (renderGlyph, "renderGlyph", "(CCLandroid/graphics/Paint;Landroid/graphics/Matrix;Landroid/graphics/Rect;)[I") \ | |||
| STATICMETHOD (createHTTPStream, "createHTTPStream", "(Ljava/lang/String;Z[BLjava/lang/String;I[ILjava/lang/StringBuffer;ILjava/lang/String;)L" JUCE_ANDROID_ACTIVITY_CLASSPATH "$HTTPStream;") \ | |||
| METHOD (launchURL, "launchURL", "(Ljava/lang/String;)V") \ | |||
| METHOD (showMessageBox, "showMessageBox", "(Ljava/lang/String;Ljava/lang/String;J)V") \ | |||
| METHOD (showOkCancelBox, "showOkCancelBox", "(Ljava/lang/String;Ljava/lang/String;JLjava/lang/String;Ljava/lang/String;)V") \ | |||
| METHOD (showYesNoCancelBox, "showYesNoCancelBox", "(Ljava/lang/String;Ljava/lang/String;J)V") \ | |||
| STATICMETHOD (getLocaleValue, "getLocaleValue", "(Z)Ljava/lang/String;") \ | |||
| STATICMETHOD (getDocumentsFolder, "getDocumentsFolder", "()Ljava/lang/String;") \ | |||
| STATICMETHOD (getPicturesFolder, "getPicturesFolder", "()Ljava/lang/String;") \ | |||
| STATICMETHOD (getMusicFolder, "getMusicFolder", "()Ljava/lang/String;") \ | |||
| STATICMETHOD (getDownloadsFolder, "getDownloadsFolder", "()Ljava/lang/String;") \ | |||
| STATICMETHOD (getMoviesFolder, "getMoviesFolder", "()Ljava/lang/String;") \ | |||
| METHOD (getTypeFaceFromAsset, "getTypeFaceFromAsset", "(Ljava/lang/String;)Landroid/graphics/Typeface;") \ | |||
| METHOD (getClipboardContent, "getClipboardContent", "()Ljava/lang/String;") \ | |||
| METHOD (setClipboardContent, "setClipboardContent", "(Ljava/lang/String;)V") \ | |||
| METHOD (excludeClipRegion, "excludeClipRegion", "(Landroid/graphics/Canvas;FFFF)V") \ | |||
| METHOD (renderGlyph, "renderGlyph", "(CCLandroid/graphics/Paint;Landroid/graphics/Matrix;Landroid/graphics/Rect;)[I") \ | |||
| STATICMETHOD (createHTTPStream, "createHTTPStream", "(Ljava/lang/String;Z[BLjava/lang/String;I[ILjava/lang/StringBuffer;ILjava/lang/String;)L" JUCE_ANDROID_ACTIVITY_CLASSPATH "$HTTPStream;") \ | |||
| METHOD (launchURL, "launchURL", "(Ljava/lang/String;)V") \ | |||
| METHOD (showMessageBox, "showMessageBox", "(Ljava/lang/String;Ljava/lang/String;J)V") \ | |||
| METHOD (showOkCancelBox, "showOkCancelBox", "(Ljava/lang/String;Ljava/lang/String;JLjava/lang/String;Ljava/lang/String;)V") \ | |||
| METHOD (showYesNoCancelBox, "showYesNoCancelBox", "(Ljava/lang/String;Ljava/lang/String;J)V") \ | |||
| STATICMETHOD (getLocaleValue, "getLocaleValue", "(Z)Ljava/lang/String;") \ | |||
| STATICMETHOD (getDocumentsFolder, "getDocumentsFolder", "()Ljava/lang/String;") \ | |||
| STATICMETHOD (getPicturesFolder, "getPicturesFolder", "()Ljava/lang/String;") \ | |||
| STATICMETHOD (getMusicFolder, "getMusicFolder", "()Ljava/lang/String;") \ | |||
| STATICMETHOD (getDownloadsFolder, "getDownloadsFolder", "()Ljava/lang/String;") \ | |||
| STATICMETHOD (getMoviesFolder, "getMoviesFolder", "()Ljava/lang/String;") \ | |||
| METHOD (getTypeFaceFromAsset, "getTypeFaceFromAsset", "(Ljava/lang/String;)Landroid/graphics/Typeface;") \ | |||
| METHOD (getTypeFaceFromByteArray,"getTypeFaceFromByteArray","([B)Landroid/graphics/Typeface;") \ | |||
| METHOD (setScreenSaver, "setScreenSaver", "(Z)V") \ | |||
| METHOD (getScreenSaver, "getScreenSaver", "()Z") \ | |||
| @@ -307,12 +307,16 @@ extern AndroidSystem android; | |||
| METHOD (getAndroidBluetoothManager, "getAndroidBluetoothManager", "()L" JUCE_ANDROID_ACTIVITY_CLASSPATH "$BluetoothManager;") \ | |||
| METHOD (getAndroidSDKVersion, "getAndroidSDKVersion", "()I") \ | |||
| METHOD (audioManagerGetProperty, "audioManagerGetProperty", "(Ljava/lang/String;)Ljava/lang/String;") \ | |||
| METHOD (hasSystemFeature, "hasSystemFeature", "(Ljava/lang/String;)Z" ) \ | |||
| METHOD (hasSystemFeature, "hasSystemFeature", "(Ljava/lang/String;)Z" ) \ | |||
| METHOD (requestRuntimePermission, "requestRuntimePermission", "(IJ)V" ) \ | |||
| METHOD (isPermissionGranted, "isPermissionGranted", "(I)Z" ) \ | |||
| METHOD (isPermissionGranted, "isPermissionGranted", "(I)Z" ) \ | |||
| METHOD (isPermissionDeclaredInManifest, "isPermissionDeclaredInManifest", "(I)Z" ) \ | |||
| METHOD (getSystemService, "getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;") \ | |||
| STATICMETHOD (createInvocationHandler, "createInvocationHandler", "(J)Ljava/lang/reflect/InvocationHandler;") \ | |||
| METHOD (bindService, "bindService", "(Landroid/content/Intent;Landroid/content/ServiceConnection;I)Z") \ | |||
| METHOD (unbindService, "unbindService", "(Landroid/content/ServiceConnection;)V") \ | |||
| METHOD (startIntentSenderForResult, "startIntentSenderForResult", "(Landroid/content/IntentSender;ILandroid/content/Intent;III)V") \ | |||
| METHOD (getPackageName, "getPackageName", "()Ljava/lang/String;") \ | |||
| DECLARE_JNI_CLASS (JuceAppActivity, JUCE_ANDROID_ACTIVITY_CLASSPATH); | |||
| #undef JNI_CLASS_MEMBERS | |||
| @@ -59,6 +59,16 @@ namespace | |||
| return createNSURLFromFile (f.getFullPathName()); | |||
| } | |||
| static inline NSArray* createNSArrayFromStringArray (const StringArray& strings) | |||
| { | |||
| auto* array = [[NSMutableArray alloc] init]; | |||
| for (auto string: strings) | |||
| [array addObject:juceStringToNS (string)]; | |||
| return [array autorelease]; | |||
| } | |||
| #if JUCE_MAC | |||
| template <typename RectangleType> | |||
| static NSRect makeNSRect (const RectangleType& r) noexcept | |||
| @@ -31,6 +31,11 @@ extern juce::JUCEApplicationBase* juce_CreateApplication(); // (from START_JUCE_ | |||
| namespace juce | |||
| { | |||
| //============================================================================== | |||
| #if JUCE_MODULE_AVAILABLE_juce_product_unlocking | |||
| extern void juce_inAppPurchaseCompleted (jobject intentData); | |||
| #endif | |||
| //============================================================================== | |||
| JUCE_JNI_CALLBACK (JUCE_ANDROID_ACTIVITY_CLASSNAME, launchApp, void, (JNIEnv* env, jobject activity, | |||
| jstring appFile, jstring appDataDir)) | |||
| @@ -83,6 +88,18 @@ JUCE_JNI_CALLBACK (JUCE_ANDROID_ACTIVITY_CLASSNAME, quitApp, void, (JNIEnv* env, | |||
| android.shutdown (env); | |||
| } | |||
| JUCE_JNI_CALLBACK (JUCE_ANDROID_ACTIVITY_CLASSNAME, appActivityResult, void, (JNIEnv* env, jobject, jint requestCode, jint /*resultCode*/, jobject intentData)) | |||
| { | |||
| setEnv (env); | |||
| #if JUCE_MODULE_AVAILABLE_juce_product_unlocking | |||
| if (requestCode == 1001) | |||
| juce_inAppPurchaseCompleted (intentData); | |||
| #else | |||
| ignoreUnused (intentData, requestCode); | |||
| #endif | |||
| } | |||
| //============================================================================== | |||
| #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ | |||
| METHOD (drawBitmap, "drawBitmap", "([IIIFFIIZLandroid/graphics/Paint;)V") \ | |||
| @@ -0,0 +1,66 @@ | |||
| /* | |||
| ============================================================================== | |||
| This file is part of the JUCE library. | |||
| Copyright (c) 2017 - ROLI Ltd. | |||
| JUCE is an open source library subject to commercial or open-source | |||
| licensing. | |||
| By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
| Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
| 27th April 2017). | |||
| End User License Agreement: www.juce.com/juce-5-licence | |||
| Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
| Or: You may also use this code under the terms of the GPL v3 (see | |||
| www.gnu.org/licenses). | |||
| JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
| EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
| DISCLAIMED. | |||
| ============================================================================== | |||
| */ | |||
| InAppPurchases::InAppPurchases() : pimpl (new Pimpl (*this)) {} | |||
| InAppPurchases::~InAppPurchases() {} | |||
| bool InAppPurchases::isInAppPurchasesSupported() const | |||
| { | |||
| return pimpl->isInAppPurchasesSupported(); | |||
| } | |||
| void InAppPurchases::getProductsInformation (const StringArray& productIdentifiers) | |||
| { | |||
| pimpl->getProductsInformation (productIdentifiers); | |||
| } | |||
| void InAppPurchases::purchaseProduct (const String& productIdentifier, | |||
| bool isSubscription, | |||
| const StringArray& upgradeProductIdentifiers, | |||
| bool creditForUnusedSubscription) | |||
| { | |||
| pimpl->purchaseProduct (productIdentifier, isSubscription, | |||
| upgradeProductIdentifiers, creditForUnusedSubscription); | |||
| } | |||
| void InAppPurchases::restoreProductsBoughtList (bool includeDownloadInfo, const String& subscriptionsSharedSecret) | |||
| { | |||
| pimpl->restoreProductsBoughtList (includeDownloadInfo, subscriptionsSharedSecret); | |||
| } | |||
| void InAppPurchases::consumePurchase (const String& productIdentifier, const String& purchaseToken) | |||
| { | |||
| pimpl->consumePurchase (productIdentifier, purchaseToken); | |||
| } | |||
| void InAppPurchases::addListener (Listener* l) { listeners.add (l); } | |||
| void InAppPurchases::removeListener (Listener* l) { listeners.remove (l); } | |||
| void InAppPurchases::startDownloads (const Array<Download*>& downloads) { pimpl->startDownloads (downloads); } | |||
| void InAppPurchases::pauseDownloads (const Array<Download*>& downloads) { pimpl->pauseDownloads (downloads); } | |||
| void InAppPurchases::resumeDownloads (const Array<Download*>& downloads) { pimpl->resumeDownloads (downloads); } | |||
| void InAppPurchases::cancelDownloads (const Array<Download*>& downloads) { pimpl->cancelDownloads (downloads); } | |||
| @@ -0,0 +1,269 @@ | |||
| /* | |||
| ============================================================================== | |||
| This file is part of the JUCE library. | |||
| Copyright (c) 2017 - ROLI Ltd. | |||
| JUCE is an open source library subject to commercial or open-source | |||
| licensing. | |||
| By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
| Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
| 27th April 2017). | |||
| End User License Agreement: www.juce.com/juce-5-licence | |||
| Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
| Or: You may also use this code under the terms of the GPL v3 (see | |||
| www.gnu.org/licenses). | |||
| JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
| EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
| DISCLAIMED. | |||
| ============================================================================== | |||
| */ | |||
| /** | |||
| Provides in-app purchase functionality. | |||
| Your app should create a single instance of this class, and on iOS it should | |||
| be created as soon as your app starts. This is because on application startup | |||
| any previously pending transactions will be resumed. | |||
| Once an InAppPurchases object is created, call addListener() to attach listeners. | |||
| */ | |||
| class JUCE_API InAppPurchases | |||
| { | |||
| public: | |||
| //============================================================================== | |||
| /** Represents a product available in the store. */ | |||
| struct Product | |||
| { | |||
| /** Product ID (also known as SKU) that uniquely identifies a product in the store. */ | |||
| String identifier; | |||
| /** Title of the product. */ | |||
| String title; | |||
| /** Description of the product. */ | |||
| String description; | |||
| /** Price of the product in local currency. */ | |||
| String price; | |||
| /** Price locale. */ | |||
| String priceLocale; | |||
| }; | |||
| //============================================================================== | |||
| /** Represents a purchase of a product in the store. */ | |||
| struct Purchase | |||
| { | |||
| /** A unique order identifier for the transaction (generated by the store). */ | |||
| String orderId; | |||
| /** A unique identifier of in-app product that was purchased. */ | |||
| String productId; | |||
| /** This will be bundle ID on iOS and package name on Android, of the application for which this | |||
| in-app product was purchased. */ | |||
| String applicationBundleName; | |||
| /** Date of the purchase (in ISO8601 format). */ | |||
| String purchaseTime; | |||
| /** Android only: purchase token that should be used to consume purchase, provided that In-App product | |||
| is consumable. */ | |||
| String purchaseToken; | |||
| }; | |||
| //============================================================================== | |||
| /** iOS only: represents in-app purchase download. Download will be available only | |||
| for purchases that are hosted on the AppStore. */ | |||
| struct Download | |||
| { | |||
| enum class Status | |||
| { | |||
| waiting = 0, /**< The download is waiting to start. Called at the beginning of a download operation. */ | |||
| active, /**< The download is in progress. */ | |||
| paused, /**< The download was paused and is awaiting resuming or cancelling. */ | |||
| finished, /**< The download was finished successfully. */ | |||
| failed, /**< The download failed (e.g. because of no internet connection). */ | |||
| cancelled, /**< The download was cancelled. */ | |||
| }; | |||
| virtual ~Download() {} | |||
| /** A unique identifier for the in-app product to be downloaded. */ | |||
| virtual String getProductId() const = 0; | |||
| /** Content length in bytes. */ | |||
| virtual int64 getContentLength() const = 0; | |||
| /** Content version. */ | |||
| virtual String getContentVersion() const = 0; | |||
| /** Returns current status of the download. */ | |||
| virtual Status getStatus() const = 0; | |||
| }; | |||
| //============================================================================== | |||
| /** Represents an object that gets notified about events such as product info returned or product purchase | |||
| finished. */ | |||
| struct Listener | |||
| { | |||
| virtual ~Listener() {} | |||
| /** Called whenever a product info is returned after a call to InAppPurchases::getProductsInformation(). */ | |||
| virtual void productsInfoReturned (const Array<Product>& /*products*/) {} | |||
| struct PurchaseInfo | |||
| { | |||
| Purchase purchase; | |||
| Array<Download*> downloads; | |||
| }; | |||
| /** Called whenever a purchase is complete, with additional state whether the purchase completed successfully. | |||
| For hosted content (iOS only), the downloads array within PurchaseInfo will contain all download objects corresponding | |||
| with the purchase. For non-hosted content, the downloads array will be empty. | |||
| InAppPurchases class will own downloads and will delete them as soon as they are finished. | |||
| NOTE: it is possible to receive this callback for the same purchase multiple times. If that happens, | |||
| only the newest set of downloads and the newest orderId will be valid, the old ones should be not used anymore! | |||
| */ | |||
| virtual void productPurchaseFinished (const PurchaseInfo&, bool /*success*/, const String& /*statusDescription*/) {} | |||
| /** Called when a list of all purchases is restored. This can be used to figure out to | |||
| which products a user is entitled to. | |||
| NOTE: it is possible to receive this callback for the same purchase multiple times. If that happens, | |||
| only the newest set of downloads and the newest orderId will be valid, the old ones should be not used anymore! | |||
| */ | |||
| virtual void purchasesListRestored (const Array<PurchaseInfo>&, bool /*success*/, const String& /*statusDescription*/) {} | |||
| /** Called whenever a product consumption finishes. */ | |||
| virtual void productConsumed (const String& /*productId*/, bool /*success*/, const String& /*statusDescription*/) {} | |||
| /** iOS only: Called when a product download progress gets updated. If the download was interrupted in the last | |||
| application session, this callback may be called after the application starts. | |||
| If the download was in progress and the application was closed, the download may happily continue in the | |||
| background by OS. If you open the app and the download is still in progress, you will receive this callback. | |||
| If the download finishes in the background before you start the app again, you will receive productDownloadFinished | |||
| callback instead. The download will only stop when it is explicitly cancelled or when it is finished. | |||
| */ | |||
| virtual void productDownloadProgressUpdate (Download&, float /*progress*/, RelativeTime /*timeRemaining*/) {} | |||
| /** iOS only: Called when a product download is paused. This may also be called after the application starts, if | |||
| the download was in a paused state and the application was closed before finishing the download. | |||
| Only after the download is finished successfully or cancelled you will stop receiving this callback on startup. | |||
| */ | |||
| virtual void productDownloadPaused (Download&) {} | |||
| /** iOS only: Called when a product download finishes (successfully or not). Call Download::getStatus() | |||
| to check if the downloaded finished successfully. | |||
| It is your responsibility to move the download content into your app directory and to clean up | |||
| any files that are no longer needed. | |||
| After the download is finished, the download object is destroyed and should not be accessed anymore. | |||
| */ | |||
| virtual void productDownloadFinished (Download&, const URL& /*downloadedContentPath*/) {} | |||
| }; | |||
| //============================================================================== | |||
| /** Checks whether in-app purchases is supported on current platform. On iOS this always returns true. */ | |||
| bool isInAppPurchasesSupported() const; | |||
| /** Asynchronously requests information for products with given ids. Upon completion, for each enquired product | |||
| there is going to be a corresponding @class Product object. | |||
| If there is no information available for the given product identifier, it will be ignored. | |||
| */ | |||
| void getProductsInformation (const StringArray& productIdentifiers); | |||
| /** Asynchronously requests to buy a product with given id. | |||
| @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 creditForUnusedSubscription (Android only) controls whether a user should be credited for any unused subscription time on | |||
| the products that are being upgraded or downgraded. | |||
| */ | |||
| void purchaseProduct (const String& productIdentifier, | |||
| bool isSubscription, | |||
| const StringArray& upgradeOrDowngradeFromSubscriptionsWithProductIdentifiers = {}, | |||
| bool creditForUnusedSubscription = true); | |||
| /** Asynchronously asks about a list of products that a user has already bought. Upon completion, Listener::purchasesListReceived() | |||
| callback will be invoked. The user may be prompted to login first. | |||
| @param includeDownloadInfo (iOS only) if true, then after restoration is successfull, the downloads array passed to | |||
| Listener::purchasesListReceived() callback will contain all the download objects corresponding with | |||
| the purchase. In the opposite case, the downloads array will be empty. | |||
| @param subscriptionsSharedSecret (iOS only) required when not including download information and when there are | |||
| auto-renewable subscription set up with this app. Refer to In-App-Purchase settings in the store. | |||
| */ | |||
| void restoreProductsBoughtList (bool includeDownloadInfo, const juce::String& subscriptionsSharedSecret = {}); | |||
| /** Android only: asynchronously sends a request to mark a purchase with given identifier as consumed. | |||
| To consume a product, provide product identifier as well as a purchase token that was generated when | |||
| the product was purchased. The purchase token can also be retrieved by using getProductsInformation(). | |||
| In general if it is available on hand, it is better to use it, because otherwise another async | |||
| request will be sent to the store, to first retrieve the token. | |||
| After successful consumption, a product will no longer be returned in getProductsBought() and | |||
| it will be available for purchase. | |||
| On iOS consumption happens automatically. If the product was set as consumable, this function is a no-op. | |||
| */ | |||
| void consumePurchase (const String& productIdentifier, const String& purchaseToken = {}); | |||
| //============================================================================== | |||
| /** Adds a listener. */ | |||
| void addListener (Listener*); | |||
| /** Removes a listener. */ | |||
| void removeListener (Listener*); | |||
| //============================================================================== | |||
| /** iOS only: Starts downloads of hosted content from the store. */ | |||
| void startDownloads (const Array<Download*>& downloads); | |||
| /** iOS only: Pauses downloads of hosted content from the store. */ | |||
| void pauseDownloads (const Array<Download*>& downloads); | |||
| /** iOS only: Resumes downloads of hosted content from the store. */ | |||
| void resumeDownloads (const Array<Download*>& downloads); | |||
| /** iOS only: Cancels downloads of hosted content from the store. */ | |||
| void cancelDownloads (const Array<Download*>& downloads); | |||
| //============================================================================== | |||
| #ifndef DOXYGEN | |||
| InAppPurchases(); | |||
| ~InAppPurchases(); | |||
| #endif | |||
| private: | |||
| //============================================================================== | |||
| ListenerList<Listener> listeners; | |||
| #if JUCE_ANDROID | |||
| friend void juce_inAppPurchaseCompleted (jobject); | |||
| #endif | |||
| struct Pimpl; | |||
| friend struct Pimpl; | |||
| ScopedPointer<Pimpl> pimpl; | |||
| }; | |||
| @@ -33,10 +33,30 @@ | |||
| #error "Incorrect use of JUCE cpp file" | |||
| #endif | |||
| #define JUCE_CORE_INCLUDE_JNI_HELPERS 1 | |||
| #define JUCE_CORE_INCLUDE_OBJC_HELPERS 1 | |||
| #define JUCE_CORE_INCLUDE_NATIVE_HEADERS 1 | |||
| // Set this flag to 1 to use test servers on iOS | |||
| #ifndef JUCE_IN_APP_PURCHASES_USE_SANDBOX_ENVIRONMENT | |||
| #define JUCE_IN_APP_PURCHASES_USE_SANDBOX_ENVIRONMENT 0 | |||
| #endif | |||
| #include "juce_product_unlocking.h" | |||
| #if JUCE_IOS || JUCE_MAC | |||
| #import <StoreKit/StoreKit.h> | |||
| #endif | |||
| namespace juce | |||
| { | |||
| #if JUCE_ANDROID | |||
| #include "native/juce_android_InAppPurchases.cpp" | |||
| #elif JUCE_IOS | |||
| #include "native/juce_ios_InAppPurchases.cpp" | |||
| #endif | |||
| #include "in_app_purchases/juce_InAppPurchases.cpp" | |||
| #include "marketplace/juce_OnlineUnlockStatus.cpp" | |||
| #if JUCE_MODULE_AVAILABLE_juce_data_structures | |||
| @@ -41,7 +41,7 @@ | |||
| website: http://www.juce.com/juce | |||
| license: GPL/Commercial | |||
| dependencies: juce_cryptography | |||
| dependencies: juce_cryptography juce_core | |||
| END_JUCE_MODULE_DECLARATION | |||
| @@ -60,9 +60,13 @@ | |||
| Tracktion Marketplace web-store, the module itself is fully open, and can | |||
| be used to connect to your own web-store instead, if you implement your | |||
| own compatible web-server back-end. | |||
| In additional, the module supports in-app purchases both on iOS and Android | |||
| platforms. | |||
| */ | |||
| //============================================================================== | |||
| #include <juce_core/juce_core.h> | |||
| #include <juce_cryptography/juce_cryptography.h> | |||
| #if JUCE_MODULE_AVAILABLE_juce_data_structures | |||
| @@ -75,6 +79,10 @@ | |||
| namespace juce | |||
| { | |||
| #if JUCE_ANDROID || JUCE_IOS | |||
| #include "in_app_purchases/juce_InAppPurchases.h" | |||
| #endif | |||
| #if JUCE_MODULE_AVAILABLE_juce_data_structures | |||
| #include "marketplace/juce_OnlineUnlockStatus.h" | |||
| #include "marketplace/juce_TracktionMarketplaceStatus.h" | |||
| @@ -0,0 +1,23 @@ | |||
| /* | |||
| ============================================================================== | |||
| This file is part of the JUCE library. | |||
| Copyright (c) 2017 - ROLI Ltd. | |||
| JUCE is an open source library subject to commercial or open-source | |||
| licensing. | |||
| The code included in this file is provided under the terms of the ISC license | |||
| http://www.isc.org/downloads/software-support-policy/isc-license. Permission | |||
| To use, copy, modify, and/or distribute this software for any purpose with or | |||
| without fee is hereby granted provided that the above copyright notice and | |||
| this permission notice appear in all copies. | |||
| JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
| EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
| DISCLAIMED. | |||
| ============================================================================== | |||
| */ | |||
| #include "juce_product_unlocking.cpp" | |||
| @@ -0,0 +1,930 @@ | |||
| /* | |||
| ============================================================================== | |||
| This file is part of the JUCE library. | |||
| Copyright (c) 2017 - ROLI Ltd. | |||
| JUCE is an open source library subject to commercial or open-source | |||
| licensing. | |||
| By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
| Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
| 27th April 2017). | |||
| End User License Agreement: www.juce.com/juce-5-licence | |||
| Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
| Or: You may also use this code under the terms of the GPL v3 (see | |||
| www.gnu.org/licenses). | |||
| JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
| EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
| DISCLAIMED. | |||
| ============================================================================== | |||
| */ | |||
| #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ | |||
| METHOD (isBillingSupported, "isBillingSupported", "(ILjava/lang/String;Ljava/lang/String;)I") \ | |||
| METHOD (getSkuDetails, "getSkuDetails", "(ILjava/lang/String;Ljava/lang/String;Landroid/os/Bundle;)Landroid/os/Bundle;") \ | |||
| METHOD (getBuyIntent, "getBuyIntent", "(ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Landroid/os/Bundle;") \ | |||
| METHOD (getBuyIntentExtraParams, "getBuyIntentExtraParams", "(ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Landroid/os/Bundle;)Landroid/os/Bundle;") \ | |||
| METHOD (getPurchases, "getPurchases", "(ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;)Landroid/os/Bundle;") \ | |||
| METHOD (consumePurchase, "consumePurchase", "(ILjava/lang/String;Ljava/lang/String;)I") \ | |||
| METHOD (getPurchaseHistory, "getPurchaseHistory", "(ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;Landroid/os/Bundle;)Landroid/os/Bundle;") | |||
| DECLARE_JNI_CLASS (IInAppBillingService, "com/android/vending/billing/IInAppBillingService"); | |||
| #undef JNI_CLASS_MEMBERS | |||
| #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ | |||
| STATICMETHOD (asInterface, "asInterface", "(Landroid/os/IBinder;)Lcom/android/vending/billing/IInAppBillingService;") \ | |||
| DECLARE_JNI_CLASS (IInAppBillingServiceStub, "com/android/vending/billing/IInAppBillingService$Stub"); | |||
| #undef JNI_CLASS_MEMBERS | |||
| #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ | |||
| METHOD (constructor, "<init>", "(I)V") \ | |||
| METHOD (add, "add", "(Ljava/lang/Object;)Z") \ | |||
| METHOD (iterator, "iterator", "()Ljava/util/Iterator;") \ | |||
| METHOD (get, "get", "(I)Ljava/lang/Object;") \ | |||
| METHOD (size, "size", "()I") | |||
| DECLARE_JNI_CLASS (ArrayList, "java/util/ArrayList"); | |||
| #undef JNI_CLASS_MEMBERS | |||
| #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ | |||
| METHOD (constructor, "<init>", "()V") \ | |||
| METHOD (putBoolean, "putBoolean", "(Ljava/lang/String;Z)V") \ | |||
| METHOD (putStringArrayList, "putStringArrayList", "(Ljava/lang/String;Ljava/util/ArrayList;)V") \ | |||
| METHOD (getInt, "getInt", "(Ljava/lang/String;)I") \ | |||
| METHOD (getStringArrayList, "getStringArrayList", "(Ljava/lang/String;)Ljava/util/ArrayList;") \ | |||
| METHOD (getString, "getString", "(Ljava/lang/String;)Ljava/lang/String;") \ | |||
| METHOD (getParcelable, "getParcelable", "(Ljava/lang/String;)Landroid/os/Parcelable;") | |||
| DECLARE_JNI_CLASS (Bundle, "android/os/Bundle"); | |||
| #undef JNI_CLASS_MEMBERS | |||
| #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ | |||
| STATICMETHOD (valueOf, "valueOf", "(I)Ljava/lang/Integer;") | |||
| DECLARE_JNI_CLASS (Integer, "java/lang/Integer"); | |||
| #undef JNI_CLASS_MEMBERS | |||
| #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ | |||
| METHOD (constructor, "<init>", "()V") \ | |||
| METHOD (constructWithString, "<init>", "(Ljava/lang/String;)V") \ | |||
| METHOD (setPackage, "setPackage", "(Ljava/lang/String;)Landroid/content/Intent;") \ | |||
| METHOD (getIntExtra, "getIntExtra", "(Ljava/lang/String;I)I") \ | |||
| METHOD (getStringExtra, "getStringExtra", "(Ljava/lang/String;)Ljava/lang/String;") | |||
| DECLARE_JNI_CLASS (Intent, "android/content/Intent"); | |||
| #undef JNI_CLASS_MEMBERS | |||
| #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ | |||
| METHOD (hasNext, "hasNext", "()Z") \ | |||
| METHOD (next, "next", "()Ljava/lang/Object;") | |||
| DECLARE_JNI_CLASS (Iterator, "java/util/Iterator"); | |||
| #undef JNI_CLASS_MEMBERS | |||
| #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ | |||
| METHOD (getIntentSender, "getIntentSender", "()Landroid/content/IntentSender;") | |||
| DECLARE_JNI_CLASS (PendingIntent, "android/app/PendingIntent"); | |||
| #undef JNI_CLASS_MEMBERS | |||
| //============================================================================== | |||
| struct ServiceConnection : public AndroidInterfaceImplementer | |||
| { | |||
| virtual void onServiceConnected (jobject component, jobject iBinder) = 0; | |||
| virtual void onServiceDisconnected (jobject component) = 0; | |||
| jobject invoke (jobject proxy, jobject method, jobjectArray args) override | |||
| { | |||
| auto* env = getEnv(); | |||
| auto methodName = juceString ((jstring) env->CallObjectMethod (method, Method.getName)); | |||
| if (methodName == "onServiceConnected") | |||
| { | |||
| onServiceConnected (env->GetObjectArrayElement (args, 0), | |||
| env->GetObjectArrayElement (args, 1)); | |||
| return nullptr; | |||
| } | |||
| if (methodName == "onServiceDisconnected") | |||
| { | |||
| onServiceDisconnected (env->GetObjectArrayElement (args, 0)); | |||
| return nullptr; | |||
| } | |||
| return AndroidInterfaceImplementer::invoke (proxy, method, args); | |||
| } | |||
| }; | |||
| //============================================================================== | |||
| struct InAppPurchases::Pimpl : private AsyncUpdater, | |||
| private ServiceConnection | |||
| { | |||
| Pimpl (InAppPurchases& parent) : owner (parent) | |||
| { | |||
| getInAppPurchaseInstances().add (this); | |||
| auto* env = getEnv(); | |||
| auto intent = env->NewObject (Intent, Intent.constructWithString, | |||
| javaString ("com.android.vending.billing.InAppBillingService.BIND").get()); | |||
| env->CallObjectMethod (intent, Intent.setPackage, javaString ("com.android.vending").get()); | |||
| serviceConnection = GlobalRef (CreateJavaInterface (this, "android/content/ServiceConnection").get()); | |||
| android.activity.callBooleanMethod (JuceAppActivity.bindService, intent, | |||
| serviceConnection.get(), 1 /*BIND_AUTO_CREATE*/); | |||
| } | |||
| ~Pimpl() | |||
| { | |||
| if (serviceConnection != nullptr) | |||
| { | |||
| android.activity.callVoidMethod (JuceAppActivity.unbindService, serviceConnection.get()); | |||
| serviceConnection.clear(); | |||
| } | |||
| getInAppPurchaseInstances().removeFirstMatchingValue (this); | |||
| } | |||
| //============================================================================== | |||
| bool isInAppPurchasesSupported() { return isInAppPurchasesSupported (inAppBillingService); } | |||
| void getProductsInformation (const StringArray& productIdentifiers) | |||
| { | |||
| if (! checkIsReady()) | |||
| return; | |||
| auto callback = [this](const Array<InAppPurchases::Product>& products) | |||
| { | |||
| const ScopedLock lock (getProductsInformationJobResultsLock); | |||
| getProductsInformationJobResults.insert (0, products); | |||
| triggerAsyncUpdate(); | |||
| }; | |||
| threadPool->addJob (new GetProductsInformationJob (inAppBillingService, getPackageName(), | |||
| productIdentifiers, callback), true); | |||
| } | |||
| void purchaseProduct (const String& productIdentifier, bool isSubscription, | |||
| const StringArray& subscriptionIdentifiers, bool creditForUnusedSubscription) | |||
| { | |||
| if (! checkIsReady()) | |||
| return; | |||
| // Upgrading/downgrading only makes sense for subscriptions! | |||
| jassert (subscriptionIdentifiers.isEmpty() || isSubscription); | |||
| auto buyIntentBundle = getBuyIntentBundle (productIdentifier, isSubscription, | |||
| subscriptionIdentifiers, creditForUnusedSubscription); | |||
| auto* env = getEnv(); | |||
| auto responseCodeString = javaString ("RESPONSE_CODE"); | |||
| auto responseCode = env->CallIntMethod (buyIntentBundle.get(), Bundle.getInt, responseCodeString.get()); | |||
| if (responseCode == 0) | |||
| { | |||
| auto buyIntentString = javaString ("BUY_INTENT"); | |||
| auto pendingIntent = LocalRef<jobject> (env->CallObjectMethod (buyIntentBundle.get(), Bundle.getParcelable, buyIntentString.get())); | |||
| auto requestCode = 1001; | |||
| auto intentSender = LocalRef<jobject> (env->CallObjectMethod (pendingIntent.get(), PendingIntent.getIntentSender)); | |||
| auto fillInIntent = LocalRef<jobject> (env->NewObject (Intent, Intent.constructor)); | |||
| auto flagsMask = LocalRef<jobject> (env->CallStaticObjectMethod (Integer, Integer.valueOf, 0)); | |||
| auto flagsValues = LocalRef<jobject> (env->CallStaticObjectMethod (Integer, Integer.valueOf, 0)); | |||
| auto extraFlags = LocalRef<jobject> (env->CallStaticObjectMethod (Integer, Integer.valueOf, 0)); | |||
| android.activity.callVoidMethod (JuceAppActivity.startIntentSenderForResult, intentSender.get(), requestCode, | |||
| fillInIntent.get(), flagsMask.get(), flagsValues.get(), extraFlags.get()); | |||
| } | |||
| } | |||
| void restoreProductsBoughtList (bool, const juce::String&) | |||
| { | |||
| if (! checkIsReady()) | |||
| return; | |||
| auto callback = [this](const Array<InAppPurchases::Listener::PurchaseInfo>& purchases) | |||
| { | |||
| const ScopedLock lock (getProductsBoughtJobResultsLock); | |||
| getProductsBoughtJobResults.insert (0, purchases); | |||
| triggerAsyncUpdate(); | |||
| }; | |||
| threadPool->addJob (new GetProductsBoughtJob (inAppBillingService, | |||
| getPackageName(), callback), true); | |||
| } | |||
| void consumePurchase (const String& productIdentifier, const String& purchaseToken) | |||
| { | |||
| if (! checkIsReady()) | |||
| return; | |||
| auto callback = [this](const ConsumePurchaseJob::Result& r) | |||
| { | |||
| const ScopedLock lock (consumePurchaseJobResultsLock); | |||
| consumePurchaseJobResults.insert (0, r); | |||
| triggerAsyncUpdate(); | |||
| }; | |||
| threadPool->addJob (new ConsumePurchaseJob (inAppBillingService, getPackageName(), productIdentifier, | |||
| purchaseToken, callback), true); | |||
| } | |||
| //============================================================================== | |||
| void startDownloads (const Array<Download*>& downloads) | |||
| { | |||
| // Not available on this platform. | |||
| ignoreUnused (downloads); | |||
| jassertfalse; | |||
| } | |||
| void pauseDownloads (const Array<Download*>& downloads) | |||
| { | |||
| // Not available on this platform. | |||
| ignoreUnused (downloads); | |||
| jassertfalse; | |||
| } | |||
| void resumeDownloads (const Array<Download*>& downloads) | |||
| { | |||
| // Not available on this platform. | |||
| ignoreUnused (downloads); | |||
| jassertfalse; | |||
| } | |||
| void cancelDownloads (const Array<Download*>& downloads) | |||
| { | |||
| // Not available on this platform. | |||
| ignoreUnused (downloads); | |||
| jassertfalse; | |||
| } | |||
| //============================================================================== | |||
| LocalRef<jobject> getBuyIntentBundle (const String& productIdentifier, bool isSubscription, | |||
| const StringArray& subscriptionIdentifiers, bool creditForUnusedSubscription) | |||
| { | |||
| auto* env = getEnv(); | |||
| auto skuString = javaString (productIdentifier); | |||
| auto productTypeString = javaString (isSubscription ? "subs" : "inapp"); | |||
| auto devString = javaString (getDeveloperExtraData()); | |||
| if (subscriptionIdentifiers.isEmpty()) | |||
| return LocalRef<jobject> (inAppBillingService.callObjectMethod (IInAppBillingService.getBuyIntent, 3, | |||
| getPackageName().get(), skuString.get(), | |||
| productTypeString.get(), devString.get())); | |||
| auto skuList = LocalRef<jobject> (env->NewObject (ArrayList, ArrayList.constructor, | |||
| (int) subscriptionIdentifiers.size())); | |||
| if (skuList.get() == 0) | |||
| { | |||
| jassertfalse; | |||
| return LocalRef<jobject> (0); | |||
| } | |||
| for (const auto& identifier : subscriptionIdentifiers) | |||
| env->CallBooleanMethod (skuList.get(), ArrayList.add, javaString (identifier).get()); | |||
| auto extraParams = LocalRef<jobject> (env->NewObject (Bundle, Bundle.constructor)); | |||
| if (extraParams.get() == 0) | |||
| { | |||
| jassertfalse; | |||
| return LocalRef<jobject> (0); | |||
| } | |||
| auto skusToReplaceString = javaString ("skusToReplace"); | |||
| auto replaceSkusProrationString = javaString ("replaceSkusProration"); | |||
| env->CallVoidMethod (extraParams.get(), Bundle.putStringArrayList, skusToReplaceString.get(), skuList.get()); | |||
| env->CallVoidMethod (extraParams.get(), Bundle.putBoolean, replaceSkusProrationString.get(), creditForUnusedSubscription); | |||
| return LocalRef<jobject> (inAppBillingService.callObjectMethod (IInAppBillingService.getBuyIntentExtraParams, 6, | |||
| getPackageName().get(), skuString.get(), | |||
| productTypeString.get(), devString.get(), | |||
| extraParams.get())); | |||
| } | |||
| //============================================================================== | |||
| void notifyAboutPurchaseResult (const InAppPurchases::Purchase& purchase, bool success, const String& statusDescription) | |||
| { | |||
| owner.listeners.call (&Listener::productPurchaseFinished, { purchase, {} }, success, statusDescription); | |||
| } | |||
| //============================================================================== | |||
| bool checkIsReady() | |||
| { | |||
| return (inAppBillingService.get() != 0); | |||
| } | |||
| static bool isInAppPurchasesSupported (jobject iapService) | |||
| { | |||
| if (iapService != nullptr) | |||
| { | |||
| auto* env = getEnv(); | |||
| auto inAppString = javaString ("inapp"); | |||
| auto subsString = javaString ("subs"); | |||
| if (env->CallIntMethod (iapService, IInAppBillingService.isBillingSupported, 3, | |||
| getPackageName().get(), inAppString.get()) != 0) | |||
| return false; | |||
| if (env->CallIntMethod (iapService, IInAppBillingService.isBillingSupported, 3, | |||
| getPackageName().get(), subsString.get()) != 0) | |||
| return false; | |||
| return true; | |||
| } | |||
| // Connecting to the in-app purchase server failed! This could have multiple reasons: | |||
| // 1) Your phone/emulator must support the google play store | |||
| // 2) Your phone must be logged into the google play store and be able to receive updates | |||
| return false; | |||
| } | |||
| //============================================================================== | |||
| void onServiceConnected (jobject, jobject iBinder) override | |||
| { | |||
| auto* env = getEnv(); | |||
| LocalRef<jobject> iapService (env->CallStaticObjectMethod (IInAppBillingServiceStub, | |||
| IInAppBillingServiceStub.asInterface, | |||
| iBinder)); | |||
| if (isInAppPurchasesSupported (iapService)) | |||
| { | |||
| if (threadPool == nullptr) | |||
| threadPool = new ThreadPool (1); | |||
| inAppBillingService = GlobalRef (iapService); | |||
| } | |||
| // If you hit this assert, then in-app purchases is not available on your device, | |||
| // most likely due to too old version of Google Play API (hint: update Google Play on the device). | |||
| jassert (isInAppPurchasesSupported()); | |||
| } | |||
| void onServiceDisconnected (jobject) override | |||
| { | |||
| threadPool = nullptr; | |||
| inAppBillingService.clear(); | |||
| } | |||
| //============================================================================== | |||
| static LocalRef<jstring> getPackageName() | |||
| { | |||
| return LocalRef<jstring> ((jstring) (android.activity.callObjectMethod (JuceAppActivity.getPackageName))); | |||
| } | |||
| //============================================================================== | |||
| struct GetProductsInformationJob : public ThreadPoolJob | |||
| { | |||
| using Callback = std::function<void(const Array<InAppPurchases::Product>&)>; | |||
| GetProductsInformationJob (const GlobalRef& inAppBillingServiceToUse, | |||
| const LocalRef<jstring>& packageNameToUse, | |||
| const StringArray& productIdentifiersToUse, | |||
| const Callback& callbackToUse) | |||
| : ThreadPoolJob ("GetProductsInformationJob"), | |||
| inAppBillingService (inAppBillingServiceToUse), | |||
| packageName (packageNameToUse.get()), | |||
| productIdentifiers (productIdentifiersToUse), | |||
| callback (callbackToUse) | |||
| {} | |||
| ThreadPoolJob::JobStatus runJob() override | |||
| { | |||
| jassert (callback); | |||
| if (inAppBillingService.get() != 0) | |||
| { | |||
| // Google's Billing API limitation | |||
| auto maxQuerySize = 20; | |||
| auto pi = 0; | |||
| Array<InAppPurchases::Product> results; | |||
| StringArray identifiersToUse; | |||
| for (auto i = 0; i < productIdentifiers.size(); ++i) | |||
| { | |||
| identifiersToUse.add (productIdentifiers[i].toLowerCase()); | |||
| ++pi; | |||
| if (pi == maxQuerySize || i == productIdentifiers.size() - 1) | |||
| { | |||
| auto inAppProducts = processRetrievedProducts (queryProductsInformationFromService (identifiersToUse, "inapp")); | |||
| auto subsProducts = processRetrievedProducts (queryProductsInformationFromService (identifiersToUse, "subs")); | |||
| results.addArray (inAppProducts); | |||
| results.addArray (subsProducts); | |||
| identifiersToUse.clear(); | |||
| pi = 0; | |||
| } | |||
| } | |||
| if (callback) | |||
| callback (results); | |||
| } | |||
| else | |||
| { | |||
| if (callback) | |||
| callback ({}); | |||
| } | |||
| return jobHasFinished; | |||
| } | |||
| private: | |||
| LocalRef<jobject> queryProductsInformationFromService (const StringArray& productIdentifiersToQuery, const String& productType) | |||
| { | |||
| auto* env = getEnv(); | |||
| auto skuList = LocalRef<jobject> (env->NewObject (ArrayList, ArrayList.constructor, productIdentifiersToQuery.size())); | |||
| if (skuList.get() == 0) | |||
| return LocalRef<jobject> (0); | |||
| for (const auto& pi : productIdentifiersToQuery) | |||
| env->CallBooleanMethod (skuList.get(), ArrayList.add, javaString (pi).get()); | |||
| auto querySkus = LocalRef<jobject> (env->NewObject (Bundle, Bundle.constructor)); | |||
| if (querySkus.get() == 0) | |||
| return LocalRef<jobject> (0); | |||
| auto itemIdListString = javaString ("ITEM_ID_LIST"); | |||
| env->CallVoidMethod (querySkus.get(), Bundle.putStringArrayList, itemIdListString.get(), skuList.get()); | |||
| auto productTypeString = javaString (productType); | |||
| auto productDetails = LocalRef<jobject> (inAppBillingService.callObjectMethod (IInAppBillingService.getSkuDetails, | |||
| 3, (jstring) packageName.get(), | |||
| productTypeString.get(), querySkus.get())); | |||
| return productDetails; | |||
| } | |||
| Array<InAppPurchases::Product> processRetrievedProducts (LocalRef<jobject> retrievedProducts) | |||
| { | |||
| Array<InAppPurchases::Product> products; | |||
| if (retrievedProducts.get() != 0) | |||
| { | |||
| auto* env = getEnv(); | |||
| auto responseCodeString = javaString ("RESPONSE_CODE"); | |||
| auto responseCode = env->CallIntMethod (retrievedProducts.get(), Bundle.getInt, responseCodeString.get()); | |||
| if (responseCode == 0) | |||
| { | |||
| auto detailsListString = javaString ("DETAILS_LIST"); | |||
| auto responseList = LocalRef<jobject> (env->CallObjectMethod (retrievedProducts.get(), Bundle.getStringArrayList, | |||
| detailsListString.get())); | |||
| if (responseList != 0) | |||
| { | |||
| auto iterator = LocalRef<jobject> (env->CallObjectMethod (responseList.get(), ArrayList.iterator)); | |||
| if (iterator.get() != 0) | |||
| { | |||
| for (;;) | |||
| { | |||
| if (! env->CallBooleanMethod (iterator, Iterator.hasNext)) | |||
| break; | |||
| auto response = juce::LocalRef<jstring> ((jstring)env->CallObjectMethod (iterator, Iterator.next)); | |||
| if (response.get() != 0) | |||
| { | |||
| var responseData = JSON::parse (juceString (response.get())); | |||
| if (DynamicObject* object = responseData.getDynamicObject()) | |||
| { | |||
| NamedValueSet& props = object->getProperties(); | |||
| static Identifier productIdIdentifier ("productId"); | |||
| static Identifier titleIdentifier ("title"); | |||
| static Identifier descriptionIdentifier ("description"); | |||
| static Identifier priceIdentifier ("price"); | |||
| static Identifier priceCurrencyCodeIdentifier ("price_currency_code"); | |||
| var productId = props[productIdIdentifier]; | |||
| var title = props[titleIdentifier]; | |||
| var description = props[descriptionIdentifier]; | |||
| var price = props[priceIdentifier]; | |||
| var priceCurrencyCode = props[priceCurrencyCodeIdentifier]; | |||
| products.add ( { productId.toString(), | |||
| title.toString(), | |||
| description.toString(), | |||
| price.toString(), | |||
| priceCurrencyCode.toString() } ); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| return products; | |||
| } | |||
| GlobalRef inAppBillingService, packageName; | |||
| const StringArray productIdentifiers; | |||
| Callback callback; | |||
| }; | |||
| //============================================================================== | |||
| struct GetProductsBoughtJob : public ThreadPoolJob | |||
| { | |||
| using Callback = std::function<void(const Array<InAppPurchases::Listener::PurchaseInfo>&)>; | |||
| GetProductsBoughtJob (const GlobalRef& inAppBillingServiceToUse, | |||
| const LocalRef<jstring>& packageNameToUse, | |||
| const Callback& callbackToUse) | |||
| : ThreadPoolJob ("GetProductsBoughtJob"), | |||
| inAppBillingService (inAppBillingServiceToUse), | |||
| packageName (packageNameToUse.get()), | |||
| callback (callbackToUse) | |||
| {} | |||
| ThreadPoolJob::JobStatus runJob() override | |||
| { | |||
| jassert (callback); | |||
| if (inAppBillingService.get() != 0) | |||
| { | |||
| auto inAppPurchases = getProductsBought ("inapp", 0); | |||
| auto subsPurchases = getProductsBought ("subs", 0); | |||
| inAppPurchases.addArray (subsPurchases); | |||
| Array<InAppPurchases::Listener::PurchaseInfo> purchases; | |||
| for (const auto& purchase : inAppPurchases) | |||
| purchases.add ({ purchase, {} }); | |||
| if (callback) | |||
| callback (purchases); | |||
| } | |||
| else | |||
| { | |||
| if (callback) | |||
| callback ({}); | |||
| } | |||
| return jobHasFinished; | |||
| } | |||
| private: | |||
| Array<InAppPurchases::Purchase> getProductsBought (const String& productType, jstring continuationToken) | |||
| { | |||
| Array<InAppPurchases::Purchase> purchases; | |||
| auto* env = getEnv(); | |||
| auto productTypeString = javaString (productType); | |||
| auto ownedItems = LocalRef<jobject> (inAppBillingService.callObjectMethod (IInAppBillingService.getPurchases, 3, | |||
| (jstring) packageName.get(), productTypeString.get(), | |||
| continuationToken)); | |||
| if (ownedItems.get() != 0) | |||
| { | |||
| auto responseCodeString = javaString ("RESPONSE_CODE"); | |||
| auto responseCode = env->CallIntMethod (ownedItems.get(), Bundle.getInt, responseCodeString.get()); | |||
| if (responseCode == 0) | |||
| { | |||
| auto itemListString = javaString ("INAPP_PURCHASE_ITEM_LIST"); | |||
| auto dataListString = javaString ("INAPP_PURCHASE_DATA_LIST"); | |||
| auto signatureListString = javaString ("INAPP_DATA_SIGNATURE_LIST"); | |||
| auto continuationTokenString = javaString ("INAPP_CONTINUATION_TOKEN"); | |||
| auto ownedSkus = LocalRef<jobject> (env->CallObjectMethod (ownedItems.get(), Bundle.getStringArrayList, itemListString.get())); | |||
| auto purchaseDataList = LocalRef<jobject> (env->CallObjectMethod (ownedItems.get(), Bundle.getStringArrayList, dataListString.get())); | |||
| auto signatureList = LocalRef<jobject> (env->CallObjectMethod (ownedItems.get(), Bundle.getStringArrayList, signatureListString.get())); | |||
| auto newContinuationToken = LocalRef<jstring> ((jstring) env->CallObjectMethod (ownedItems.get(), Bundle.getString, continuationTokenString.get())); | |||
| for (auto i = 0; i < env->CallIntMethod (purchaseDataList.get(), ArrayList.size); ++i) | |||
| { | |||
| auto sku = juceString ((jstring) (env->CallObjectMethod (ownedSkus.get(), ArrayList.get, i))); | |||
| auto purchaseData = juceString ((jstring) (env->CallObjectMethod (purchaseDataList.get(), ArrayList.get, i))); | |||
| auto signature = juceString ((jstring) (env->CallObjectMethod (signatureList.get(), ArrayList.get, i))); | |||
| var responseData = JSON::parse (purchaseData); | |||
| if (auto* object = responseData.getDynamicObject()) | |||
| { | |||
| auto& props = object->getProperties(); | |||
| static const Identifier orderIdIdentifier ("orderId"), | |||
| packageNameIdentifier ("packageName"), | |||
| productIdIdentifier ("productId"), | |||
| purchaseTimeIdentifier ("purchaseTime"), | |||
| purchaseTokenIdentifier ("purchaseToken"); | |||
| var orderId = props[orderIdIdentifier]; | |||
| var appPackageName = props[packageNameIdentifier]; | |||
| var productId = props[productIdIdentifier]; | |||
| var purchaseTime = props[purchaseTimeIdentifier]; | |||
| var purchaseToken = props[purchaseTokenIdentifier]; | |||
| String purchaseTimeString = Time (purchaseTime.toString().getLargeIntValue()).toString (true, true, true, true); | |||
| purchases.add ({ orderId.toString(), productId.toString(), appPackageName.toString(), purchaseTimeString, purchaseToken.toString() }); | |||
| } | |||
| } | |||
| if (newContinuationToken.get() != 0) | |||
| getProductsBought (productType, newContinuationToken.get()); | |||
| } | |||
| } | |||
| return purchases; | |||
| } | |||
| GlobalRef inAppBillingService, packageName; | |||
| Callback callback; | |||
| }; | |||
| //============================================================================== | |||
| class ConsumePurchaseJob : public ThreadPoolJob | |||
| { | |||
| public: | |||
| struct Result | |||
| { | |||
| String productIdentifier; | |||
| bool success; | |||
| String statusDescription; | |||
| }; | |||
| using Callback = std::function<void(const Result&)>; | |||
| ConsumePurchaseJob (const GlobalRef& inAppBillingServiceToUse, | |||
| const LocalRef<jstring>& packageNameToUse, | |||
| const String& productIdentifierToUse, | |||
| const String& purchaseTokenToUse, | |||
| const Callback& callbackToUse) | |||
| : ThreadPoolJob ("ConsumePurchaseJob"), | |||
| inAppBillingService (inAppBillingServiceToUse), | |||
| packageName (packageNameToUse.get()), | |||
| productIdentifier (productIdentifierToUse), | |||
| purchaseToken (purchaseTokenToUse), | |||
| callback (callbackToUse) | |||
| {} | |||
| ThreadPoolJob::JobStatus runJob() override | |||
| { | |||
| jassert (callback); | |||
| auto token = (! purchaseToken.isEmpty() ? purchaseToken : getPurchaseTokenForProductId (productIdentifier, false, 0)); | |||
| if (token.isEmpty()) | |||
| { | |||
| if (callback) | |||
| callback ({ productIdentifier, false, NEEDS_TRANS ("Item not owned") }); | |||
| return jobHasFinished; | |||
| } | |||
| auto responseCode = inAppBillingService.callIntMethod (IInAppBillingService.consumePurchase, 3, | |||
| (jstring)packageName.get(), javaString (token).get()); | |||
| if (callback) | |||
| callback ({ productIdentifier, responseCode == 0, statusCodeToUserString (responseCode) }); | |||
| return jobHasFinished; | |||
| } | |||
| private: | |||
| String getPurchaseTokenForProductId (const String productIdToLookFor, bool isSubscription, jstring continuationToken) | |||
| { | |||
| auto productTypeString = javaString (isSubscription ? "subs" : "inapp"); | |||
| auto ownedItems = LocalRef<jobject> (inAppBillingService.callObjectMethod (IInAppBillingService.getPurchases, 3, | |||
| (jstring) packageName.get(), productTypeString.get(), | |||
| continuationToken)); | |||
| if (ownedItems.get() != 0) | |||
| { | |||
| auto* env = getEnv(); | |||
| auto responseCodeString = javaString ("RESPONSE_CODE"); | |||
| auto responseCode = env->CallIntMethod (ownedItems.get(), Bundle.getInt, responseCodeString.get()); | |||
| if (responseCode == 0) | |||
| { | |||
| auto dataListString = javaString ("INAPP_PURCHASE_DATA_LIST"); | |||
| auto continuationTokenString = javaString ("INAPP_CONTINUATION_TOKEN"); | |||
| auto purchaseDataList = LocalRef<jobject> (env->CallObjectMethod (ownedItems.get(), Bundle.getStringArrayList, dataListString.get())); | |||
| auto newContinuationToken = LocalRef<jstring> ((jstring) env->CallObjectMethod (ownedItems.get(), Bundle.getString, continuationTokenString.get())); | |||
| for (auto i = 0; i < env->CallIntMethod (purchaseDataList.get(), ArrayList.size); ++i) | |||
| { | |||
| auto purchaseData = juceString ((jstring) (env->CallObjectMethod (purchaseDataList.get(), ArrayList.get, i))); | |||
| var responseData = JSON::parse (purchaseData); | |||
| if (auto* object = responseData.getDynamicObject()) | |||
| { | |||
| static const Identifier productIdIdentifier ("productId"), | |||
| purchaseTokenIdentifier ("purchaseToken"); | |||
| auto& props = object->getProperties(); | |||
| var productId = props[productIdIdentifier]; | |||
| if (productId.toString() == productIdToLookFor) | |||
| return props[purchaseTokenIdentifier].toString(); | |||
| } | |||
| } | |||
| if (newContinuationToken.get() != 0) | |||
| return getPurchaseTokenForProductId (productIdToLookFor, isSubscription, newContinuationToken.get()); | |||
| } | |||
| } | |||
| return {}; | |||
| } | |||
| GlobalRef inAppBillingService, packageName; | |||
| const String productIdentifier, purchaseToken; | |||
| Callback callback; | |||
| }; | |||
| //============================================================================== | |||
| void handleAsyncUpdate() override | |||
| { | |||
| { | |||
| const ScopedLock lock (getProductsInformationJobResultsLock); | |||
| for (int i = getProductsInformationJobResults.size(); --i >= 0;) | |||
| { | |||
| const auto& result = getProductsInformationJobResults.getReference (i); | |||
| owner.listeners.call (&Listener::productsInfoReturned, result); | |||
| getProductsInformationJobResults.remove (i); | |||
| } | |||
| } | |||
| { | |||
| const ScopedLock lock (getProductsBoughtJobResultsLock); | |||
| for (int i = getProductsBoughtJobResults.size(); --i >= 0;) | |||
| { | |||
| const auto& result = getProductsBoughtJobResults.getReference (i); | |||
| owner.listeners.call (&Listener::purchasesListRestored, result, true, NEEDS_TRANS ("Success")); | |||
| getProductsBoughtJobResults.remove (i); | |||
| } | |||
| } | |||
| { | |||
| const ScopedLock lock (consumePurchaseJobResultsLock); | |||
| for (int i = consumePurchaseJobResults.size(); --i >= 0;) | |||
| { | |||
| const auto& result = consumePurchaseJobResults.getReference (i); | |||
| owner.listeners.call (&Listener::productConsumed, result.productIdentifier, | |||
| result.success, result.statusDescription); | |||
| consumePurchaseJobResults.remove (i); | |||
| } | |||
| } | |||
| } | |||
| //============================================================================== | |||
| static Array<Pimpl*>& getInAppPurchaseInstances() noexcept | |||
| { | |||
| static Array<Pimpl*> instances; | |||
| return instances; | |||
| } | |||
| static void inAppPurchaseCompleted (jobject intentData) | |||
| { | |||
| auto* env = getEnv(); | |||
| auto inAppPurchaseDataString = javaString ("INAPP_PURCHASE_DATA"); | |||
| auto inAppDataSignatureString = javaString ("INAPP_DATA_SIGNATURE"); | |||
| auto responseCodeString = javaString ("RESPONSE_CODE"); | |||
| auto pd = LocalRef<jstring> ((jstring) env->CallObjectMethod (intentData, Intent.getStringExtra, inAppPurchaseDataString.get())); | |||
| auto sig = LocalRef<jstring> ((jstring) env->CallObjectMethod (intentData, Intent.getStringExtra, inAppDataSignatureString.get())); | |||
| auto purchaseDataString = pd.get() != 0 ? juceString (pd.get()) : String(); | |||
| auto dataSignatureString = sig.get() != 0 ? juceString (sig.get()) : String(); | |||
| var responseData = JSON::parse (purchaseDataString); | |||
| auto responseCode = env->CallIntMethod (intentData, Intent.getIntExtra, responseCodeString.get()); | |||
| auto statusCodeUserString = statusCodeToUserString (responseCode); | |||
| if (auto* object = responseData.getDynamicObject()) | |||
| { | |||
| auto& props = object->getProperties(); | |||
| static const Identifier orderIdIdentifier ("orderId"), | |||
| packageNameIdentifier ("packageName"), | |||
| productIdIdentifier ("productId"), | |||
| purchaseTimeIdentifier ("purchaseTime"), | |||
| purchaseTokenIdentifier ("purchaseToken"), | |||
| developerPayloadIdentifier ("developerPayload"); | |||
| var orderId = props[orderIdIdentifier]; | |||
| var packageName = props[packageNameIdentifier]; | |||
| var productId = props[productIdIdentifier]; | |||
| var purchaseTime = props[purchaseTimeIdentifier]; | |||
| var purchaseToken = props[purchaseTokenIdentifier]; | |||
| var developerPayload = props[developerPayloadIdentifier]; | |||
| if (auto* target = getPimplFromDeveloperExtraData (developerPayload)) | |||
| { | |||
| auto purchaseTimeString = Time (purchaseTime.toString().getLargeIntValue()) | |||
| .toString (true, true, true, true); | |||
| target->notifyAboutPurchaseResult ({ orderId.toString(), productId.toString(), packageName.toString(), | |||
| purchaseTimeString, purchaseToken.toString() }, | |||
| true, statusCodeUserString); | |||
| } | |||
| } | |||
| } | |||
| //============================================================================== | |||
| String getDeveloperExtraData() | |||
| { | |||
| static const Identifier inAppPurchaseInstance ("inAppPurchaseInstance"); | |||
| DynamicObject::Ptr developerString (new DynamicObject()); | |||
| developerString->setProperty (inAppPurchaseInstance, | |||
| "0x" + String::toHexString (reinterpret_cast<pointer_sized_int> (this))); | |||
| return JSON::toString (var (developerString)); | |||
| } | |||
| static Pimpl* getPimplFromDeveloperExtraData (const String& developerExtra) | |||
| { | |||
| static const Identifier inAppPurchaseInstance ("inAppPurchaseInstance"); | |||
| if (DynamicObject::Ptr developerData = JSON::fromString (developerExtra).getDynamicObject()) | |||
| { | |||
| String hexAddr = developerData->getProperty (inAppPurchaseInstance); | |||
| if (hexAddr.startsWith ("0x")) | |||
| hexAddr = hexAddr.fromFirstOccurrenceOf ("0x", false, false); | |||
| auto* target = reinterpret_cast<Pimpl*> (static_cast<pointer_sized_int> (hexAddr.getHexValue64())); | |||
| if (getInAppPurchaseInstances().contains (target)) | |||
| return target; | |||
| } | |||
| return nullptr; | |||
| } | |||
| //============================================================================== | |||
| static String statusCodeToUserString (int statusCode) | |||
| { | |||
| switch (statusCode) | |||
| { | |||
| case 0: return NEEDS_TRANS ("Success"); | |||
| case 1: return NEEDS_TRANS ("Cancelled by user"); | |||
| case 2: return NEEDS_TRANS ("Service unavailable"); | |||
| case 3: return NEEDS_TRANS ("Billing unavailable"); | |||
| case 4: return NEEDS_TRANS ("Item unavailable"); | |||
| case 5: return NEEDS_TRANS ("Internal error"); | |||
| case 6: return NEEDS_TRANS ("Generic error"); | |||
| case 7: return NEEDS_TRANS ("Item already owned"); | |||
| case 8: return NEEDS_TRANS ("Item not owned"); | |||
| default: jassertfalse; return NEEDS_TRANS ("Unknown status"); | |||
| } | |||
| } | |||
| //============================================================================== | |||
| InAppPurchases& owner; | |||
| GlobalRef inAppBillingService, serviceConnection; | |||
| ScopedPointer<ThreadPool> threadPool; | |||
| CriticalSection getProductsInformationJobResultsLock, | |||
| getProductsBoughtJobResultsLock, | |||
| consumePurchaseJobResultsLock; | |||
| Array<Array<InAppPurchases::Product>> getProductsInformationJobResults; | |||
| Array<Array<InAppPurchases::Listener::PurchaseInfo>> getProductsBoughtJobResults; | |||
| Array<ConsumePurchaseJob::Result> consumePurchaseJobResults; | |||
| }; | |||
| //============================================================================== | |||
| void juce_inAppPurchaseCompleted (jobject intentData) | |||
| { | |||
| InAppPurchases::Pimpl::inAppPurchaseCompleted (intentData); | |||
| } | |||
| @@ -0,0 +1,679 @@ | |||
| /* | |||
| ============================================================================== | |||
| This file is part of the JUCE library. | |||
| Copyright (c) 2017 - ROLI Ltd. | |||
| JUCE is an open source library subject to commercial or open-source | |||
| licensing. | |||
| By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
| Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
| 27th April 2017). | |||
| End User License Agreement: www.juce.com/juce-5-licence | |||
| Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
| Or: You may also use this code under the terms of the GPL v3 (see | |||
| www.gnu.org/licenses). | |||
| JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
| EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
| DISCLAIMED. | |||
| ============================================================================== | |||
| */ | |||
| template <> struct ContainerDeletePolicy<SKProductsRequest> { static void destroy (NSObject* o) { [o release]; } }; | |||
| template <> struct ContainerDeletePolicy<SKReceiptRefreshRequest> { static void destroy (NSObject* o) { [o release]; } }; | |||
| template <> struct ContainerDeletePolicy<NSObject<SKProductsRequestDelegate,SKPaymentTransactionObserver>> { static void destroy (NSObject* o) { [o release]; } }; | |||
| //============================================================================== | |||
| struct SKDelegateAndPaymentObserver | |||
| { | |||
| SKDelegateAndPaymentObserver() : delegate ([getClass().createInstance() init]) | |||
| { | |||
| Class::setThis (delegate, this); | |||
| } | |||
| virtual ~SKDelegateAndPaymentObserver() {} | |||
| virtual void didReceiveResponse (SKProductsRequest*, SKProductsResponse*) = 0; | |||
| virtual void requestDidFinish (SKRequest*) = 0; | |||
| virtual void updatedTransactions (SKPaymentQueue*, NSArray<SKPaymentTransaction*>*) = 0; | |||
| virtual void restoreCompletedTransactionsFailedWithError (SKPaymentQueue*, NSError*) = 0; | |||
| virtual void restoreCompletedTransactionsFinished (SKPaymentQueue*) = 0; | |||
| virtual void updatedDownloads (SKPaymentQueue*, NSArray<SKDownload*>*) = 0; | |||
| protected: | |||
| ScopedPointer<NSObject<SKProductsRequestDelegate, SKPaymentTransactionObserver>> delegate; | |||
| private: | |||
| struct Class : public ObjCClass<NSObject<SKProductsRequestDelegate, SKPaymentTransactionObserver>> | |||
| { | |||
| //============================================================================== | |||
| Class() : ObjCClass<NSObject<SKProductsRequestDelegate, SKPaymentTransactionObserver>> ("SKDelegateAndPaymentObserverBase_") | |||
| { | |||
| addIvar<SKDelegateAndPaymentObserver*> ("self"); | |||
| addMethod (@selector (productsRequest:didReceiveResponse:), didReceiveResponse, "v@:@@"); | |||
| addMethod (@selector (requestDidFinish:), requestDidFinish, "v@:@"); | |||
| addMethod (@selector (paymentQueue:updatedTransactions:), updatedTransactions, "v@:@@"); | |||
| addMethod (@selector (paymentQueue:restoreCompletedTransactionsFailedWithError:), restoreCompletedTransactionsFailedWithError, "v@:@@"); | |||
| addMethod (@selector (paymentQueueRestoreCompletedTransactionsFinished:), restoreCompletedTransactionsFinished, "v@:@"); | |||
| addMethod (@selector (paymentQueue:updatedDownloads:), updatedDownloads, "v@:@@"); | |||
| registerClass(); | |||
| } | |||
| //============================================================================== | |||
| static SKDelegateAndPaymentObserver& getThis (id self) { return *getIvar<SKDelegateAndPaymentObserver*> (self, "self"); } | |||
| static void setThis (id self, SKDelegateAndPaymentObserver* s) { object_setInstanceVariable (self, "self", s); } | |||
| //============================================================================== | |||
| static void didReceiveResponse (id self, SEL, SKProductsRequest* request, SKProductsResponse* response) { getThis (self).didReceiveResponse (request, response); } | |||
| static void requestDidFinish (id self, SEL, SKRequest* request) { getThis (self).requestDidFinish (request); } | |||
| static void updatedTransactions (id self, SEL, SKPaymentQueue* queue, NSArray<SKPaymentTransaction*>* trans) { getThis (self).updatedTransactions (queue, trans); } | |||
| static void restoreCompletedTransactionsFailedWithError (id self, SEL, SKPaymentQueue* q, NSError* err) { getThis (self).restoreCompletedTransactionsFailedWithError (q, err); } | |||
| static void restoreCompletedTransactionsFinished (id self, SEL, SKPaymentQueue* queue) { getThis (self).restoreCompletedTransactionsFinished (queue); } | |||
| static void updatedDownloads (id self, SEL, SKPaymentQueue* queue, NSArray<SKDownload*>* downloads) { getThis (self).updatedDownloads (queue, downloads); } | |||
| }; | |||
| //============================================================================== | |||
| static Class& getClass() | |||
| { | |||
| static Class c; | |||
| return c; | |||
| } | |||
| }; | |||
| //============================================================================== | |||
| struct InAppPurchases::Pimpl : public SKDelegateAndPaymentObserver | |||
| { | |||
| /** AppStore implementation of hosted content download. */ | |||
| struct DownloadImpl : public Download | |||
| { | |||
| DownloadImpl (SKDownload* downloadToUse) : download (downloadToUse) {} | |||
| String getProductId() const override { return nsStringToJuce (download.contentIdentifier); } | |||
| int64 getContentLength() const override { return download.contentLength; } | |||
| String getContentVersion() const override { return nsStringToJuce (download.contentVersion); } | |||
| Status getStatus() const override { return SKDownloadStateToDownloadStatus (download.downloadState); } | |||
| SKDownload* download; | |||
| }; | |||
| /** Represents a pending request initialised with [SKProductRequest start]. */ | |||
| struct PendingProductInfoRequest | |||
| { | |||
| enum class Type | |||
| { | |||
| query = 0, | |||
| purchase | |||
| }; | |||
| Type type; | |||
| ScopedPointer<SKProductsRequest> request; | |||
| }; | |||
| /** Represents a pending request started from [SKReceiptRefreshRequest start]. */ | |||
| struct PendingReceiptRefreshRequest | |||
| { | |||
| String subscriptionsSharedSecret; | |||
| ScopedPointer<SKReceiptRefreshRequest> request; | |||
| }; | |||
| /** Represents a transaction with pending downloads. Only after all downloads | |||
| are finished, the transaction is marked as finished. */ | |||
| struct PendingDownloadsTransaction | |||
| { | |||
| PendingDownloadsTransaction (SKPaymentTransaction* t) : transaction (t) | |||
| { | |||
| addDownloadsFromSKTransaction (transaction); | |||
| } | |||
| void addDownloadsFromSKTransaction (SKPaymentTransaction* transactionToUse) | |||
| { | |||
| for (SKDownload* download in transactionToUse.downloads) | |||
| downloads.add (new DownloadImpl (download)); | |||
| } | |||
| bool canBeMarkedAsFinished() const | |||
| { | |||
| for (SKDownload* d in transaction.downloads) | |||
| { | |||
| if (d.downloadState != SKDownloadStateFinished | |||
| && d.downloadState != SKDownloadStateFailed | |||
| && d.downloadState != SKDownloadStateCancelled) | |||
| { | |||
| return false; | |||
| } | |||
| } | |||
| return true; | |||
| } | |||
| OwnedArray<DownloadImpl> downloads; | |||
| SKPaymentTransaction* const transaction; | |||
| }; | |||
| //============================================================================== | |||
| Pimpl (InAppPurchases& p) : owner (p) { [[SKPaymentQueue defaultQueue] addTransactionObserver: delegate]; } | |||
| ~Pimpl() noexcept { [[SKPaymentQueue defaultQueue] removeTransactionObserver: delegate]; } | |||
| //============================================================================== | |||
| bool isInAppPurchasesSupported() const { return true; } | |||
| void getProductsInformation (const StringArray& productIdentifiers) | |||
| { | |||
| auto* productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers: [NSSet setWithArray: createNSArrayFromStringArray (productIdentifiers)]]; | |||
| pendingProductInfoRequests.add (new PendingProductInfoRequest {PendingProductInfoRequest::Type::query, productsRequest}); | |||
| productsRequest.delegate = delegate; | |||
| [productsRequest start]; | |||
| } | |||
| void purchaseProduct (const String& productIdentifier, bool, const StringArray&, bool) | |||
| { | |||
| if (! [SKPaymentQueue canMakePayments]) | |||
| { | |||
| owner.listeners.call (&Listener::productPurchaseFinished, {}, false, NEEDS_TRANS ("Payments not allowed")); | |||
| return; | |||
| } | |||
| auto* productIdentifiers = [NSArray arrayWithObject: juceStringToNS (productIdentifier)]; | |||
| auto* productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithArray:productIdentifiers]]; | |||
| pendingProductInfoRequests.add (new PendingProductInfoRequest {PendingProductInfoRequest::Type::purchase, productsRequest}); | |||
| productsRequest.delegate = delegate; | |||
| [productsRequest start]; | |||
| } | |||
| void restoreProductsBoughtList (bool includeDownloadInfo, const String& subscriptionsSharedSecret) | |||
| { | |||
| if (includeDownloadInfo) | |||
| { | |||
| [[SKPaymentQueue defaultQueue] restoreCompletedTransactions]; | |||
| } | |||
| else | |||
| { | |||
| auto* receiptRequest = [[SKReceiptRefreshRequest alloc] init]; | |||
| pendingReceiptRefreshRequests.add (new PendingReceiptRefreshRequest {subscriptionsSharedSecret, | |||
| [receiptRequest retain]}); | |||
| receiptRequest.delegate = delegate; | |||
| [receiptRequest start]; | |||
| } | |||
| } | |||
| void consumePurchase (const String&, const String&) {} | |||
| //============================================================================== | |||
| void startDownloads (const Array<Download*>& downloads) | |||
| { | |||
| [[SKPaymentQueue defaultQueue] startDownloads: downloadsToSKDownloads (removeInvalidDownloads (downloads))]; | |||
| } | |||
| void pauseDownloads (const Array<Download*>& downloads) | |||
| { | |||
| [[SKPaymentQueue defaultQueue] pauseDownloads: downloadsToSKDownloads (removeInvalidDownloads (downloads))]; | |||
| } | |||
| void resumeDownloads (const Array<Download*>& downloads) | |||
| { | |||
| [[SKPaymentQueue defaultQueue] resumeDownloads: downloadsToSKDownloads (removeInvalidDownloads (downloads))]; | |||
| } | |||
| void cancelDownloads (const Array<Download*>& downloads) | |||
| { | |||
| [[SKPaymentQueue defaultQueue] cancelDownloads: downloadsToSKDownloads (removeInvalidDownloads (downloads))]; | |||
| } | |||
| //============================================================================== | |||
| void didReceiveResponse (SKProductsRequest* request, SKProductsResponse* response) override | |||
| { | |||
| for (auto i = 0; i < pendingProductInfoRequests.size(); ++i) | |||
| { | |||
| auto& pendingRequest = *pendingProductInfoRequests[i]; | |||
| if (pendingRequest.request == request) | |||
| { | |||
| if (pendingRequest.type == PendingProductInfoRequest::Type::query) notifyProductsInfoReceived (response.products); | |||
| else if (pendingRequest.type == PendingProductInfoRequest::Type::purchase) startPurchase (response.products); | |||
| else break; | |||
| pendingProductInfoRequests.remove (i); | |||
| return; | |||
| } | |||
| } | |||
| // Unknown request received! | |||
| jassertfalse; | |||
| } | |||
| void requestDidFinish (SKRequest* request) override | |||
| { | |||
| if (auto receiptRefreshRequest = getAs<SKReceiptRefreshRequest> (request)) | |||
| { | |||
| for (auto i = 0; i < pendingReceiptRefreshRequests.size(); ++i) | |||
| { | |||
| auto& pendingRequest = *pendingReceiptRefreshRequests[i]; | |||
| if (pendingRequest.request == receiptRefreshRequest) | |||
| { | |||
| processReceiptRefreshResponseWithSubscriptionsSharedSecret (pendingRequest.subscriptionsSharedSecret); | |||
| pendingReceiptRefreshRequests.remove (i); | |||
| return; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| void updatedTransactions (SKPaymentQueue*, NSArray<SKPaymentTransaction*>* transactions) override | |||
| { | |||
| for (SKPaymentTransaction* transaction in transactions) | |||
| { | |||
| switch (transaction.transactionState) | |||
| { | |||
| case SKPaymentTransactionStatePurchasing: break; | |||
| case SKPaymentTransactionStateDeferred: break; | |||
| case SKPaymentTransactionStateFailed: processTransactionFinish (transaction, false); break; | |||
| case SKPaymentTransactionStatePurchased: processTransactionFinish (transaction, true); break; | |||
| case SKPaymentTransactionStateRestored: processTransactionFinish (transaction, true); break; | |||
| default: jassertfalse; break; // Unexpected transaction state | |||
| } | |||
| } | |||
| } | |||
| void restoreCompletedTransactionsFailedWithError (SKPaymentQueue*, NSError* error) override | |||
| { | |||
| owner.listeners.call (&Listener::purchasesListRestored, {}, false, nsStringToJuce (error.localizedDescription)); | |||
| } | |||
| void restoreCompletedTransactionsFinished (SKPaymentQueue*) override | |||
| { | |||
| owner.listeners.call (&Listener::purchasesListRestored, restoredPurchases, true, NEEDS_TRANS ("Success")); | |||
| restoredPurchases.clear(); | |||
| } | |||
| void updatedDownloads (SKPaymentQueue*, NSArray<SKDownload*>* downloads) override | |||
| { | |||
| for (SKDownload* download in downloads) | |||
| { | |||
| if (auto* pendingDownload = getPendingDownloadFor (download)) | |||
| { | |||
| switch (download.downloadState) | |||
| { | |||
| case SKDownloadStateWaiting: break; | |||
| case SKDownloadStatePaused: owner.listeners.call (&Listener::productDownloadPaused, *pendingDownload); break; | |||
| case SKDownloadStateActive: owner.listeners.call (&Listener::productDownloadProgressUpdate, *pendingDownload, | |||
| download.progress, RelativeTime (download.timeRemaining)); break; | |||
| case SKDownloadStateFinished: | |||
| case SKDownloadStateFailed: | |||
| case SKDownloadStateCancelled: processDownloadFinish (pendingDownload, download); break; | |||
| default: jassertfalse; break; // Unexpected download state | |||
| } | |||
| } | |||
| } | |||
| } | |||
| //============================================================================== | |||
| void notifyProductsInfoReceived (NSArray<SKProduct*>* products) | |||
| { | |||
| Array<Product> productsToReturn; | |||
| for (SKProduct* skProduct in products) | |||
| productsToReturn.add (SKProductToIAPProduct (skProduct)); | |||
| owner.listeners.call (&Listener::productsInfoReturned, productsToReturn); | |||
| } | |||
| void startPurchase (NSArray<SKProduct*>* products) | |||
| { | |||
| if ([products count] > 0) | |||
| { | |||
| // Only one product can be bought at once! | |||
| jassert ([products count] == 1); | |||
| auto* product = products[0]; | |||
| auto* payment = [SKPayment paymentWithProduct: product]; | |||
| [[SKPaymentQueue defaultQueue] addPayment: payment]; | |||
| } | |||
| else | |||
| { | |||
| owner.listeners.call (&Listener::productPurchaseFinished, {}, false, | |||
| NEEDS_TRANS ("Your app is not setup for payments")); | |||
| } | |||
| } | |||
| //============================================================================== | |||
| Array<Download*> removeInvalidDownloads (const Array<Download*>& downloadsToUse) | |||
| { | |||
| Array<Download*> downloads (downloadsToUse); | |||
| for (int i = downloads.size(); --i >= 0;) | |||
| { | |||
| auto hasPendingDownload = hasDownloadInPendingDownloadsTransaction (*downloads[i]); | |||
| // Invalid download passed, it does not exist in pending downloads list | |||
| jassert (hasPendingDownload); | |||
| if (! hasPendingDownload) | |||
| downloads.remove (i); | |||
| } | |||
| return downloads; | |||
| } | |||
| bool hasDownloadInPendingDownloadsTransaction (const Download& download) | |||
| { | |||
| for (auto* pdt : pendingDownloadsTransactions) | |||
| for (auto* pendingDownload : pdt->downloads) | |||
| if (pendingDownload == &download) | |||
| return true; | |||
| return false; | |||
| } | |||
| //============================================================================== | |||
| void processTransactionFinish (SKPaymentTransaction* transaction, bool success) | |||
| { | |||
| auto orderId = nsStringToJuce (transaction.transactionIdentifier); | |||
| auto packageName = nsStringToJuce ([[NSBundle mainBundle] bundleIdentifier]); | |||
| auto productId = nsStringToJuce (transaction.payment.productIdentifier); | |||
| auto purchaseTime = Time (1000 * (int64) transaction.transactionDate.timeIntervalSince1970) | |||
| .toString (true, true, true, true); | |||
| Purchase purchase { orderId, productId, packageName, purchaseTime, {} }; | |||
| Array<Download*> downloads; | |||
| // If transaction failed or there are no downloads, finish the transaction immediately, otherwise | |||
| // finish the transaction only after all downloads are finished. | |||
| if (transaction.transactionState == SKPaymentTransactionStateFailed | |||
| || transaction.downloads == nil | |||
| || [transaction.downloads count] == 0) | |||
| { | |||
| [[SKPaymentQueue defaultQueue] finishTransaction: transaction]; | |||
| } | |||
| else | |||
| { | |||
| // On application startup or when the app is resumed we may receive multiple | |||
| // "purchased" callbacks with the same underlying transaction. Sadly, only | |||
| // the last set of downloads will be valid. | |||
| auto* pdt = getPendingDownloadsTransactionForSKTransaction (transaction); | |||
| if (pdt == nullptr) | |||
| { | |||
| pdt = pendingDownloadsTransactions.add (new PendingDownloadsTransaction (transaction)); | |||
| } | |||
| else | |||
| { | |||
| pdt->downloads.clear(); | |||
| pdt->addDownloadsFromSKTransaction (transaction); | |||
| } | |||
| for (auto* download : pdt->downloads) | |||
| downloads.add (download); | |||
| } | |||
| if (transaction.transactionState == SKPaymentTransactionStateRestored) | |||
| restoredPurchases.add ({ purchase, downloads }); | |||
| else | |||
| owner.listeners.call (&Listener::productPurchaseFinished, { purchase, downloads }, success, | |||
| SKPaymentTransactionStateToString (transaction.transactionState)); | |||
| } | |||
| PendingDownloadsTransaction* getPendingDownloadsTransactionForSKTransaction (SKPaymentTransaction* transaction) | |||
| { | |||
| for (auto* pdt : pendingDownloadsTransactions) | |||
| if (pdt->transaction == transaction) | |||
| return pdt; | |||
| return nullptr; | |||
| } | |||
| //============================================================================== | |||
| PendingDownloadsTransaction* getPendingDownloadsTransactionSKDownloadFor (SKDownload* download) | |||
| { | |||
| for (auto* pdt : pendingDownloadsTransactions) | |||
| for (auto* pendingDownload : pdt->downloads) | |||
| if (pendingDownload->download == download) | |||
| return pdt; | |||
| jassertfalse; | |||
| return nullptr; | |||
| } | |||
| Download* getPendingDownloadFor (SKDownload* download) | |||
| { | |||
| if (auto* pdt = getPendingDownloadsTransactionSKDownloadFor (download)) | |||
| for (auto* pendingDownload : pdt->downloads) | |||
| if (pendingDownload->download == download) | |||
| return pendingDownload; | |||
| jassertfalse; | |||
| return nullptr; | |||
| } | |||
| void processDownloadFinish (Download* pendingDownload, SKDownload* download) | |||
| { | |||
| if (auto* pdt = getPendingDownloadsTransactionSKDownloadFor (download)) | |||
| { | |||
| auto contentURL = download.downloadState == SKDownloadStateFinished | |||
| ? URL (nsStringToJuce (download.contentURL.absoluteString)) | |||
| : URL(); | |||
| owner.listeners.call (&Listener::productDownloadFinished, *pendingDownload, contentURL); | |||
| if (pdt->canBeMarkedAsFinished()) | |||
| { | |||
| // All downloads finished, mark transaction as finished too. | |||
| [[SKPaymentQueue defaultQueue] finishTransaction: pdt->transaction]; | |||
| pendingDownloadsTransactions.removeObject (pdt); | |||
| } | |||
| } | |||
| } | |||
| //============================================================================== | |||
| void processReceiptRefreshResponseWithSubscriptionsSharedSecret (const String& secret) | |||
| { | |||
| auto* receiptURL = [[NSBundle mainBundle] appStoreReceiptURL]; | |||
| if (auto* receiptData = [NSData dataWithContentsOfURL: receiptURL]) | |||
| fetchReceiptDetailsFromAppStore (receiptData, secret); | |||
| else | |||
| owner.listeners.call (&Listener::purchasesListRestored, {}, false, NEEDS_TRANS ("Receipt fetch failed")); | |||
| } | |||
| void fetchReceiptDetailsFromAppStore (NSData* receiptData, const String& secret) | |||
| { | |||
| auto* requestContents = [NSMutableDictionary dictionaryWithCapacity: (NSUInteger) (secret.isNotEmpty() ? 2 : 1)]; | |||
| [requestContents setObject: [receiptData base64EncodedStringWithOptions:0] forKey: nsStringLiteral ("receipt-data")]; | |||
| if (secret.isNotEmpty()) | |||
| [requestContents setObject: juceStringToNS (secret) forKey: nsStringLiteral ("password")]; | |||
| NSError* error; | |||
| auto* requestData = [NSJSONSerialization dataWithJSONObject: requestContents | |||
| options: 0 | |||
| error: &error]; | |||
| if (requestData == nil) | |||
| { | |||
| owner.listeners.call (&Listener::purchasesListRestored, {}, false, NEEDS_TRANS ("Receipt fetch failed")); | |||
| return; | |||
| } | |||
| #if JUCE_IN_APP_PURCHASES_USE_SANDBOX_ENVIRONMENT | |||
| auto storeURL = "https://sandbox.itunes.apple.com/verifyReceipt"; | |||
| #else | |||
| auto storeURL = "https://buy.itunes.apple.com/verifyReceipt"; | |||
| #endif | |||
| // TODO: use juce URL here | |||
| auto* storeRequest = [NSMutableURLRequest requestWithURL: [NSURL URLWithString: nsStringLiteral (storeURL)]]; | |||
| [storeRequest setHTTPMethod: nsStringLiteral ("POST")]; | |||
| [storeRequest setHTTPBody: requestData]; | |||
| auto* task = [[NSURLSession sharedSession] dataTaskWithRequest: storeRequest | |||
| completionHandler: | |||
| ^(NSData* data, NSURLResponse*, NSError* connectionError) | |||
| { | |||
| if (connectionError != nil) | |||
| { | |||
| owner.listeners.call (&Listener::purchasesListRestored, {}, false, NEEDS_TRANS ("Receipt fetch failed")); | |||
| } | |||
| else | |||
| { | |||
| NSError* err; | |||
| if (NSDictionary* receiptDetails = [NSJSONSerialization JSONObjectWithData: data options: 0 error: &err]) | |||
| processReceiptDetails (receiptDetails); | |||
| else | |||
| owner.listeners.call (&Listener::purchasesListRestored, {}, false, NEEDS_TRANS ("Receipt fetch failed")); | |||
| } | |||
| }]; | |||
| [task resume]; | |||
| } | |||
| void processReceiptDetails (NSDictionary* receiptDetails) | |||
| { | |||
| if (auto receipt = getAs<NSDictionary> (receiptDetails[nsStringLiteral ("receipt")])) | |||
| { | |||
| if (auto bundleId = getAs<NSString> (receipt[nsStringLiteral ("bundle_id")])) | |||
| { | |||
| if (auto inAppPurchases = getAs<NSArray> (receipt[nsStringLiteral ("in_app")])) | |||
| { | |||
| Array<Listener::PurchaseInfo> purchases; | |||
| for (id inAppPurchaseData in inAppPurchases) | |||
| { | |||
| if (auto* purchaseData = getAs<NSDictionary> (inAppPurchaseData)) | |||
| { | |||
| // Ignore products that were cancelled. | |||
| if (purchaseData[nsStringLiteral ("cancellation_date")] != nil) | |||
| continue; | |||
| if (auto transactionId = getAs<NSString> (purchaseData[nsStringLiteral ("original_transaction_id")])) | |||
| { | |||
| if (auto productId = getAs<NSString> (purchaseData[nsStringLiteral ("product_id")])) | |||
| { | |||
| if (auto purchaseTime = getAs<NSNumber> (purchaseData[nsStringLiteral ("purchase_date_ms")])) | |||
| { | |||
| purchases.add ({ { nsStringToJuce (transactionId), | |||
| nsStringToJuce (productId), | |||
| nsStringToJuce (bundleId), | |||
| Time ([purchaseTime integerValue]).toString (true, true, true, true), | |||
| {} }, {} }); | |||
| } | |||
| else | |||
| { | |||
| return sendReceiptFetchFail(); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| else | |||
| { | |||
| return sendReceiptFetchFail(); | |||
| } | |||
| } | |||
| MessageManager::callAsync ([this, purchases]() { owner.listeners.call (&Listener::purchasesListRestored, | |||
| purchases, true, NEEDS_TRANS ("Success")); }); | |||
| return; | |||
| } | |||
| } | |||
| } | |||
| sendReceiptFetchFail(); | |||
| } | |||
| void sendReceiptFetchFail() | |||
| { | |||
| MessageManager::callAsync ([this]() { owner.listeners.call (&Listener::purchasesListRestored, | |||
| {}, false, NEEDS_TRANS ("Receipt fetch failed")); }); | |||
| } | |||
| //============================================================================== | |||
| static Product SKProductToIAPProduct (SKProduct* skProduct) | |||
| { | |||
| NSNumberFormatter* numberFormatter = [[NSNumberFormatter alloc] init]; | |||
| [numberFormatter setFormatterBehavior: NSNumberFormatterBehavior10_4]; | |||
| [numberFormatter setNumberStyle: NSNumberFormatterCurrencyStyle]; | |||
| [numberFormatter setLocale: skProduct.priceLocale]; | |||
| auto identifier = nsStringToJuce (skProduct.productIdentifier); | |||
| auto title = nsStringToJuce (skProduct.localizedTitle); | |||
| auto description = nsStringToJuce (skProduct.localizedDescription); | |||
| auto priceLocale = nsStringToJuce (skProduct.priceLocale.languageCode); | |||
| auto price = nsStringToJuce ([numberFormatter stringFromNumber: skProduct.price]); | |||
| [numberFormatter release]; | |||
| return { identifier, title, description, price, priceLocale }; | |||
| } | |||
| static String SKPaymentTransactionStateToString (SKPaymentTransactionState state) | |||
| { | |||
| switch (state) | |||
| { | |||
| case SKPaymentTransactionStatePurchasing: return NEEDS_TRANS ("Purchasing"); | |||
| case SKPaymentTransactionStatePurchased: return NEEDS_TRANS ("Success"); | |||
| case SKPaymentTransactionStateFailed: return NEEDS_TRANS ("Failure"); | |||
| case SKPaymentTransactionStateRestored: return NEEDS_TRANS ("Restored"); | |||
| case SKPaymentTransactionStateDeferred: return NEEDS_TRANS ("Deferred"); | |||
| default: jassertfalse; return NEEDS_TRANS ("Unknown status"); | |||
| } | |||
| } | |||
| static Download::Status SKDownloadStateToDownloadStatus (SKDownloadState state) | |||
| { | |||
| switch (state) | |||
| { | |||
| case SKDownloadStateWaiting: return Download::Status::waiting; | |||
| case SKDownloadStateActive: return Download::Status::active; | |||
| case SKDownloadStatePaused: return Download::Status::paused; | |||
| case SKDownloadStateFinished: return Download::Status::finished; | |||
| case SKDownloadStateFailed: return Download::Status::failed; | |||
| case SKDownloadStateCancelled: return Download::Status::cancelled; | |||
| default: jassertfalse; return Download::Status::waiting; | |||
| } | |||
| } | |||
| static NSArray<SKDownload*>* downloadsToSKDownloads (const Array<Download*>& downloads) | |||
| { | |||
| NSMutableArray<SKDownload*>* skDownloads = [NSMutableArray arrayWithCapacity: (NSUInteger) downloads.size()]; | |||
| for (const auto& d : downloads) | |||
| if (auto impl = dynamic_cast<DownloadImpl*>(d)) | |||
| [skDownloads addObject: impl->download]; | |||
| return skDownloads; | |||
| } | |||
| template <typename ObjCType> | |||
| static ObjCType* getAs (id o) | |||
| { | |||
| if (o == nil || ! [o isKindOfClass: [ObjCType class]]) | |||
| return nil; | |||
| return (ObjCType*) o; | |||
| } | |||
| //============================================================================== | |||
| InAppPurchases& owner; | |||
| OwnedArray<PendingProductInfoRequest> pendingProductInfoRequests; | |||
| OwnedArray<PendingReceiptRefreshRequest> pendingReceiptRefreshRequests; | |||
| OwnedArray<PendingDownloadsTransaction> pendingDownloadsTransactions; | |||
| Array<Listener::PurchaseInfo> restoredPurchases; | |||
| }; | |||