diff options
author | Eskil Abrahamsen Blomfeldt <eskil.abrahamsen-blomfeldt@digia.com> | 2014-02-13 11:32:19 +0100 |
---|---|---|
committer | Andy Nichols <andy.nichols@digia.com> | 2014-02-13 14:08:44 +0200 |
commit | cc249300a9088039f1025c20afea74beac95411a (patch) | |
tree | 8e6f95d13b44763a0d7fc5e94ec8bf6602de5275 | |
parent | fcc2667febd3122eb1e2d4c0a119e12e609a0067 (diff) |
Initial commit of Qt Mobile Extras
At this point, contains an unfinished in-app purchase API
with an unfinished Android implementation.
Change-Id: Id01b61f6d588557404a7df0b002b4b6d3cb00f33
Reviewed-by: Andy Nichols <andy.nichols@digia.com>
50 files changed, 3566 insertions, 0 deletions
diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1a045fa --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +.tag export-subst +.gitignore export-ignore +.gitattributes export-ignore +.commit-template export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4a5200a --- /dev/null +++ b/.gitignore @@ -0,0 +1,126 @@ +# This file is used to ignore files which are generated in the Qt build system +# ---------------------------------------------------------------------------- + +callgrind.out.* +pcviewer.cfg +*~ +*.a +*.la +*.core +*.moc +*.o +*.obj +*.orig +*.swp +*.rej +*.so +*.pbxuser +*.mode1 +*.mode1v3 +*_pch.h.cpp +*_resource.rc +.#* +*.*# +core +.qmake.cache +.qmake.vars +*.prl +tags +.DS_Store +*.debug +Makefile* +*.prl +*.app +*.pro.user +*.qmlproject.user +*.gcov +moc_*.cpp +ui_*.h +qrc_*.cpp + +# Test generated files +QObject.log +tst_* +!tst_*.* +tst_*.log +tst_*.debug +tst_*~ + +# xemacs temporary files +*.flc + +# Vim temporary files +.*.swp + +# Visual Studio generated files +*.ib_pdb_index +*.idb +*.ilk +*.pdb +*.sln +*.suo +*.vcproj +*vcproj.*.*.user +*.ncb +*.vcxproj +*.vcxproj.filters +*.vcxproj.user + +# MinGW generated files +*.Debug +*.Release + +# Symlinks generated by configure +.DS_Store +.pch +.rcc +*.app + + +# Directories to ignore +# --------------------- + +debug +include/* +include/*/* +lib/* +!lib/fonts +!lib/README +plugins/*/* +release +tmp +doc-build +doc/html/* +doc/qch +doc-build +.rcc +.pch +.metadata + +# runonphone crash dumps +d_exc_*.txt +d_exc_*.stk + +# Generated by abldfast.bat from devtools. +.abldsteps.* + +# Carbide project files +# --------------------- +.project +.cproject +.make.cache +*.d + +qtc-debugging-helper + +.pc/ + +# INTEGRITY generated files +*.gpj +*.int +*.ael +*.dla +*.dnm +*.dep +*.map +work diff --git a/.qmake.conf b/.qmake.conf new file mode 100644 index 0000000..16328b7 --- /dev/null +++ b/.qmake.conf @@ -0,0 +1,3 @@ +load(qt_build_config) + +MODULE_VERSION = 5.3.0 @@ -0,0 +1 @@ +$Format:%H$ diff --git a/qtmobile-extras.pro b/qtmobile-extras.pro new file mode 100644 index 0000000..58c33f2 --- /dev/null +++ b/qtmobile-extras.pro @@ -0,0 +1 @@ +load(qt_parts) diff --git a/src/android/android.pro b/src/android/android.pro new file mode 100644 index 0000000..8d19c1b --- /dev/null +++ b/src/android/android.pro @@ -0,0 +1,2 @@ +TEMPLATE = subdirs +SUBDIRS += bundledjar.pro distributedjar.pro diff --git a/src/android/bundledjar.pro b/src/android/bundledjar.pro new file mode 100644 index 0000000..f6348dc --- /dev/null +++ b/src/android/bundledjar.pro @@ -0,0 +1,3 @@ +TARGET = QtMobileExtras-bundled +CONFIG += bundled_jar_file +include(jar.pri) diff --git a/src/android/distributedjar.pro b/src/android/distributedjar.pro new file mode 100644 index 0000000..80ad488 --- /dev/null +++ b/src/android/distributedjar.pro @@ -0,0 +1,2 @@ +TARGET = QtMobileExtras +include(jar.pri) diff --git a/src/android/jar.pri b/src/android/jar.pri new file mode 100644 index 0000000..84fac10 --- /dev/null +++ b/src/android/jar.pri @@ -0,0 +1,52 @@ +CONFIG += java +DESTDIR = $$[QT_INSTALL_PREFIX/get]/jar + +PATHPREFIX = $$PWD/src/com/digia/qt5/android/mobileextras/ + +!build_pass { + isEmpty(SDK_ROOT): SDK_ROOT = $$(ANDROID_SDK_ROOT) + isEmpty(SDK_ROOT): SDK_ROOT = $$DEFAULT_ANDROID_SDK_ROOT + + isEmpty(BUILD_TOOLS_REVISION) { + BUILD_TOOLS_REVISION = $$(ANDROID_BUILD_TOOLS_REVISION) + isEmpty(BUILD_TOOLS_REVISION) { + BUILD_TOOLS_REVISIONS = $$files($$SDK_ROOT/build-tools/*) + for (REVISION, BUILD_TOOLS_REVISIONS) { + BASENAME = $$basename(REVISION) + greaterThan(BASENAME, $$BUILD_TOOLS_REVISION): BUILD_TOOLS_REVISION = $$BASENAME + } + } + } + + API_VERSION_TO_USE = $$(ANDROID_API_VERSION) + isEmpty(API_VERSION_TO_USE): API_VERSION_TO_USE = $$API_VERSION + isEmpty(API_VERSION_TO_USE): API_VERSION_TO_USE = android-10 + + FRAMEWORK_AIDL_FILE = $$SDK_ROOT/platforms/$$API_VERSION_TO_USE/framework.aidl + !exists($$FRAMEWORK_AIDL_FILE) { + error("The Path $$FRAMEWORK_AIDL_FILE does not exist. Make sure the ANDROID_SDK_ROOT and ANDROID_API_VERSION environment variables are correctly set.") + } + + AIDL_CMD = $$SDK_ROOT/platform-tools/aidl + contains(QMAKE_HOST.os, Windows): AIDL_CMD += ".exe" + !exists($$AIDL_CMD): AIDL_CMD = $$SDK_ROOT/build-tools/$$BUILD_TOOLS_REVISION/aidl + !exists($$AIDL_CMD): error("The path $$AIDL_CMD does not exist. Please set the environment variable ANDROID_BUILD_TOOLS_REVISION to the revision of the build tools installed in your Android SDK.") + + system($$AIDL_CMD -I$$PWD/src -p$$FRAMEWORK_AIDL_FILE $$PWD/src/com/android/vending/billing/IInAppBillingService.aidl $$PWD/src/com/android/vending/billing/IInAppBillingService.java) +} + +JAVACLASSPATH += $$PWD/src/ +JAVASOURCES += \ + $$PATHPREFIX/QtInAppPurchase.java \ + $$PATHPREFIX/Security.java \ + $$PATHPREFIX/Base64.java \ + $$PATHPREFIX/Base64DecoderException.java + $$PWD/src/android/vending/billing/.java + + + +# install +target.path = $$[QT_INSTALL_PREFIX]/jar +INSTALLS += target + +OTHER_FILES += $$JAVASOURCES diff --git a/src/android/src/com/android/vending/billing/IInAppBillingService.aidl b/src/android/src/com/android/vending/billing/IInAppBillingService.aidl new file mode 100644 index 0000000..2a492f7 --- /dev/null +++ b/src/android/src/com/android/vending/billing/IInAppBillingService.aidl @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.vending.billing; + +import android.os.Bundle; + +/** + * 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_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 + */ +interface IInAppBillingService { + /** + * Checks support for the requested billing API version, package and in-app type. + * Minimum API version supported by this interface is 3. + * @param apiVersion the billing version which the app is using + * @param packageName the package name of the calling app + * @param type type of the in-app item being purchased "inapp" for one-time purchases + * and "subs" for subscription. + * @return RESULT_OK(0) on success, corresponding result code on failures + */ + int isBillingSupported(int apiVersion, String packageName, String type); + + /** + * 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 Third-party is using + * @param packageName the package name of the calling app + * @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, other response codes on + * failure as listed above. + * "DETAILS_LIST" with a StringArrayList containing purchase information + * in JSON format similar to: + * '{ "productId" : "exampleSku", "type" : "inapp", "price" : "$5.00", + * "title : "Example Title", "description" : "This is an example description" }' + */ + Bundle getSkuDetails(int apiVersion, String packageName, String type, in Bundle skusBundle); + + /** + * 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 the type of the in-app item ("inapp" for one-time purchases + * and "subs" for subscription). + * @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, other response codes on + * failure as listed above. + * "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, other response codes on + * failure as listed above. + * "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 + * TODO: change this to app-specific keys. + */ + Bundle getBuyIntent(int apiVersion, String packageName, String sku, String type, + String developerPayload); + + /** + * 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 the type of the in-app items being requested + * ("inapp" for one-time purchases and "subs" for subscription). + * @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, other response codes on + * failure as listed above. + * "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. + */ + Bundle getPurchases(int apiVersion, String packageName, String type, String continuationToken); + + /** + * Consume the last purchase of the given SKU. This will result in this item being removed + * from all subsequent responses to getPurchases() and allow re-purchase of this item. + * @param apiVersion billing API version that the app is using + * @param packageName package name of the calling app + * @param purchaseToken token in the purchase information JSON that identifies the purchase + * to be consumed + * @return 0 if consumption succeeded. Appropriate error values for failures. + */ + int consumePurchase(int apiVersion, String packageName, String purchaseToken); +} diff --git a/src/android/src/com/digia/qt5/android/mobileextras/Base64.java b/src/android/src/com/digia/qt5/android/mobileextras/Base64.java new file mode 100644 index 0000000..2eb71fa --- /dev/null +++ b/src/android/src/com/digia/qt5/android/mobileextras/Base64.java @@ -0,0 +1,571 @@ +// Portions copyright 2002, Google, Inc. +// Portions copyright (c) 2014 Digia Plc and/or its subsidiary(-ies). +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.digia.qt5.android.mobileextras; + +// This code was converted from code at http://iharder.sourceforge.net/base64/ +// Lots of extraneous features were removed. +/* The original code said: + * <p> + * I am placing this code in the Public Domain. Do with it as you will. + * This software comes with no guarantees or warranties but with + * plenty of well-wishing instead! + * Please visit + * <a href="http://iharder.net/xmlizable">http://iharder.net/xmlizable</a> + * periodically to check for updates or to contribute improvements. + * </p> + * + * @author Robert Harder + * @author rharder@usa.net + * @version 1.3 + */ + +/** + * Base64 converter class. This code is not a complete MIME encoder; + * it simply converts binary data to base64 data and back. + * + * <p>Note {@link CharBase64} is a GWT-compatible implementation of this + * class. + */ +public class Base64 { + /** Specify encoding (value is {@code true}). */ + public final static boolean ENCODE = true; + + /** Specify decoding (value is {@code false}). */ + public final static boolean DECODE = false; + + /** The equals sign (=) as a byte. */ + private final static byte EQUALS_SIGN = (byte) '='; + + /** The new line character (\n) as a byte. */ + private final static byte NEW_LINE = (byte) '\n'; + + /** + * The 64 valid Base64 values. + */ + private final static byte[] ALPHABET = + {(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', + (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', + (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P', + (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', + (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', + (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', + (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', + (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o', + (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', + (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', + (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3', + (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8', + (byte) '9', (byte) '+', (byte) '/'}; + + /** + * The 64 valid web safe Base64 values. + */ + private final static byte[] WEBSAFE_ALPHABET = + {(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', + (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', + (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P', + (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', + (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', + (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', + (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', + (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o', + (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', + (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', + (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3', + (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8', + (byte) '9', (byte) '-', (byte) '_'}; + + /** + * Translates a Base64 value to either its 6-bit reconstruction value + * or a negative number indicating some other meaning. + **/ + private final static byte[] DECODABET = {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8 + -5, -5, // Whitespace: Tab and Linefeed + -9, -9, // Decimal 11 - 12 + -5, // Whitespace: Carriage Return + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 + -9, -9, -9, -9, -9, // Decimal 27 - 31 + -5, // Whitespace: Space + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42 + 62, // Plus sign at decimal 43 + -9, -9, -9, // Decimal 44 - 46 + 63, // Slash at decimal 47 + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine + -9, -9, -9, // Decimal 58 - 60 + -1, // Equals sign at decimal 61 + -9, -9, -9, // Decimal 62 - 64 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N' + 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z' + -9, -9, -9, -9, -9, -9, // Decimal 91 - 96 + 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm' + 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z' + -9, -9, -9, -9, -9 // Decimal 123 - 127 + /* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */ + }; + + /** The web safe decodabet */ + private final static byte[] WEBSAFE_DECODABET = + {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8 + -5, -5, // Whitespace: Tab and Linefeed + -9, -9, // Decimal 11 - 12 + -5, // Whitespace: Carriage Return + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 + -9, -9, -9, -9, -9, // Decimal 27 - 31 + -5, // Whitespace: Space + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 44 + 62, // Dash '-' sign at decimal 45 + -9, -9, // Decimal 46-47 + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine + -9, -9, -9, // Decimal 58 - 60 + -1, // Equals sign at decimal 61 + -9, -9, -9, // Decimal 62 - 64 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N' + 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z' + -9, -9, -9, -9, // Decimal 91-94 + 63, // Underscore '_' at decimal 95 + -9, // Decimal 96 + 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm' + 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z' + -9, -9, -9, -9, -9 // Decimal 123 - 127 + /* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */ + }; + + // Indicates white space in encoding + private final static byte WHITE_SPACE_ENC = -5; + // Indicates equals sign in encoding + private final static byte EQUALS_SIGN_ENC = -1; + + /** Defeats instantiation. */ + private Base64() { + } + + /* ******** E N C O D I N G M E T H O D S ******** */ + + /** + * Encodes up to three bytes of the array <var>source</var> + * and writes the resulting four Base64 bytes to <var>destination</var>. + * The source and destination arrays can be manipulated + * anywhere along their length by specifying + * <var>srcOffset</var> and <var>destOffset</var>. + * This method does not check to make sure your arrays + * are large enough to accommodate <var>srcOffset</var> + 3 for + * the <var>source</var> array or <var>destOffset</var> + 4 for + * the <var>destination</var> array. + * The actual number of significant bytes in your array is + * given by <var>numSigBytes</var>. + * + * @param source the array to convert + * @param srcOffset the index where conversion begins + * @param numSigBytes the number of significant bytes in your array + * @param destination the array to hold the conversion + * @param destOffset the index where output will be put + * @param alphabet is the encoding alphabet + * @return the <var>destination</var> array + * @since 1.3 + */ + private static byte[] encode3to4(byte[] source, int srcOffset, + int numSigBytes, byte[] destination, int destOffset, byte[] alphabet) { + // 1 2 3 + // 01234567890123456789012345678901 Bit position + // --------000000001111111122222222 Array position from threeBytes + // --------| || || || | Six bit groups to index alphabet + // >>18 >>12 >> 6 >> 0 Right shift necessary + // 0x3f 0x3f 0x3f Additional AND + + // Create buffer with zero-padding if there are only one or two + // significant bytes passed in the array. + // We have to shift left 24 in order to flush out the 1's that appear + // when Java treats a value as negative that is cast from a byte to an int. + int inBuff = + (numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0) + | (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0) + | (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0); + + switch (numSigBytes) { + case 3: + destination[destOffset] = alphabet[(inBuff >>> 18)]; + destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; + destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f]; + destination[destOffset + 3] = alphabet[(inBuff) & 0x3f]; + return destination; + case 2: + destination[destOffset] = alphabet[(inBuff >>> 18)]; + destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; + destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f]; + destination[destOffset + 3] = EQUALS_SIGN; + return destination; + case 1: + destination[destOffset] = alphabet[(inBuff >>> 18)]; + destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; + destination[destOffset + 2] = EQUALS_SIGN; + destination[destOffset + 3] = EQUALS_SIGN; + return destination; + default: + return destination; + } // end switch + } // end encode3to4 + + /** + * Encodes a byte array into Base64 notation. + * Equivalent to calling + * {@code encodeBytes(source, 0, source.length)} + * + * @param source The data to convert + * @since 1.4 + */ + public static String encode(byte[] source) { + return encode(source, 0, source.length, ALPHABET, true); + } + + /** + * Encodes a byte array into web safe Base64 notation. + * + * @param source The data to convert + * @param doPadding is {@code true} to pad result with '=' chars + * if it does not fall on 3 byte boundaries + */ + public static String encodeWebSafe(byte[] source, boolean doPadding) { + return encode(source, 0, source.length, WEBSAFE_ALPHABET, doPadding); + } + + /** + * Encodes a byte array into Base64 notation. + * + * @param source the data to convert + * @param off offset in array where conversion should begin + * @param len length of data to convert + * @param alphabet the encoding alphabet + * @param doPadding is {@code true} to pad result with '=' chars + * if it does not fall on 3 byte boundaries + * @since 1.4 + */ + public static String encode(byte[] source, int off, int len, byte[] alphabet, + boolean doPadding) { + byte[] outBuff = encode(source, off, len, alphabet, Integer.MAX_VALUE); + int outLen = outBuff.length; + + // If doPadding is false, set length to truncate '=' + // padding characters + while (doPadding == false && outLen > 0) { + if (outBuff[outLen - 1] != '=') { + break; + } + outLen -= 1; + } + + return new String(outBuff, 0, outLen); + } + + /** + * Encodes a byte array into Base64 notation. + * + * @param source the data to convert + * @param off offset in array where conversion should begin + * @param len length of data to convert + * @param alphabet is the encoding alphabet + * @param maxLineLength maximum length of one line. + * @return the BASE64-encoded byte array + */ + public static byte[] encode(byte[] source, int off, int len, byte[] alphabet, + int maxLineLength) { + int lenDiv3 = (len + 2) / 3; // ceil(len / 3) + int len43 = lenDiv3 * 4; + byte[] outBuff = new byte[len43 // Main 4:3 + + (len43 / maxLineLength)]; // New lines + + int d = 0; + int e = 0; + int len2 = len - 2; + int lineLength = 0; + for (; d < len2; d += 3, e += 4) { + + // The following block of code is the same as + // encode3to4( source, d + off, 3, outBuff, e, alphabet ); + // but inlined for faster encoding (~20% improvement) + int inBuff = + ((source[d + off] << 24) >>> 8) + | ((source[d + 1 + off] << 24) >>> 16) + | ((source[d + 2 + off] << 24) >>> 24); + outBuff[e] = alphabet[(inBuff >>> 18)]; + outBuff[e + 1] = alphabet[(inBuff >>> 12) & 0x3f]; + outBuff[e + 2] = alphabet[(inBuff >>> 6) & 0x3f]; + outBuff[e + 3] = alphabet[(inBuff) & 0x3f]; + + lineLength += 4; + if (lineLength == maxLineLength) { + outBuff[e + 4] = NEW_LINE; + e++; + lineLength = 0; + } // end if: end of line + } // end for: each piece of array + + if (d < len) { + encode3to4(source, d + off, len - d, outBuff, e, alphabet); + + lineLength += 4; + if (lineLength == maxLineLength) { + // Add a last newline + outBuff[e + 4] = NEW_LINE; + e++; + } + e += 4; + } + + assert (e == outBuff.length); + return outBuff; + } + + + /* ******** D E C O D I N G M E T H O D S ******** */ + + + /** + * Decodes four bytes from array <var>source</var> + * and writes the resulting bytes (up to three of them) + * to <var>destination</var>. + * The source and destination arrays can be manipulated + * anywhere along their length by specifying + * <var>srcOffset</var> and <var>destOffset</var>. + * This method does not check to make sure your arrays + * are large enough to accommodate <var>srcOffset</var> + 4 for + * the <var>source</var> array or <var>destOffset</var> + 3 for + * the <var>destination</var> array. + * This method returns the actual number of bytes that + * were converted from the Base64 encoding. + * + * + * @param source the array to convert + * @param srcOffset the index where conversion begins + * @param destination the array to hold the conversion + * @param destOffset the index where output will be put + * @param decodabet the decodabet for decoding Base64 content + * @return the number of decoded bytes converted + * @since 1.3 + */ + private static int decode4to3(byte[] source, int srcOffset, + byte[] destination, int destOffset, byte[] decodabet) { + // Example: Dk== + if (source[srcOffset + 2] == EQUALS_SIGN) { + int outBuff = + ((decodabet[source[srcOffset]] << 24) >>> 6) + | ((decodabet[source[srcOffset + 1]] << 24) >>> 12); + + destination[destOffset] = (byte) (outBuff >>> 16); + return 1; + } else if (source[srcOffset + 3] == EQUALS_SIGN) { + // Example: DkL= + int outBuff = + ((decodabet[source[srcOffset]] << 24) >>> 6) + | ((decodabet[source[srcOffset + 1]] << 24) >>> 12) + | ((decodabet[source[srcOffset + 2]] << 24) >>> 18); + + destination[destOffset] = (byte) (outBuff >>> 16); + destination[destOffset + 1] = (byte) (outBuff >>> 8); + return 2; + } else { + // Example: DkLE + int outBuff = + ((decodabet[source[srcOffset]] << 24) >>> 6) + | ((decodabet[source[srcOffset + 1]] << 24) >>> 12) + | ((decodabet[source[srcOffset + 2]] << 24) >>> 18) + | ((decodabet[source[srcOffset + 3]] << 24) >>> 24); + + destination[destOffset] = (byte) (outBuff >> 16); + destination[destOffset + 1] = (byte) (outBuff >> 8); + destination[destOffset + 2] = (byte) (outBuff); + return 3; + } + } // end decodeToBytes + + + /** + * Decodes data from Base64 notation. + * + * @param s the string to decode (decoded in default encoding) + * @return the decoded data + * @since 1.4 + */ + public static byte[] decode(String s) throws Base64DecoderException { + byte[] bytes = s.getBytes(); + return decode(bytes, 0, bytes.length); + } + + /** + * Decodes data from web safe Base64 notation. + * Web safe encoding uses '-' instead of '+', '_' instead of '/' + * + * @param s the string to decode (decoded in default encoding) + * @return the decoded data + */ + public static byte[] decodeWebSafe(String s) throws Base64DecoderException { + byte[] bytes = s.getBytes(); + return decodeWebSafe(bytes, 0, bytes.length); + } + + /** + * Decodes Base64 content in byte array format and returns + * the decoded byte array. + * + * @param source The Base64 encoded data + * @return decoded data + * @since 1.3 + * @throws Base64DecoderException + */ + public static byte[] decode(byte[] source) throws Base64DecoderException { + return decode(source, 0, source.length); + } + + /** + * Decodes web safe Base64 content in byte array format and returns + * the decoded data. + * Web safe encoding uses '-' instead of '+', '_' instead of '/' + * + * @param source the string to decode (decoded in default encoding) + * @return the decoded data + */ + public static byte[] decodeWebSafe(byte[] source) + throws Base64DecoderException { + return decodeWebSafe(source, 0, source.length); + } + + /** + * Decodes Base64 content in byte array format and returns + * the decoded byte array. + * + * @param source the Base64 encoded data + * @param off the offset of where to begin decoding + * @param len the length of characters to decode + * @return decoded data + * @since 1.3 + * @throws Base64DecoderException + */ + public static byte[] decode(byte[] source, int off, int len) + throws Base64DecoderException { + return decode(source, off, len, DECODABET); + } + + /** + * Decodes web safe Base64 content in byte array format and returns + * the decoded byte array. + * Web safe encoding uses '-' instead of '+', '_' instead of '/' + * + * @param source the Base64 encoded data + * @param off the offset of where to begin decoding + * @param len the length of characters to decode + * @return decoded data + */ + public static byte[] decodeWebSafe(byte[] source, int off, int len) + throws Base64DecoderException { + return decode(source, off, len, WEBSAFE_DECODABET); + } + + /** + * Decodes Base64 content using the supplied decodabet and returns + * the decoded byte array. + * + * @param source the Base64 encoded data + * @param off the offset of where to begin decoding + * @param len the length of characters to decode + * @param decodabet the decodabet for decoding Base64 content + * @return decoded data + */ + public static byte[] decode(byte[] source, int off, int len, byte[] decodabet) + throws Base64DecoderException { + int len34 = len * 3 / 4; + byte[] outBuff = new byte[2 + len34]; // Upper limit on size of output + int outBuffPosn = 0; + + byte[] b4 = new byte[4]; + int b4Posn = 0; + int i = 0; + byte sbiCrop = 0; + byte sbiDecode = 0; + for (i = 0; i < len; i++) { + sbiCrop = (byte) (source[i + off] & 0x7f); // Only the low seven bits + sbiDecode = decodabet[sbiCrop]; + + if (sbiDecode >= WHITE_SPACE_ENC) { // White space Equals sign or better + if (sbiDecode >= EQUALS_SIGN_ENC) { + // An equals sign (for padding) must not occur at position 0 or 1 + // and must be the last byte[s] in the encoded value + if (sbiCrop == EQUALS_SIGN) { + int bytesLeft = len - i; + byte lastByte = (byte) (source[len - 1 + off] & 0x7f); + if (b4Posn == 0 || b4Posn == 1) { + throw new Base64DecoderException( + "invalid padding byte '=' at byte offset " + i); + } else if ((b4Posn == 3 && bytesLeft > 2) + || (b4Posn == 4 && bytesLeft > 1)) { + throw new Base64DecoderException( + "padding byte '=' falsely signals end of encoded value " + + "at offset " + i); + } else if (lastByte != EQUALS_SIGN && lastByte != NEW_LINE) { + throw new Base64DecoderException( + "encoded value has invalid trailing byte"); + } + break; + } + + b4[b4Posn++] = sbiCrop; + if (b4Posn == 4) { + outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet); + b4Posn = 0; + } + } + } else { + throw new Base64DecoderException("Bad Base64 input character at " + i + + ": " + source[i + off] + "(decimal)"); + } + } + + // Because web safe encoding allows non padding base64 encodes, we + // need to pad the rest of the b4 buffer with equal signs when + // b4Posn != 0. There can be at most 2 equal signs at the end of + // four characters, so the b4 buffer must have two or three + // characters. This also catches the case where the input is + // padded with EQUALS_SIGN + if (b4Posn != 0) { + if (b4Posn == 1) { + throw new Base64DecoderException("single trailing character at offset " + + (len - 1)); + } + b4[b4Posn++] = EQUALS_SIGN; + outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet); + } + + byte[] out = new byte[outBuffPosn]; + System.arraycopy(outBuff, 0, out, 0, outBuffPosn); + return out; + } +} diff --git a/src/android/src/com/digia/qt5/android/mobileextras/Base64DecoderException.java b/src/android/src/com/digia/qt5/android/mobileextras/Base64DecoderException.java new file mode 100644 index 0000000..e24eb36 --- /dev/null +++ b/src/android/src/com/digia/qt5/android/mobileextras/Base64DecoderException.java @@ -0,0 +1,33 @@ +// Copyright 2002, Google, Inc. +// Copyright (c) 2014 Digia Plc and/or its subsidiary(-ies). +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.digia.qt5.android.mobileextras; + +/** + * Exception thrown when encountering an invalid Base64 input character. + * + * @author nelson + */ +public class Base64DecoderException extends Exception { + public Base64DecoderException() { + super(); + } + + public Base64DecoderException(String s) { + super(s); + } + + private static final long serialVersionUID = 1L; +} diff --git a/src/android/src/com/digia/qt5/android/mobileextras/QtInAppPurchase.java b/src/android/src/com/digia/qt5/android/mobileextras/QtInAppPurchase.java new file mode 100644 index 0000000..63fbc7a --- /dev/null +++ b/src/android/src/com/digia/qt5/android/mobileextras/QtInAppPurchase.java @@ -0,0 +1,267 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Mobile Extras Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +package com.digia.qt5.android.mobileextras; + +import java.util.ArrayList; +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.pm.PackageManager; +import android.os.RemoteException; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.util.Log; +import org.json.JSONObject; +import org.json.JSONException; + + +import com.android.vending.billing.IInAppBillingService; + +public class QtInAppPurchase +{ + private Context m_context = null; + private IInAppBillingService m_service = null; + private String m_publicKey = null; + private long m_nativePointer; + + public static final int RESULT_OK = 0; + public static final int RESULT_QTMOBILEEXTRAS_ERROR = 9; // No match with any already defined response codes + public static final String TAG = "QtInAppPurchase"; + public static final String TYPE_INAPP = "inapp"; + public static final int IAP_VERSION = 3; + + private ServiceConnection m_serviceConnection = new ServiceConnection() + { + @Override + public void onServiceConnected(ComponentName name, IBinder service) + { + m_service = IInAppBillingService.Stub.asInterface(service); + try { + int response = m_service.isBillingSupported(3, m_context.getPackageName(), TYPE_INAPP); + if (response != RESULT_OK) { + Log.e(TAG, "In-app billing not supported"); + return; + } + } catch (RemoteException e) { + e.printStackTrace(); + } + + // Asynchronously populate list of purchased products + final Handler handler = new Handler(); + Thread thread = new Thread(new Runnable() + { + public void run() + { + queryPurchasedProducts(); + handler.post(new Runnable() + { + public void run() { purchasedProductsQueried(m_nativePointer); } + }); + } + }); + thread.start(); + } + + @Override + public void onServiceDisconnected(ComponentName name) + { + m_service = null; + } + }; + + public QtInAppPurchase(Context context, long nativePointer) + { + m_context = context; + m_nativePointer = nativePointer; + } + + public void initializeConnection() + { + + Intent serviceIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND"); + if (!m_context.getPackageManager().queryIntentServices(serviceIntent, 0).isEmpty()) { + m_context.bindService(serviceIntent, m_serviceConnection, Context.BIND_AUTO_CREATE); + } else { + Log.e(TAG, "No in-app billing service available."); + purchasedProductsQueried(m_nativePointer); + } + } + + private int bundleResponseCode(Bundle bundle) + { + Object o = bundle.get("RESPONSE_CODE"); + if (o == null) { + // Works around known issue where the response code is not bundled. + return RESULT_OK; + } else if (o instanceof Integer) { + return ((Integer)o).intValue(); + } else if (o instanceof Long) { + return (int)((Long)o).longValue(); + } + + Log.e(TAG, "Unexpected result for response code: " + o); + return RESULT_QTMOBILEEXTRAS_ERROR; + } + + private void queryPurchasedProducts() + { + if (m_service == null) { + Log.e(TAG, "queryPurchasedProducts: Service not initialized"); + return; + } + + String continuationToken = null; + try { + do { + Bundle ownedItems = m_service.getPurchases(IAP_VERSION, + m_context.getPackageName(), + TYPE_INAPP, + continuationToken); + int responseCode = bundleResponseCode(ownedItems); + if (responseCode != RESULT_OK) { + Log.e(TAG, "queryPurchasedProducts: Failed to query purchases products"); + return; + } + + ArrayList<String> dataList = ownedItems.getStringArrayList("INAPP_PURCHASE_DATA_LIST"); + if (dataList == null) { + Log.e(TAG, "queryPurchasedProducts: No data list in bundle"); + return; + } + + ArrayList<String> signatureList = ownedItems.getStringArrayList("INAPP_PURCHASE_SIGNATURE_LIST"); + if (signatureList == null) { + Log.e(TAG, "queryPurchasedProducts: No signature list in bundle"); + return; + } + + if (dataList.size() != signatureList.size()) { + Log.e(TAG, "queryPurchasedProducts: Mismatching sizes of lists in bundle"); + return; + } + + for (int i=0; i<dataList.size(); ++i) { + String data = dataList.get(i); + String signature = signatureList.get(i); + + if (m_publicKey != null && !Security.verifyPurchase(m_publicKey, data, signature)) { + Log.e(TAG, "queryPurchasedProducts: Cannot verify signature of purchase"); + return; + } else { + try { + JSONObject jo = new JSONObject(data); + String productId = jo.getString("productId"); + int purchaseState = jo.getInt("purchaseState"); + + if (purchaseState == 0) + registerPurchased(m_nativePointer, productId, signature, data); + } catch (JSONException e) { + e.printStackTrace(); + } + } + } + + continuationToken = ownedItems.getString("INAPP_CONTINUATION_TOKEN"); + + } while (continuationToken != null && continuationToken.length() > 0); + } catch (RemoteException e) { + e.printStackTrace(); + } + + } + + private void queryDetails(final String productId) + { + if (m_service == null) { + Log.e(TAG, "queryDetails: Service not initialized"); + queryFailed(m_nativePointer, productId); + return; + } + + // Asynchronously query details about product + Thread thread = new Thread(new Runnable() + { + public void run() + { + try { + ArrayList<String> productIds = new ArrayList<String>(); + productIds.add(productId); + Bundle productIdBundle = new Bundle(); + productIdBundle.putStringArrayList("ITEM_ID_LIST", productIds); + + Bundle bundle = m_service.getSkuDetails(IAP_VERSION, + m_context.getPackageName(), + "inapp", + productIdBundle); + int responseCode = bundleResponseCode(bundle); + if (responseCode != RESULT_OK) { + Log.e(TAG, "queryDetails: Couldn't retrieve sku details."); + queryFailed(m_nativePointer, productId); + return; + } + + ArrayList<String> detailsList = bundle.getStringArrayList("DETAILS_LIST"); + if (detailsList == null) { + Log.e(TAG, "queryDetails: No details list in response."); + queryFailed(m_nativePointer, productId); + return; + } + + for (String details : detailsList) { + try { + JSONObject jo = new JSONObject(details); + String queriedProductId = jo.getString("productId"); + String queriedPrice = jo.getString("price"); + if (queriedProductId == null || queriedPrice == null) { + Log.e(TAG, "Data missing from product details."); + } else if (productId.equals(queriedProductId)) { + registerProduct(m_nativePointer, queriedProductId, queriedPrice); + return; + } + } catch (JSONException e) { + e.printStackTrace(); + } + + } + + queryFailed(m_nativePointer, productId); + } catch (RemoteException e) { + e.printStackTrace(); + queryFailed(m_nativePointer, productId); + } + } + }); + thread.start(); + } + + public void setPublicKey(String publicKey) + { + m_publicKey = publicKey; + } + + private native static void queryFailed(long nativePointer, String productId); + private native static void purchasedProductsQueried(long nativePointer); + private native static void registerProduct(long nativePointer, String productId, String price); + private native static void registerPurchased(long nativePointer, String productId, String signature, String data); +} diff --git a/src/android/src/com/digia/qt5/android/mobileextras/Security.java b/src/android/src/com/digia/qt5/android/mobileextras/Security.java new file mode 100644 index 0000000..1c56746 --- /dev/null +++ b/src/android/src/com/digia/qt5/android/mobileextras/Security.java @@ -0,0 +1,131 @@ +/* Copyright (c) 2012 Google Inc. + * Copyright (c) 2014 Digia Plc and/or its subsidiary(-ies). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.digia.qt5.android.mobileextras; + +import android.text.TextUtils; +import android.util.Log; + +import org.json.JSONException; +import org.json.JSONObject; + + +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; + +/** + * Security-related methods. For a secure implementation, all of this code + * should be implemented on a server that communicates with the + * application on the device. For the sake of simplicity and clarity of this + * example, this code is included here and is executed on the device. If you + * must verify the purchases on the phone, you should obfuscate this code to + * make it harder for an attacker to replace the code with stubs that treat all + * purchases as verified. + */ +public class Security { + private static final String TAG = "IABUtil/Security"; + + private static final String KEY_FACTORY_ALGORITHM = "RSA"; + private static final String SIGNATURE_ALGORITHM = "SHA1withRSA"; + + /** + * Verifies that the data was signed with the given signature, and returns + * the verified purchase. The data is in JSON format and signed + * with a private key. The data also contains the {@link PurchaseState} + * and product ID of the purchase. + * @param base64PublicKey the base64-encoded public key to use for verifying. + * @param signedData the signed JSON string (signed, not encrypted) + * @param signature the signature for the data, signed with the private key + */ + public static boolean verifyPurchase(String base64PublicKey, String signedData, String signature) { + if (signedData == null) { + Log.e(TAG, "data is null"); + return false; + } + + boolean verified = false; + if (!TextUtils.isEmpty(signature)) { + PublicKey key = Security.generatePublicKey(base64PublicKey); + verified = Security.verify(key, signedData, signature); + if (!verified) { + Log.w(TAG, "signature does not match data."); + return false; + } + } + return true; + } + + /** + * Generates a PublicKey instance from a string containing the + * Base64-encoded public key. + * + * @param encodedPublicKey Base64-encoded public key + * @throws IllegalArgumentException if encodedPublicKey is invalid + */ + public static PublicKey generatePublicKey(String encodedPublicKey) { + try { + byte[] decodedKey = Base64.decode(encodedPublicKey); + KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM); + return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey)); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } catch (InvalidKeySpecException e) { + Log.e(TAG, "Invalid key specification."); + throw new IllegalArgumentException(e); + } catch (Base64DecoderException e) { + Log.e(TAG, "Base64 decoding failed."); + throw new IllegalArgumentException(e); + } + } + + /** + * Verifies that the signature from the server matches the computed + * signature on the data. Returns true if the data is correctly signed. + * + * @param publicKey public key associated with the developer account + * @param signedData signed data from server + * @param signature server signature + * @return true if the data and signature match + */ + public static boolean verify(PublicKey publicKey, String signedData, String signature) { + Signature sig; + try { + sig = Signature.getInstance(SIGNATURE_ALGORITHM); + sig.initVerify(publicKey); + sig.update(signedData.getBytes()); + if (!sig.verify(Base64.decode(signature))) { + Log.e(TAG, "Signature verification failed."); + return false; + } + return true; + } catch (NoSuchAlgorithmException e) { + Log.e(TAG, "NoSuchAlgorithmException."); + } catch (InvalidKeyException e) { + Log.e(TAG, "Invalid key specification."); + } catch (SignatureException e) { + Log.e(TAG, "Signature exception."); + } catch (Base64DecoderException e) { + Log.e(TAG, "Base64 decoding failed."); + } + return false; + } +} diff --git a/src/imports/imports.pro b/src/imports/imports.pro new file mode 100644 index 0000000..33e4bda --- /dev/null +++ b/src/imports/imports.pro @@ -0,0 +1,3 @@ +TEMPLATE = subdirs +SUBDIRS = inapppurchase + diff --git a/src/imports/inapppurchase/inapppurchase.cpp b/src/imports/inapppurchase/inapppurchase.cpp new file mode 100644 index 0000000..b6d7fa2 --- /dev/null +++ b/src/imports/inapppurchase/inapppurchase.cpp @@ -0,0 +1,68 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Mobile Extras Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qinappproductconsumable_p.h" +#include "qinappstoreqmltype_p.h" + +#include <QtQml/qqmlextensionplugin.h> +#include <QtQml/qqml.h> + +#include <QtMobileExtras/qinappproduct.h> +#include <QtMobileExtras/qinapptransaction.h> + +QT_BEGIN_NAMESPACE + +class QInAppPurchaseModule : public QQmlExtensionPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface/1.0") +public: + void registerTypes(const char *uri) + { + Q_ASSERT(QLatin1String(uri) == QLatin1String("QtMobileExtras")); + + qmlRegisterType<QInAppStoreQmlType>(uri, + 1, 0, + "Store"); + qmlRegisterType<QInAppProductConsumable>(uri, + 1, 0, + "ConsumableProduct"); + qmlRegisterType<QInAppProductConsumable>(uri, + 1, 0, + "UnlockableProduct"); + qmlRegisterUncreatableType<QInAppTransaction>(uri, + 1, 0, + "Transaction", + trUtf8("Transaction is provided by InAppStore")); + } + + void initializeEngine(QQmlEngine *engine, const char *uri) + { + Q_UNUSED(uri); + Q_UNUSED(engine); + } +}; + +QT_END_NAMESPACE + +#include "inapppurchase.moc" + + + diff --git a/src/imports/inapppurchase/inapppurchase.pro b/src/imports/inapppurchase/inapppurchase.pro new file mode 100644 index 0000000..3511dc5 --- /dev/null +++ b/src/imports/inapppurchase/inapppurchase.pro @@ -0,0 +1,21 @@ +CXX_MODULE = mobileextras +TARGET = declarative_mobileextras +TARGETPATH = QtMobileExtras +IMPORT_VERSION = 1.0 + +QT += qml quick mobileextras +SOURCES += inapppurchase.cpp \ + qinappproductconsumable.cpp \ + qinappproductqmltype.cpp \ + qinappproductunlockable.cpp \ + qinappstoreqmltype.cpp + +load(qml_plugin) + +HEADERS += \ + qinappproductconsumable_p.h \ + qinappproductqmltype_p.h \ + qinappproductunlockable_p.h \ + qinappstoreqmltype_p.h + +OTHER_FILES += qmldir diff --git a/src/imports/inapppurchase/qinappproductconsumable.cpp b/src/imports/inapppurchase/qinappproductconsumable.cpp new file mode 100644 index 0000000..3ea684a --- /dev/null +++ b/src/imports/inapppurchase/qinappproductconsumable.cpp @@ -0,0 +1,31 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Mobile Extras Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qinappproductconsumable_p.h" +#include <QtMobileExtras/qinappstore.h> + +QT_BEGIN_NAMESPACE + +QInAppProductConsumable::QInAppProductConsumable(QObject *parent) + : QInAppProductQmlType(QInAppProduct::Consumable, parent) +{ +} + +QT_END_NAMESPACE diff --git a/src/imports/inapppurchase/qinappproductconsumable_p.h b/src/imports/inapppurchase/qinappproductconsumable_p.h new file mode 100644 index 0000000..977fe8a --- /dev/null +++ b/src/imports/inapppurchase/qinappproductconsumable_p.h @@ -0,0 +1,39 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Mobile Extras Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QINAPPPRODUCTCONSUMABLE_P_H +#define QINAPPPRODUCTCONSUMABLE_P_H + +#include "qinappproductqmltype_p.h" + +QT_BEGIN_NAMESPACE + +class QInAppStore; +class QInAppTransaction; +class QInAppProductConsumable : public QInAppProductQmlType +{ + Q_OBJECT +public: + explicit QInAppProductConsumable(QObject *parent = 0); +}; + +QT_END_NAMESPACE + +#endif // QINAPPPRODUCTCONSUMABLE_P_H diff --git a/src/imports/inapppurchase/qinappproductqmltype.cpp b/src/imports/inapppurchase/qinappproductqmltype.cpp new file mode 100644 index 0000000..566e9e9 --- /dev/null +++ b/src/imports/inapppurchase/qinappproductqmltype.cpp @@ -0,0 +1,177 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Mobile Extras Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qinappproductqmltype_p.h" +#include "qinappstoreqmltype_p.h" +#include <QtMobileExtras/qinapptransaction.h> +#include <QtMobileExtras/qinappstore.h> +#include <QtCore/qcoreevent.h> + +QT_BEGIN_NAMESPACE + +QInAppProductQmlType::QInAppProductQmlType(QInAppProduct::ProductType requiredType, QObject *parent) + : QObject(parent) + , m_status(Uninitialized) + , m_requiredType(requiredType) + , m_componentComplete(false) + , m_store(0) + , m_product(0) +{ +} + +void QInAppProductQmlType::setStore(QInAppStore *store) +{ + if (m_store == store && m_store != 0) + return; + + if (m_store != 0) + m_store->disconnect(this); + + m_store = store; + if (m_store == 0) { + qWarning("Parent of products should be a Store instance."); + } else { + connect(m_store, SIGNAL(productRegistered(QInAppProduct*)), + this, SLOT(handleProductRegistered(QInAppProduct *))); + connect(m_store, SIGNAL(productUnknown(QInAppProduct::ProductType,QString)), + this, SLOT(handleProductUnknown(QInAppProduct::ProductType,QString))); + connect(m_store, SIGNAL(transactionReady(QInAppTransaction*)), + this, SLOT(handleTransaction(QInAppTransaction*))); + } + + updateProduct(); +} + +void QInAppProductQmlType::componentComplete() +{ + if (!m_componentComplete) { + m_componentComplete = true; + updateProduct(); + } +} + +void QInAppProductQmlType::setIdentifier(const QString &identifier) +{ + if (m_identifier == identifier || !m_componentComplete) + return; + + m_identifier = identifier; + updateProduct(); + emit identifierChanged(); +} + +void QInAppProductQmlType::updateProduct() +{ + qDebug("store == %p", m_store); + if (m_store == 0) + return; + + QInAppProduct *product = m_store->registeredProduct(m_identifier); + qDebug("product == %p vs. %p", product, m_product); + if (product != 0 && product == m_product) + return; + + Status oldStatus = m_status; + if (product == 0) { + m_status = PendingRegistration; + m_store->registerProduct(m_requiredType, m_identifier); + } else if (product->productType() != m_requiredType) { + product = 0; + m_status = Unknown; + } else { + m_status = Registered; + } + + setProduct(product); + if (oldStatus != m_status) + emit statusChanged(); +} + +QString QInAppProductQmlType::identifier() const +{ + return m_identifier; +} + +QInAppProductQmlType::Status QInAppProductQmlType::status() const +{ + return m_status; +} + +QString QInAppProductQmlType::price() const +{ + return m_product != 0 ? m_product->price() : QString(); +} + +void QInAppProductQmlType::setProduct(QInAppProduct *product) +{ + if (m_product == product) + return; + + QString oldPrice = price(); + m_product = product; + if (price() != oldPrice) + emit priceChanged(); +} + +void QInAppProductQmlType::handleProductRegistered(QInAppProduct *product) +{ + if (product->identifier() == m_identifier && product->productType() == m_requiredType) { + setProduct(product); + if (m_status != Registered) { + m_status = Registered; + emit statusChanged(); + } + } else if (product->identifier() == m_identifier) { + setProduct(0); + if (m_status != Unknown) { + m_status = Unknown; + emit statusChanged(); + } + } +} + +void QInAppProductQmlType::handleProductUnknown(QInAppProduct::ProductType, const QString &identifier) +{ + if (identifier == m_identifier) { + setProduct(0); + if (m_status != Unknown) { + m_status = Unknown; + emit statusChanged(); + } + } +} + +void QInAppProductQmlType::handleTransaction(QInAppTransaction *transaction) +{ + if (transaction->status() == QInAppTransaction::PurchaseApproved) + emit purchaseSucceeded(transaction); + else + emit purchaseFailed(transaction); +} + +void QInAppProductQmlType::purchase() +{ + if (m_product != 0) + m_product->purchase(); + else + qWarning("Attempted to purchase unregistered product"); +} + +QT_END_NAMESPACE diff --git a/src/imports/inapppurchase/qinappproductqmltype_p.h b/src/imports/inapppurchase/qinappproductqmltype_p.h new file mode 100644 index 0000000..1f6ae3e --- /dev/null +++ b/src/imports/inapppurchase/qinappproductqmltype_p.h @@ -0,0 +1,91 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Mobile Extras Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QINAPPPRODUCTQMLTYPE_P_H +#define QINAPPPRODUCTQMLTYPE_P_H + +#include <QtMobileExtras/qinappproduct.h> +#include <QtQml/qqmlparserstatus.h> +#include <QtQuick/qquickitem.h> + +QT_BEGIN_NAMESPACE + +class QInAppStore; +class QInAppTransaction; +class QInAppProductQmlType : public QObject, public QQmlParserStatus +{ + Q_OBJECT + Q_ENUMS(Status) + Q_PROPERTY(QString identifier READ identifier WRITE setIdentifier NOTIFY identifierChanged) + Q_PROPERTY(QString price READ price NOTIFY priceChanged) + Q_PROPERTY(Status status READ status NOTIFY statusChanged) +public: + enum Status { + Uninitialized, + PendingRegistration, + Registered, + Unknown + }; + + explicit QInAppProductQmlType(QInAppProduct::ProductType productType, + QObject *parent = 0); + + Q_INVOKABLE void purchase(); + + void setIdentifier(const QString &identifier); + QString identifier() const; + + Status status() const; + QString price() const; + + void setStore(QInAppStore *store); + +Q_SIGNALS: + void purchaseSucceeded(QInAppTransaction *transaction); + void purchaseFailed(QInAppTransaction *transaction); + void identifierChanged(); + void statusChanged(); + void priceChanged(); + +protected: + void componentComplete(); + void classBegin() {} + +private Q_SLOTS: + void handleTransaction(QInAppTransaction *transaction); + void handleProductRegistered(QInAppProduct *product); + void handleProductUnknown(QInAppProduct::ProductType, const QString &identifier); + +private: + void setProduct(QInAppProduct *product); + void updateProduct(); + + QString m_identifier; + Status m_status; + QInAppProduct::ProductType m_requiredType; + bool m_componentComplete; + + QInAppStore *m_store; + QInAppProduct *m_product; +}; + +QT_END_NAMESPACE + +#endif // QINAPPPRODUCTQMLTYPE_P_H diff --git a/src/imports/inapppurchase/qinappproductunlockable.cpp b/src/imports/inapppurchase/qinappproductunlockable.cpp new file mode 100644 index 0000000..2d268c7 --- /dev/null +++ b/src/imports/inapppurchase/qinappproductunlockable.cpp @@ -0,0 +1,30 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Mobile Extras Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qinappproductunlockable_p.h" + +QT_BEGIN_NAMESPACE + +QInAppProductUnlockable::QInAppProductUnlockable(QObject *parent) + : QInAppProductQmlType(QInAppProduct::Unlockable, parent) +{ +} + +QT_END_NAMESPACE diff --git a/src/imports/inapppurchase/qinappproductunlockable_p.h b/src/imports/inapppurchase/qinappproductunlockable_p.h new file mode 100644 index 0000000..715c5a1 --- /dev/null +++ b/src/imports/inapppurchase/qinappproductunlockable_p.h @@ -0,0 +1,33 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Mobile Extras Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QINAPPPRODUCTUNLOCKABLE_P_H +#define QINAPPPRODUCTUNLOCKABLE_P_H + +#include "qinappproductqmltype_p.h" + +class QInAppProductUnlockable : public QInAppProductQmlType +{ + Q_OBJECT +public: + explicit QInAppProductUnlockable(QObject *parent = 0); +}; + +#endif // QINAPPPRODUCTUNLOCKABLE_P_H diff --git a/src/imports/inapppurchase/qinappstoreqmltype.cpp b/src/imports/inapppurchase/qinappstoreqmltype.cpp new file mode 100644 index 0000000..0d3d917 --- /dev/null +++ b/src/imports/inapppurchase/qinappstoreqmltype.cpp @@ -0,0 +1,76 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Mobile Extras Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qinappstoreqmltype_p.h" +#include <QtMobileExtras/qinappstore.h> + +static void addProduct(QQmlListProperty<QInAppProductQmlType> *property, QInAppProductQmlType *product) +{ + QInAppStoreQmlType *store = qobject_cast<QInAppStoreQmlType *>(property->object); + Q_ASSERT(store != 0); + product->setStore(store->store()); + + QList<QInAppProductQmlType *> *m_products = reinterpret_cast<QList<QInAppProductQmlType *> *>(property->data); + Q_ASSERT(m_products != 0); + + m_products->append(product); +} + +static int productCount(QQmlListProperty<QInAppProductQmlType> *property) +{ + QList<QInAppProductQmlType *> *m_products = reinterpret_cast<QList<QInAppProductQmlType *> *>(property->data); + Q_ASSERT(m_products != 0); + + return m_products->size(); +} + +static void clearProducts(QQmlListProperty<QInAppProductQmlType> *property) +{ + QList<QInAppProductQmlType *> *m_products = reinterpret_cast<QList<QInAppProductQmlType *> *>(property->data); + Q_ASSERT(m_products != 0); + + foreach (QInAppProductQmlType *product, m_products) + product->setStore(0); + m_products->clear(); +} + +static QInAppProductQmlType *productAt(QQmlListProperty<QInAppProductQmlType> *property, int index) +{ + QList<QInAppProductQmlType *> *m_products = reinterpret_cast<QList<QInAppProductQmlType *> *>(property->data); + Q_ASSERT(m_products != 0); + + return m_products->at(index); +} + +QInAppStoreQmlType::QInAppStoreQmlType(QObject *parent) + : QObject(parent) + , m_store(new QInAppStore(this)) +{ +} + +QInAppStore *QInAppStoreQmlType::store() const +{ + return m_store; +} + +QQmlListProperty<QInAppProductQmlType> QInAppStoreQmlType::products() +{ + return QQmlListProperty<QInAppProductQmlType>(this, &m_products, &addProduct, &productCount, &productAt, &clearProducts); +} diff --git a/src/imports/inapppurchase/qinappstoreqmltype_p.h b/src/imports/inapppurchase/qinappstoreqmltype_p.h new file mode 100644 index 0000000..746938c --- /dev/null +++ b/src/imports/inapppurchase/qinappstoreqmltype_p.h @@ -0,0 +1,48 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Mobile Extras Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QINAPPSTOREQMLTYPE_P_H +#define QINAPPSTOREQMLTYPE_P_H + +#include "qinappproductqmltype_p.h" +#include <QtQuick/qquickitem.h> + +QT_BEGIN_NAMESPACE + +class QInAppStore; +class QInAppStoreQmlType : public QObject +{ + Q_OBJECT + Q_PROPERTY(QQmlListProperty<QInAppProductQmlType> products READ products DESIGNABLE false) + Q_CLASSINFO("DefaultProperty", "products") +public: + explicit QInAppStoreQmlType(QObject *parent = 0); + + QInAppStore *store() const; + QQmlListProperty<QInAppProductQmlType> products(); + +private: + QInAppStore *m_store; + QList<QInAppProductQmlType *> m_products; +}; + +QT_END_NAMESPACE + +#endif // QINAPPSTOREQMLTYPE_P_H diff --git a/src/imports/inapppurchase/qmldir b/src/imports/inapppurchase/qmldir new file mode 100644 index 0000000..2b0c271 --- /dev/null +++ b/src/imports/inapppurchase/qmldir @@ -0,0 +1,4 @@ +module QtMobileExtras +plugin declarative_mobileextras +typeinfo plugins.qmltypes +classname QInAppPurchaseModule diff --git a/src/mobileextras/inapppurchase/android/android.pri b/src/mobileextras/inapppurchase/android/android.pri new file mode 100644 index 0000000..31ac238 --- /dev/null +++ b/src/mobileextras/inapppurchase/android/android.pri @@ -0,0 +1,10 @@ +INCLUDEPATH += $$PWD +SOURCES += \ + $$PWD/qandroidinapppurchasebackend.cpp \ + $$PWD/qandroidjni.cpp \ + $$PWD/qandroidinapptransaction.cpp \ + $$PWD/qandroidinappproduct.cpp +HEADERS += \ + $$PWD/qandroidinapppurchasebackend_p.h \ + $$PWD/qandroidinapptransaction_p.h \ + $$PWD/qandroidinappproduct_p.h diff --git a/src/mobileextras/inapppurchase/android/qandroidinappproduct.cpp b/src/mobileextras/inapppurchase/android/qandroidinappproduct.cpp new file mode 100644 index 0000000..116c1d9 --- /dev/null +++ b/src/mobileextras/inapppurchase/android/qandroidinappproduct.cpp @@ -0,0 +1,40 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Mobile Extras Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qandroidinappproduct_p.h" + +QT_BEGIN_NAMESPACE + +QAndroidInAppProduct::QAndroidInAppProduct(const QString &price, + ProductType productType, + const QString &identifier, + QObject *parent) + : QInAppProduct(price, productType, identifier, parent) +{ +} + + +void QAndroidInAppProduct::purchase() +{ +#warning Unimplemented +} + +QT_END_NAMESPACE + diff --git a/src/mobileextras/inapppurchase/android/qandroidinappproduct_p.h b/src/mobileextras/inapppurchase/android/qandroidinappproduct_p.h new file mode 100644 index 0000000..e107e07 --- /dev/null +++ b/src/mobileextras/inapppurchase/android/qandroidinappproduct_p.h @@ -0,0 +1,54 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Mobile Extras Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QANDROIDINAPPPRODUCT_P_H +#define QANDROIDINAPPPRODUCT_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qinappproduct.h" + +QT_BEGIN_NAMESPACE + +class QAndroidInAppProduct : public QInAppProduct +{ + Q_OBJECT +public: + explicit QAndroidInAppProduct(const QString &price, + ProductType productType, + const QString &identifier, + QObject *parent = 0); + + + void purchase(); +}; + +QT_END_NAMESPACE + +#endif // QANDROIDINAPPPRODUCT_P_H diff --git a/src/mobileextras/inapppurchase/android/qandroidinapppurchasebackend.cpp b/src/mobileextras/inapppurchase/android/qandroidinapppurchasebackend.cpp new file mode 100644 index 0000000..4f11b17 --- /dev/null +++ b/src/mobileextras/inapppurchase/android/qandroidinapppurchasebackend.cpp @@ -0,0 +1,163 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Mobile Extras Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qandroidinapppurchasebackend_p.h" +#include "qandroidinappproduct_p.h" +#include "qandroidinapptransaction_p.h" + +#include <QtAndroidExtras/qandroidfunctions.h> + +QT_BEGIN_NAMESPACE + +QAndroidInAppPurchaseBackend::QAndroidInAppPurchaseBackend(QObject *parent) + : QInAppPurchaseBackend(parent) + , m_mutex(QMutex::Recursive) + , m_isReady(false) +{ + m_javaObject = QAndroidJniObject("com/digia/qt5/android/mobileextras/QtInAppPurchase", + "(Landroid/content/Context;J)V", + QtAndroid::androidActivity().object<jobject>(), + reinterpret_cast<jlong>(this)); + if (!m_javaObject.isValid()) { + qWarning("Cannot initialize IAP backend for Android due to missing dependency: QtInAppPurchase class"); + return; + } +} + +void QAndroidInAppPurchaseBackend::initialize() +{ + m_javaObject.callMethod<void>("initializeConnection"); +#warning Read tokens here +} + +bool QAndroidInAppPurchaseBackend::isReady() const +{ + QMutexLocker locker(&m_mutex); + return m_isReady; +} + +void QAndroidInAppPurchaseBackend::restorePurchases() +{ + // ### Go through existing purchases, remove finalization token and emit transactions +#warning Unimplemented +} + +void QAndroidInAppPurchaseBackend::queryProduct(QInAppProduct::ProductType productType, + const QString &identifier) +{ + QMutexLocker locker(&m_mutex); + if (m_productTypeForPendingId.contains(identifier)) { + qWarning("Product query already pending for %s", qPrintable(identifier)); + return; + } + + m_productTypeForPendingId[identifier] = productType; + m_javaObject.callMethod<void>("queryDetails", + "(Ljava/lang/String;)V", + QAndroidJniObject::fromString(identifier).object<jstring>()); +} + +void QAndroidInAppPurchaseBackend::setPlatformProperty(const QString &propertyName, const QString &value) +{ + if (propertyName.compare(QStringLiteral("AndroidPublicKey"), Qt::CaseInsensitive) == 0) { + m_javaObject.callMethod<void>("setPublicKey", + "(Ljava/lang/String;)", + QAndroidJniObject::fromString(value).object<jstring>()); + } +} + +void QAndroidInAppPurchaseBackend::registerQueryFailure(const QString &productId) +{ + QMutexLocker locker(&m_mutex); + QHash<QString, QInAppProduct::ProductType>::iterator it = m_productTypeForPendingId.find(productId); + Q_ASSERT(it != m_productTypeForPendingId.end()); + + emit productQueryFailed(it.value(), it.key()); + m_productTypeForPendingId.erase(it); +} + +bool QAndroidInAppPurchaseBackend::transactionFinalizedForProduct(QInAppProduct *product) +{ + Q_ASSERT(m_signatureAndDataForPurchase.contains(product->identifier())); + return product->productType() == QInAppProduct::Consumable + || m_finalizedUnlockableProducts.contains(product->identifier()); +} + +void QAndroidInAppPurchaseBackend::createTransactionForProduct(QInAppTransaction::TransactionStatus status, + QInAppProduct *product, + const QPair<QString, QString> &signatureAndData) +{ + QAndroidInAppTransaction *transaction = new QAndroidInAppTransaction(signatureAndData.first, + signatureAndData.second, + status, + product, + this); + emit transactionReady(transaction); +} + +void QAndroidInAppPurchaseBackend::checkFinalizationStatus(QInAppProduct *product) +{ + // Verifies the finalization status of an item based on the following logic: + // 1. If the item is not purchased yet, do nothing (it's either never been purchased, or it's a + // consumed consumable. + // 2. If the item is purchased, and it's a consumable, it's unfinalized. Emit a new transaction. + // Consumable items are consumed when they are finalized. + // 3. If the item is purchased, and it's an unlockable, check the local cache for finalized + // unlockable purchases. If it's not there, then the transaction is unfinalized. This means + // that if the cache gets deleted or corrupted, the worst-case scenario is that the transactions + // are republished. + QHash<QString, QPair<QString, QString> >::iterator it = m_signatureAndDataForPurchase.find(product->identifier()); + if (it == m_signatureAndDataForPurchase.end()) + return; + + if (!transactionFinalizedForProduct(product)) + createTransactionForProduct(QInAppTransaction::PurchaseApproved, product, it.value()); + + m_signatureAndDataForPurchase.erase(it); +} + +void QAndroidInAppPurchaseBackend::registerProduct(const QString &productId, const QString &price) +{ + QMutexLocker locker(&m_mutex); + QHash<QString, QInAppProduct::ProductType>::iterator it = m_productTypeForPendingId.find(productId); + Q_ASSERT(it != m_productTypeForPendingId.end()); + + QAndroidInAppProduct *product = new QAndroidInAppProduct(price, it.value(), it.key(), this); + checkFinalizationStatus(product); + + emit productQueryDone(product); + m_productTypeForPendingId.erase(it); +} + +void QAndroidInAppPurchaseBackend::registerPurchased(const QString &productId, const QString &signature, const QString &data) +{ + QMutexLocker locker(&m_mutex); + m_signatureAndDataForPurchase.insert(productId, qMakePair(signature, data)); +} + + +void QAndroidInAppPurchaseBackend::registerReady() +{ + QMutexLocker locker(&m_mutex); + m_isReady = true; + emit ready(); +} + +QT_END_NAMESPACE diff --git a/src/mobileextras/inapppurchase/android/qandroidinapppurchasebackend_p.h b/src/mobileextras/inapppurchase/android/qandroidinapppurchasebackend_p.h new file mode 100644 index 0000000..9422e2c --- /dev/null +++ b/src/mobileextras/inapppurchase/android/qandroidinapppurchasebackend_p.h @@ -0,0 +1,83 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Mobile Extras Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + + +#ifndef QANDROIDINAPPPURCHASEBACKEND_P_H +#define QANDROIDINAPPPURCHASEBACKEND_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qinapppurchasebackend_p.h" +#include "qinappproduct.h" +#include "qinapptransaction.h" + +#include <QtCore/qmutex.h> +#include <QtCore/qset.h> +#include <QtAndroidExtras/qandroidjniobject.h> + +QT_BEGIN_NAMESPACE + +class QAndroidInAppPurchaseBackend : public QInAppPurchaseBackend +{ + Q_OBJECT +public: + explicit QAndroidInAppPurchaseBackend(QObject *parent = 0); + + void initialize(); + bool isReady() const; + + void queryProduct(QInAppProduct::ProductType productType, const QString &identifier); + void restorePurchases(); + + void setPlatformProperty(const QString &propertyName, const QString &value); + + // Callbacks from Java + void registerQueryFailure(const QString &productId); + void registerProduct(const QString &productId, const QString &price); + void registerPurchased(const QString &productId, const QString &signature, const QString &data); + void registerReady(); + +private: + void checkFinalizationStatus(QInAppProduct *product); + void createTransactionForProduct(QInAppTransaction::TransactionStatus status, + QInAppProduct *product, + const QPair<QString, QString> &signature); + bool transactionFinalizedForProduct(QInAppProduct *product); + + mutable QMutex m_mutex; + bool m_isReady; + QAndroidJniObject m_javaObject; + QHash<QString, QInAppProduct::ProductType> m_productTypeForPendingId; + QHash<QString, QPair<QString, QString> > m_signatureAndDataForPurchase; + QSet<QString> m_finalizedUnlockableProducts; +}; + +QT_END_NAMESPACE + +#endif // QANDROIDINAPPPURCHASEBACKEND_P_H diff --git a/src/mobileextras/inapppurchase/android/qandroidinapptransaction.cpp b/src/mobileextras/inapppurchase/android/qandroidinapptransaction.cpp new file mode 100644 index 0000000..5e5ba20 --- /dev/null +++ b/src/mobileextras/inapppurchase/android/qandroidinapptransaction.cpp @@ -0,0 +1,52 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Mobile Extras Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qandroidinapptransaction_p.h" + +QT_BEGIN_NAMESPACE + +QAndroidInAppTransaction::QAndroidInAppTransaction(const QString &signature, + const QString &data, + TransactionStatus status, + QInAppProduct *product, + QObject *parent) + : QInAppTransaction(status, product, parent) + , m_signature(signature) + , m_data(data) +{ +} + +QString QAndroidInAppTransaction::platformProperty(const QString &propertyName) const +{ + if (propertyName.compare(QStringLiteral("AndroidSignature"), Qt::CaseInsensitive) == 0) + return m_signature; + else if (propertyName.compare(QStringLiteral("AndroidPurchaseData"), Qt::CaseInsensitive) == 0) + return m_data; + else + return QInAppTransaction::platformProperty(propertyName); +} + +void QAndroidInAppTransaction::finalize() +{ + // ### consume consumable or store finalized data for unlockable +#warning Unimplemented +} + +QT_END_NAMESPACE diff --git a/src/mobileextras/inapppurchase/android/qandroidinapptransaction_p.h b/src/mobileextras/inapppurchase/android/qandroidinapptransaction_p.h new file mode 100644 index 0000000..f2dcfd8 --- /dev/null +++ b/src/mobileextras/inapppurchase/android/qandroidinapptransaction_p.h @@ -0,0 +1,62 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Mobile Extras Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + + +#ifndef QANDROIDINAPPTRANSACTION_P_H +#define QANDROIDINAPPTRANSACTION_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/qobject.h> +#include "qinapptransaction.h" + +QT_BEGIN_NAMESPACE + +class QAndroidInAppTransaction : public QInAppTransaction +{ + Q_OBJECT +public: + explicit QAndroidInAppTransaction(const QString &signature, + const QString &data, + TransactionStatus status, + QInAppProduct *product, + QObject *parent = 0); + + void finalize(); + QString platformProperty(const QString &propertyName) const; + +private: + QString m_signature; + QString m_data; + +}; + +QT_END_NAMESPACE + +#endif // QANDROIDINAPPTRANSACTION_P_H diff --git a/src/mobileextras/inapppurchase/android/qandroidjni.cpp b/src/mobileextras/inapppurchase/android/qandroidjni.cpp new file mode 100644 index 0000000..2bb2ebd --- /dev/null +++ b/src/mobileextras/inapppurchase/android/qandroidjni.cpp @@ -0,0 +1,74 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Mobile Extras Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qandroidinapppurchasebackend_p.h" + +#include <QtAndroidExtras/qandroidjniobject.h> +#include <jni.h> + +QT_USE_NAMESPACE + +static void queryFailed(jclass, jlong nativePointer, jstring productId) +{ + QAndroidInAppPurchaseBackend *backend = reinterpret_cast<QAndroidInAppPurchaseBackend *>(nativePointer); + backend->registerQueryFailure(QAndroidJniObject(productId).toString()); +} + +static void purchasedProductsQueried(jclass, jlong nativePointer) +{ + QAndroidInAppPurchaseBackend *backend = reinterpret_cast<QAndroidInAppPurchaseBackend *>(nativePointer); + backend->registerReady(); +} + +static void registerProduct(jclass, jlong nativePointer, jstring productId, jstring price) +{ + QAndroidInAppPurchaseBackend *backend = reinterpret_cast<QAndroidInAppPurchaseBackend *>(nativePointer); + backend->registerProduct(QAndroidJniObject(productId).toString(), + QAndroidJniObject(price).toString()); +} + +static void registerPurchased(jclass, jlong nativePointer, jstring productId, jstring signature, jstring data) +{ + QAndroidInAppPurchaseBackend *backend = reinterpret_cast<QAndroidInAppPurchaseBackend *>(nativePointer); + backend->registerPurchased(QAndroidJniObject(productId).toString(), + QAndroidJniObject(signature).toString(), + QAndroidJniObject(data).toString()); + +} + +static JNINativeMethod methods[] = { + {"queryFailed", "(JLjava/lang/String;)V", (void *)queryFailed}, + {"purchasedProductsQueried", "(J)V", (void *)purchasedProductsQueried}, + {"registerProduct", "(JLjava/lang/String;Ljava/lang/String;)V", (void *)registerProduct}, + {"registerPurchased", "(JLjava/lang/String;Ljava/lang/String;Ljava/lang/String;)V", (void *)registerPurchased} +}; + +jint JNICALL JNI_OnLoad(JavaVM *vm, void *) +{ + JNIEnv *env; + if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_4) != JNI_OK) + return JNI_FALSE; + + jclass clazz = env->FindClass("com/digia/qt5/android/mobileextras/QtInAppPurchase"); + if (env->RegisterNatives(clazz, methods, sizeof(methods) / sizeof(methods[0])) < 0) + return JNI_FALSE; + + return JNI_VERSION_1_4; +} diff --git a/src/mobileextras/inapppurchase/inapppurchase.pri b/src/mobileextras/inapppurchase/inapppurchase.pri new file mode 100644 index 0000000..abaa22d --- /dev/null +++ b/src/mobileextras/inapppurchase/inapppurchase.pri @@ -0,0 +1,24 @@ +INCLUDEPATH += $$PWD + +ANDROID_PERMISSIONS += \ + com.android.vending.BILLING + +HEADERS += \ + $$PWD/qinappstore.h \ + $$PWD/qinappproduct.h \ + $$PWD/qinapptransaction.h \ + $$PWD/qinappstore_p.h \ + $$PWD/qinapppurchasebackend_p.h \ + $$PWD/qinapppurchasebackendfactory_p.h + +SOURCES += \ + $$PWD/qinappproduct.cpp \ + $$PWD/qinapptransaction.cpp \ + $$PWD/qinappstore.cpp \ + $$PWD/qinapppurchasebackend.cpp \ + $$PWD/qinapppurchasebackendfactory.cpp + +android { + QT += androidextras + include ($$PWD/android/android.pri) +} diff --git a/src/mobileextras/inapppurchase/qinappproduct.cpp b/src/mobileextras/inapppurchase/qinappproduct.cpp new file mode 100644 index 0000000..91b7074 --- /dev/null +++ b/src/mobileextras/inapppurchase/qinappproduct.cpp @@ -0,0 +1,106 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Mobile Extras Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qinappproduct.h" + +QT_BEGIN_NAMESPACE + +struct QInAppProductPrivate +{ + QInAppProductPrivate(const QString &price, QInAppProduct::ProductType type, const QString &id) + : localPrice(price) + , productType(type) + , identifier(id) + { + } + + QString localPrice; + QInAppProduct::ProductType productType; + QString identifier; +}; + +/*! + * \class QInAppProduct + * \brief A product registered in the store + * + * QInAppProduct encapsulates a product in the external store after it has been registered in \c QInAppStore + * and confirmed to exist. It has an identifier which matches the identifier of the product in the external + * store, it has a price which is retrieved from the external store, and it has a product type. + * + * The product type can be either \c Consumable or \c Unlockable. The former type of products can be purchased + * any number of times as long as each transaction is finalized explicitly by the application. The latter type + * can only be purchased once. + */ + +/*! + * \internal + */\ +QInAppProduct::QInAppProduct(const QString &price, ProductType productType, const QString &identifier, QObject *parent) + : QObject(parent) +{ + d = QSharedPointer<QInAppProductPrivate>(new QInAppProductPrivate(price, productType, identifier)); +} + +/*! + * \internal + */\ +QInAppProduct::~QInAppProduct() +{ +} + +/*! + * Returns the price of the product as reported by the external store. This is usually the price in the + * locale of the current user. + */ +QString QInAppProduct::price() const +{ + return d->localPrice; +} + +/*! + * Returns the identifier of the product. This matches the identifier of the product which is registered + * in the external store. + */ +QString QInAppProduct::identifier() const +{ + return d->identifier; +} + +/*! + * Returns the type of the product. This can either be \c Consumable or \c Unlockable. The former are products + * which can be purchased any number of times (granted that each transaction is explicitly finalized by the + * application first) and the latter are products which can only be purchased once per user. + */ +QInAppProduct::ProductType QInAppProduct::productType() const +{ + return d->productType; +} + +/*! + * \fn void QInAppProduct::purchase() + * + * Launches the purchase flow for this product. The purchase is done asynchronously. When the purchase has + * either been completed successfully or failed for some reason, the QInAppStore instance containing + * this product will emit a QInAppStore::transactionReady() signal with information about the transaction. + * + * \sa QInAppTransaction + */ + +QT_END_NAMESPACE diff --git a/src/mobileextras/inapppurchase/qinappproduct.h b/src/mobileextras/inapppurchase/qinappproduct.h new file mode 100644 index 0000000..8593a0a --- /dev/null +++ b/src/mobileextras/inapppurchase/qinappproduct.h @@ -0,0 +1,67 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Mobile Extras Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QINAPPPRODUCT_H +#define QINAPPPRODUCT_H + +#include <QtCore/qobject.h> +#include <QtCore/qsharedpointer.h> +#include <QtMobileExtras/qmobileextrasglobal.h> + +QT_BEGIN_NAMESPACE + +class QInAppProductPrivate; +class Q_MOBILEEXTRAS_EXPORT QInAppProduct: public QObject +{ + Q_OBJECT + Q_ENUMS(ProductType) + Q_PROPERTY(QString identifier READ identifier CONSTANT) + Q_PROPERTY(ProductType productType READ productType CONSTANT) + Q_PROPERTY(QString price READ price CONSTANT) + +public: + enum ProductType + { + Consumable, + Unlockable + }; + + ~QInAppProduct(); + + QString identifier() const; + ProductType productType() const; + + QString price() const; + + Q_INVOKABLE virtual void purchase() = 0; + +protected: + explicit QInAppProduct(const QString &price, ProductType productType, const QString &identifier, QObject *parent = 0); + +private: + friend class QInAppStore; + Q_DISABLE_COPY(QInAppProduct) + + QSharedPointer<QInAppProductPrivate> d; +}; + +QT_END_NAMESPACE + +#endif // QINAPPPRODUCT_H diff --git a/src/mobileextras/inapppurchase/qinapppurchasebackend.cpp b/src/mobileextras/inapppurchase/qinapppurchasebackend.cpp new file mode 100644 index 0000000..cbd7751 --- /dev/null +++ b/src/mobileextras/inapppurchase/qinapppurchasebackend.cpp @@ -0,0 +1,59 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Mobile Extras Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qinapppurchasebackend_p.h" + +QT_BEGIN_NAMESPACE + +QInAppPurchaseBackend::QInAppPurchaseBackend(QObject *parent) + : QObject(parent) +{ +} + +void QInAppPurchaseBackend::initialize() +{ + emit ready(); +} + +bool QInAppPurchaseBackend::isReady() const +{ + return true; +} + +void QInAppPurchaseBackend::queryProduct(QInAppProduct::ProductType productType, + const QString &identifier) +{ + qWarning("QInAppPurchaseBackend not implemented on this platform!"); + Q_UNUSED(productType); + Q_UNUSED(identifier); +} + +void QInAppPurchaseBackend::restorePurchases() +{ + qWarning("QInAppPurchaseBackend not implemented on this platform!"); +} + +void QInAppPurchaseBackend::setPlatformProperty(const QString &propertyName, const QString &value) +{ + Q_UNUSED(propertyName); + Q_UNUSED(value); +} + +QT_END_NAMESPACE diff --git a/src/mobileextras/inapppurchase/qinapppurchasebackend_p.h b/src/mobileextras/inapppurchase/qinapppurchasebackend_p.h new file mode 100644 index 0000000..a35145a --- /dev/null +++ b/src/mobileextras/inapppurchase/qinapppurchasebackend_p.h @@ -0,0 +1,65 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Mobile Extras Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QINAPPPURCHASEBACKEND_P_H +#define QINAPPPURCHASEBACKEND_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qinappproduct.h" +#include <QtCore/qobject.h> + +QT_BEGIN_NAMESPACE + +class QInAppProduct; +class QInAppTransaction; +class QInAppPurchaseBackend : public QObject +{ + Q_OBJECT +public: + explicit QInAppPurchaseBackend(QObject *parent = 0); + + virtual void initialize(); + virtual bool isReady() const; + + virtual void queryProduct(QInAppProduct::ProductType productType, const QString &identifier); + virtual void restorePurchases(); + + virtual void setPlatformProperty(const QString &propertyName, const QString &value); + +Q_SIGNALS: + void ready(); + void transactionReady(QInAppTransaction *transaction); + void productQueryFailed(QInAppProduct::ProductType productType, const QString &identifier); + void productQueryDone(QInAppProduct *product); +}; + +QT_END_NAMESPACE + +#endif // QINAPPPURCHASEBACKEND_P_H diff --git a/src/mobileextras/inapppurchase/qinapppurchasebackendfactory.cpp b/src/mobileextras/inapppurchase/qinapppurchasebackendfactory.cpp new file mode 100644 index 0000000..aed17e9 --- /dev/null +++ b/src/mobileextras/inapppurchase/qinapppurchasebackendfactory.cpp @@ -0,0 +1,40 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Mobile Extras Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qinapppurchasebackendfactory_p.h" + +#if defined(Q_OS_ANDROID) +# include "qandroidinapppurchasebackend_p.h" +#else +# include "qinapppurchasebackend_p.h" +#endif + +QT_BEGIN_NAMESPACE + +QInAppPurchaseBackend *QInAppPurchaseBackendFactory::create() +{ +#if defined(Q_OS_ANDROID) + return new QAndroidInAppPurchaseBackend; +#else + return new QInAppPurchaseBackend; +#endif +} + +QT_END_NAMESPACE diff --git a/src/mobileextras/inapppurchase/qinapppurchasebackendfactory_p.h b/src/mobileextras/inapppurchase/qinapppurchasebackendfactory_p.h new file mode 100644 index 0000000..722dd79 --- /dev/null +++ b/src/mobileextras/inapppurchase/qinapppurchasebackendfactory_p.h @@ -0,0 +1,48 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Mobile Extras Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QINAPPPURCHASEBACKENDFACTORY_P_H +#define QINAPPPURCHASEBACKENDFACTORY_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/qglobal.h> + +QT_BEGIN_NAMESPACE + +class QInAppPurchaseBackend; +class QInAppPurchaseBackendFactory +{ +public: + static QInAppPurchaseBackend *create(); +}; + +QT_END_NAMESPACE + +#endif // QINAPPPURCHASEBACKENDFACTORY_P_H diff --git a/src/mobileextras/inapppurchase/qinappstore.cpp b/src/mobileextras/inapppurchase/qinappstore.cpp new file mode 100644 index 0000000..a1f56ae --- /dev/null +++ b/src/mobileextras/inapppurchase/qinappstore.cpp @@ -0,0 +1,268 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Mobile Extras Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qinappstore.h" +#include "qinappstore_p.h" +#include "qinapppurchasebackend_p.h" +#include "qinapppurchasebackendfactory_p.h" + +QT_BEGIN_NAMESPACE + +/*! + \class QInAppStore + \brief The main entry point for managing in-app purchases + + QInAppStore is used for managing in-app purchases in your application in a + cross-platform way. + + \section1 Using the QInAppStore + In general there are two steps to completing an in-app purchase using the + API: + + \section2 Initialize the store + Upon start-up of your application, connect all signals in QInAppStore to + related slots in your own QObject. Then use the registerProduct() function + to register the ID of each product you expect to find registered in the + external store, as well as its type. + + Registering a product is asynchronous, and will at some point yield one of + the following two signals: + 1. productRegistered() if the product was found in the external store with + a matching type. + 2. productUnknown() if the product was not found in the external store with + the type you specified. + + In addition, a transactionReady() signal may be emitted for any existing + transaction which has not yet been finalized. At this point, you should + check if the transaction has previously been registered. If it hasn't, + register it right away. Finally, call QInAppTransaction::finalize() on + the transaction. + + \section2 Complete a purchase + Once the items have been successfully registered in the store, you can + purchase them. Get the previously registered QInAppProduct using + registeredProduct() and call QInAppProduct::purchase(). This call is + also asynchronous. + + At some point later on, the transactionReady() signal will be emitted for + the purchase. Check QInAppTransaction::status() to see if the purchase was + completed successfully. If it was, then you must save the information about + the purchase in a safe way, so that the application can restore it later. + + When you are done, call QInAppTransaction::finalize(), regardless of its + status. Transactions which are not finalized will be emitted again the next + time your application calls registerProduct() for the same product. + + \note Please mind that QInAppStore does not save the purchased + state of items in the store for you. The application should store this + information in a safe way upon receiving the transactionReady() signal, + before calling QInAppTransaction::finalize(). + + \section1 Types of purchases + There are two types of purchases supported by QInAppStore: + QInAppProduct::Consumable and QInAppProduct::Unlockable. The former will be + consumed when the transaction is completed and QInAppTransaction::finalize() + is called, meaning that it can be purchased again, any number of times. + Unlockable items can only be purchased once. + + Consumable products are temporary and can be purchased multiple times. + Examples could be a day-ticket on the bus or a magic sword in a computer game. + Note that when purchasing the same product multiple times, you should call + QInAppTransaction::finalize() on each transaction before you can purchase the + same product again. + + Unlockable products are products that a user will buy once, and the purchase + of these items will be persistent. It can typically be used for things like + unlocking content or functionality in the application. + + \section1 Restoring purchases + If your application has unlockable products, and does not store the purchase + states of these products in a way which makes it possible to restore them + when the user reinstalls the application, you should provide a way for the + user to restore the purchases manually. + + Call the restorePurchases() function to begin this process. Granted that + the remote store supports it, you will then at some point get transactionReady() + for each unlockable item which has previously been purchased by the current user. + + Save the purchase state of each product and call QInAppTransaction::finalize() as + you would for a regular purchase. + + Since restorePurchases() may, on some platforms, cause the user to be prompted for + their password, it should usually be called as a reaction to user input. For instance + applications can have a button in the UI which will trigger restorePurchases() and + which users can hit manually if they have reinstalled the application (or installed + it on a new device) and need to unlock the features that they have previously paid + for. + + \note This depends on support for this functionality in the remote store. If + the remote store does not save the purchase state of unlockable products for + you, the call will yield no transactionReady() signals, as if no products have + been purchased. Both the Android and iOS backends support restoring unlockable + products. + +*/ + +QInAppStore::QInAppStore(QObject *parent) + : QObject(parent) +{ + d = QSharedPointer<QInAppStorePrivate>(new QInAppStorePrivate); + setupBackend(); +} + +QInAppStore::~QInAppStore() +{ +} + +/*! + * \internal + */ +void QInAppStore::setupBackend() +{ + d->backend = QInAppPurchaseBackendFactory::create(); + + connect(d->backend, SIGNAL(ready()), + this, SLOT(registerPendingProducts())); + connect(d->backend, SIGNAL(transactionReady(QInAppTransaction *)), + this, SIGNAL(transactionReady(QInAppTransaction *))); + connect(d->backend, SIGNAL(productQueryFailed(QInAppProduct::ProductType,QString)), + this, SIGNAL(productUnknown(QInAppProduct::ProductType,QString))); + connect(d->backend, SIGNAL(productQueryDone(QInAppProduct *)), + this, SLOT(registerProduct(QInAppProduct*))); +} + +/*! + * \internal + */ +void QInAppStore::registerProduct(QInAppProduct *product) +{ + d->registeredProducts[product->identifier()] = product; + emit productRegistered(product); +} + +/*! + * \internal + * + * Called when the backend is finished initialized and will create products which were + * registered while the backend was still working. + */ +void QInAppStore::registerPendingProducts() +{ + QHash<QString, QInAppProduct::ProductType>::const_iterator it; + for (it = d->pendingProducts.constBegin(); it != d->pendingProducts.constEnd(); ++it) { + QString identifier = it.key(); + QInAppProduct::ProductType productType = it.value(); + d->backend->queryProduct(productType, identifier); + } + d->pendingProducts.clear(); +} + +/*! + * Requests existing purchases of unlockable items and will yield a transactionReady() + * signal for each unlockable product that the remote store confirms have previously been + * purchased by the current user. + * + * This function can typically be used for restoring unlockable products when the application + * has been reinstalled and lost the saved purchase states. + * + * \note Calling this function may prompt the user for their password on some platforms. + */ +void QInAppStore::restorePurchases() +{ + d->backend->restorePurchases(); +} + +/*! + * Sets the platform specific property given by \a propertyName to \a value. This can be used + * to pass information to the platform implementation. The properties will be silently ignored + * on other platforms. + * + * Currently, the only supported platform property is "AndroidPublicKey" which is used by the Android + * backend to verify purchases. If it is not set, purchases will be accepted with no verification. + * (You can also do the verification manually by getting the signature from the QInAppTransaction object + * for the purchase.) For more information, see + * \l{http://developer.android.com/google/play/billing/billing_integrate.html#billing-security} + * {the Android documentation for billing security}. + * + */ +void QInAppStore::setPlatformProperty(const QString &propertyName, const QString &value) +{ + d->backend->setPlatformProperty(propertyName, value); +} + +/*! + * Registers a product identified by \a identifier and with the given \a productType. + * The \a identifier must match the identifier of the product in the remote store. If + * the remote store differentiates between consumable and unlockable products, the + * \a productType must also match this. + * + * Calling this function will asynchronously yield either a productRegistered() or a + * productUnknown() signal. It may also yield a transactionReady() signal if there is + * a pending transaction for the product which has not yet been finalized. + */ +void QInAppStore::registerProduct(QInAppProduct::ProductType productType, const QString &identifier) +{ + if (!d->backend->isReady()) { + if (!d->hasCalledInitialize) { + d->hasCalledInitialize = true; + d->backend->initialize(); + } + d->pendingProducts[identifier] = productType; + } else { + d->backend->queryProduct(productType, identifier); + } +} + +/*! + * Returns the previously registered product uniquely known by the \a identifier. + */ +QInAppProduct *QInAppStore::registeredProduct(const QString &identifier) const +{ + return d->registeredProducts.value(identifier); +} + +/*! + * \fn QInAppStore::productRegistered(QInAppProduct *product) + * + * This signal is emitted when information about a \a product has been collected from the + * remote store. It is emitted as a reaction to a registerProduct() call for the same + * product. + * + * \sa productUnknown() + */ + +/*! /fn QInAppStore::productUnknown(QInAppProduct::ProductType productType, const QString &identifier) + * + * This signal is emitted when a product was registered using registerProduct() and matching + * information could not be provided by the remote store. + * + * \sa productRegistered() + */ + +/*! + * \fn QInAppStore::transactionReady(QInAppTransaction *transaction) + * + * This signal is emitted whenever there is a \a transaction which needs to be finalized. + * It is emitted either when a purchase request has been made for a product, when restorePurchases() + * has been called and the product was previously purchased, or when registerProduct() was called + * for a product and there was a pending transaction for the product which had not yet been finalized. + */ + +QT_END_NAMESPACE diff --git a/src/mobileextras/inapppurchase/qinappstore.h b/src/mobileextras/inapppurchase/qinappstore.h new file mode 100644 index 0000000..2c9fb4c --- /dev/null +++ b/src/mobileextras/inapppurchase/qinappstore.h @@ -0,0 +1,64 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Mobile Extras Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QINAPPSTORE_H +#define QINAPPSTORE_H + +#include "qinappproduct.h" + +#include <QtCore/qobject.h> +#include <QtMobileExtras/qmobileextrasglobal.h> + +QT_BEGIN_NAMESPACE + +class QInAppStorePrivate; +class QInAppProduct; +class QInAppTransaction; +class Q_MOBILEEXTRAS_EXPORT QInAppStore: public QObject +{ + Q_OBJECT +public: + QInAppStore(QObject *parent = 0); + ~QInAppStore(); + + Q_INVOKABLE void restorePurchases(); + Q_INVOKABLE void registerProduct(QInAppProduct::ProductType productType, const QString &identifier); + Q_INVOKABLE QInAppProduct *registeredProduct(const QString &identifier) const; + Q_INVOKABLE void setPlatformProperty(const QString &propertyName, const QString &value); + +Q_SIGNALS: + void productRegistered(QInAppProduct *product); + void productUnknown(QInAppProduct::ProductType productType, const QString &identifier); + void transactionReady(QInAppTransaction *transaction); + +private Q_SLOTS: + void registerPendingProducts(); + void registerProduct(QInAppProduct *); + +private: + void setupBackend(); + + Q_DISABLE_COPY(QInAppStore) + QSharedPointer<QInAppStorePrivate> d; +}; + +QT_END_NAMESPACE + +#endif // QINAPPSTORE_H diff --git a/src/mobileextras/inapppurchase/qinappstore_p.h b/src/mobileextras/inapppurchase/qinappstore_p.h new file mode 100644 index 0000000..5dd8c6e --- /dev/null +++ b/src/mobileextras/inapppurchase/qinappstore_p.h @@ -0,0 +1,60 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Mobile Extras Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QINAPPSTORE_P_H +#define QINAPPSTORE_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qinappproduct.h" +#include <QtCore/qmutex.h> + +QT_BEGIN_NAMESPACE + +class QInAppProduct; +class QInAppPurchaseBackend; + +class QInAppStorePrivate +{ +public: + QInAppStorePrivate() + : backend(0) + , hasCalledInitialize(false) + { + } + + QHash<QString, QInAppProduct::ProductType> pendingProducts; + QHash<QString, QInAppProduct *> registeredProducts; + QInAppPurchaseBackend *backend; + bool hasCalledInitialize; +}; + +QT_END_NAMESPACE + +#endif // QINAPPSTORE_P_H diff --git a/src/mobileextras/inapppurchase/qinapptransaction.cpp b/src/mobileextras/inapppurchase/qinapptransaction.cpp new file mode 100644 index 0000000..ce4d124 --- /dev/null +++ b/src/mobileextras/inapppurchase/qinapptransaction.cpp @@ -0,0 +1,130 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Mobile Extras Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qinapptransaction.h" + +QT_BEGIN_NAMESPACE + +struct QInAppTransactionPrivate +{ + QInAppTransactionPrivate(QInAppTransaction::TransactionStatus s, + QInAppProduct *p) + : status(s) + , product(p) + { + } + + QInAppTransaction::TransactionStatus status; + QInAppProduct *product; +}; + +/*! + * \class QInAppTransaction + * \brief Contains information about a transaction in the external app store + * + * QInAppTransaction contains information about a transaction in the external app store and is + * usually provided as a result of calling QInAppProduct::purchase(). When the purchase flow has + * been completed by the user (confirming the purchase, for instance by entering their password), + * the QInAppStore instance containing the product will emit a QInAppStore::transactionReady() + * signal with data about the transaction. + * + * The status() provides information on whether the transaction was successful or not. If it was + * successful, then the application should take appropriate action. When the necessary action has + * been performed, finalize() should be called. The finalize() function should be called regardless + * of the status of the transaction. + * + * It is important that the application stores the purchase information before calling finalize(). + * If a transaction is not finalized (for example because the application was interrupted before + * it had a chance to save the information), then the transaction will be emitted again the next + * time the product is registered by QInAppStore::registerProduct(). + * + * Transactions can also be emitted after calling QInAppStore::restorePurchases(), at which point + * a new transaction will be emitted for each previously purchased unlockable product. + * + * \note Since transactions may under certain circumstances be emitted for the same transaction + * several times, the application should always check if the transaction has been registered + * before. Do not expect each transaction to be unique. + */ + +/*! + * \internal + */\ +QInAppTransaction::QInAppTransaction(TransactionStatus status, QInAppProduct *product, QObject *parent) + : QObject(parent) +{ + d = QSharedPointer<QInAppTransactionPrivate>(new QInAppTransactionPrivate(status, product)); +} + +/*! + * \internal + */\ +QInAppTransaction::~QInAppTransaction() +{ +} + +/*! + * Returns the product which is the object of this transaction. + */ +QInAppProduct *QInAppTransaction::product() const +{ + return d->product; +} + +/*! + * Returns the status of the transaction. If the purchase was successfully + * completed, the status will be PurchaseApproved. Otherwise, the purchase + * was unsuccessful. + */ + +QInAppTransaction::TransactionStatus QInAppTransaction::status() const +{ + return d->status; +} + +/*! + * Returns the platform-specific property given by \a propertyName. + * + * The following properties are available on Google Play: + * \list + * \li AndroidSignature: The signature of the transaction, as given by the + * private key for the application. + * \li AndroidPurchaseData: The purchase data returned by the Google Play store. + * \endlist + * These properties can be used to verify the purchase using the public key of + * your application. It is also possible to have the back-end verify the purchases + * by passing in the public key before registering products, using + * QInAppStore::setPlatformProperty(). + */ +QString QInAppTransaction::platformProperty(const QString &propertyName) const +{ + Q_UNUSED(propertyName); + return QString(); +} + +/*! + * \fn void QInAppTransaction::finalize() + * + * Call this when the application has finished performing all necessary reactions + * to the purchase. If the status is PurchaseApproved, the application should + * store the information about the transaction in a safe way before finalizing it. + * All transactions should be finalized. + */ + +QT_END_NAMESPACE diff --git a/src/mobileextras/inapppurchase/qinapptransaction.h b/src/mobileextras/inapppurchase/qinapptransaction.h new file mode 100644 index 0000000..cec5121 --- /dev/null +++ b/src/mobileextras/inapppurchase/qinapptransaction.h @@ -0,0 +1,64 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Mobile Extras Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QINAPPTRANSACTION_H +#define QINAPPTRANSACTION_H + +#include <QtCore/qobject.h> +#include <QtCore/qsharedpointer.h> +#include <QtMobileExtras/qmobileextrasglobal.h> + +QT_BEGIN_NAMESPACE + +class QInAppProduct; +class QInAppTransactionPrivate; +class Q_MOBILEEXTRAS_EXPORT QInAppTransaction: public QObject +{ + Q_OBJECT + Q_ENUMS(TransactionStatus) + Q_PROPERTY(TransactionStatus status READ status CONSTANT) + Q_PROPERTY(QInAppProduct * product READ product CONSTANT) +public: + enum TransactionStatus { + Unknown, + PurchaseApproved, + PurchaseFailed + }; + + ~QInAppTransaction(); + + QInAppProduct *product() const; + + Q_INVOKABLE virtual void finalize() = 0; + Q_INVOKABLE virtual QString platformProperty(const QString &propertyName) const; + + TransactionStatus status() const; + +protected: + explicit QInAppTransaction(TransactionStatus status, QInAppProduct *product, QObject *parent = 0); + +private: + Q_DISABLE_COPY(QInAppTransaction) + QSharedPointer<QInAppTransactionPrivate> d; +}; + +QT_END_NAMESPACE + +#endif // QINAPPTRANSACTION_H diff --git a/src/mobileextras/mobileextras.pro b/src/mobileextras/mobileextras.pro new file mode 100644 index 0000000..2ee4a9c --- /dev/null +++ b/src/mobileextras/mobileextras.pro @@ -0,0 +1,13 @@ +TARGET = QtMobileExtras +QT = core + +load(qt_module) + +ANDROID_BUNDLED_JAR_DEPENDENCIES = \ + jar/QtMobileExtras-bundled.jar +ANDROID_JAR_DEPENDENCIES = \ + jar/QtMobileExtras.jar + +HEADERS += qmobileextrasglobal.h + +include(inapppurchase/inapppurchase.pri) diff --git a/src/mobileextras/qmobileextrasglobal.h b/src/mobileextras/qmobileextrasglobal.h new file mode 100644 index 0000000..5cbfe8e --- /dev/null +++ b/src/mobileextras/qmobileextrasglobal.h @@ -0,0 +1,40 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Mobile Extras Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QTMOBILEEXTRASGLOBAL_H +#define QTMOBILEEXTRASGLOBAL_H + +#include <QtCore/qglobal.h> + +QT_BEGIN_NAMESPACE + +#ifndef QT_STATIC +# if defined(QT_BUILD_MOBILEEXTRAS_LIB) +# define Q_MOBILEEXTRAS_EXPORT Q_DECL_EXPORT +# else +# define Q_MOBILEEXTRAS_EXPORT Q_DECL_IMPORT +# endif +#else +# define Q_MOBILEEXTRAS_EXPORT +#endif + +QT_END_NAMESPACE + +#endif // QTMOBILEEXTRASGLOBAL_H diff --git a/src/src.pro b/src/src.pro new file mode 100644 index 0000000..d14f46d --- /dev/null +++ b/src/src.pro @@ -0,0 +1,2 @@ +TEMPLATE = subdirs +SUBDIRS = mobileextras android imports diff --git a/sync.profile b/sync.profile new file mode 100644 index 0000000..56897f4 --- /dev/null +++ b/sync.profile @@ -0,0 +1,17 @@ +%modules = ( # path to module name map + "QtMobileExtras" => "$basedir/src/mobileextras", +); +%moduleheaders = ( # restrict the module headers to those found in relative path +); +# Module dependencies. +# Every module that is required to build this module should have one entry. +# Each of the module version specifiers can take one of the following values: +# - A specific Git revision. +# - any git symbolic ref resolvable from the module's repository (e.g. "refs/heads/master" to track master branch) +# - an empty string to use the same branch under test (dependencies will become "refs/heads/master" if we are in the master branch) +# +%dependencies = ( + "qtbase" => "", + "qtandroidextras" => "", + "qtdeclarative" => "", +); |