Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Merge pull request #782 from poiuytrez/billingPlugin

[Android] Google Play In App Billing plugin
  • Loading branch information...
commit d8d4acea3b915460709602a91a623cd468098100 2 parents f699f4d + d7454ba
@devgeeks devgeeks authored
Showing with 3,743 additions and 0 deletions.
  1. +24 −0 Android/InAppBilling/com/android/vending/billing/IMarketBillingService.aidl
  2. +264 −0 Android/InAppBilling/com/smartmobilesoftware/inappbilling/InAppBillingPlugin.java
  3. +26 −0 Android/InAppBilling/inappbilling.js
  4. +746 −0 Android/InAppBilling/net/robotmedia/billing/BillingController.java
  5. +70 −0 Android/InAppBilling/net/robotmedia/billing/BillingReceiver.java
  6. +325 −0 Android/InAppBilling/net/robotmedia/billing/BillingRequest.java
  7. +289 −0 Android/InAppBilling/net/robotmedia/billing/BillingService.java
  8. +85 −0 Android/InAppBilling/net/robotmedia/billing/IBillingObserver.java
  9. +160 −0 Android/InAppBilling/net/robotmedia/billing/helper/AbstractBillingActivity.java
  10. +147 −0 Android/InAppBilling/net/robotmedia/billing/helper/AbstractBillingFragment.java
  11. +67 −0 Android/InAppBilling/net/robotmedia/billing/helper/AbstractBillingObserver.java
  12. +110 −0 Android/InAppBilling/net/robotmedia/billing/model/BillingDB.java
  13. +135 −0 Android/InAppBilling/net/robotmedia/billing/model/Transaction.java
  14. +77 −0 Android/InAppBilling/net/robotmedia/billing/model/TransactionManager.java
  15. +106 −0 Android/InAppBilling/net/robotmedia/billing/security/DefaultSignatureValidator.java
  16. +32 −0 Android/InAppBilling/net/robotmedia/billing/security/ISignatureValidator.java
  17. +114 −0 Android/InAppBilling/net/robotmedia/billing/utils/AESObfuscator.java
  18. +570 −0 Android/InAppBilling/net/robotmedia/billing/utils/Base64.java
  19. +32 −0 Android/InAppBilling/net/robotmedia/billing/utils/Base64DecoderException.java
  20. +75 −0 Android/InAppBilling/net/robotmedia/billing/utils/Compatibility.java
  21. +59 −0 Android/InAppBilling/net/robotmedia/billing/utils/Installation.java
  22. +75 −0 Android/InAppBilling/net/robotmedia/billing/utils/Security.java
  23. +155 −0 Android/InAppBilling/readme.md
View
24 Android/InAppBilling/com/android/vending/billing/IMarketBillingService.aidl
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2010 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;
+
+interface IMarketBillingService {
+ /** Given the arguments in bundle form, returns a bundle for results. */
+ Bundle sendBillingRequest(in Bundle bundle);
+}
View
264 Android/InAppBilling/com/smartmobilesoftware/inappbilling/InAppBillingPlugin.java
@@ -0,0 +1,264 @@
+package com.smartmobilesoftware.inappbilling;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.cordova.api.Plugin;
+import org.apache.cordova.api.PluginResult;
+import org.json.JSONArray;
+
+import net.robotmedia.billing.BillingController;
+import net.robotmedia.billing.BillingRequest.ResponseCode;
+import net.robotmedia.billing.helper.AbstractBillingObserver;
+import net.robotmedia.billing.model.Transaction;
+import net.robotmedia.billing.model.Transaction.PurchaseState;
+
+import android.app.Activity;
+import android.util.Log;
+
+// In app billing plugin
+public class InAppBillingPlugin extends Plugin {
+ // Yes, that's the two variables to edit :)
+ private static final String publicKey = "PASTE_HERE_YOUR_PUBLIC_KEY";
+ private static final byte[] salt = { 42, -70, -106, -41, 66, -53, 122,
+ -110, -127, -96, -88, 77, 127, 115, 1, 73, 17, 110, 48, -116 };
+
+ private static final String INIT_STRING = "init";
+ private static final String PURCHASE_STRING = "purchase";
+ private static final String OWN_ITEMS_STRING = "ownItems";
+
+
+ private final String TAG = "BILLING";
+ // Observer notified of events
+ private AbstractBillingObserver mBillingObserver;
+ private Activity activity;
+ private String callbackId;
+
+ // Plugin action handler
+ public PluginResult execute(String action, JSONArray data, String callbackId) {
+ // Save the callback id
+ this.callbackId = callbackId;
+ PluginResult pluginResult;
+
+ if (INIT_STRING.equals(action)) {
+ // Initialize the plugin
+ initialize();
+ // Wait for the restore transaction and billing supported test
+ pluginResult = new PluginResult(PluginResult.Status.NO_RESULT);
+ pluginResult.setKeepCallback(true);
+ return pluginResult;
+ } else if (PURCHASE_STRING.equals(action)) {
+ // purchase the item
+ try {
+
+ // Retrieve parameters
+ String productId = data.getString(0);
+ // Make the purchase
+ BillingController.requestPurchase(this.cordova.getContext(), productId, true /* confirm */, null);
+
+ pluginResult = new PluginResult(PluginResult.Status.NO_RESULT);
+ pluginResult.setKeepCallback(true);
+ } catch (Exception ex) {
+ pluginResult = new PluginResult(
+ PluginResult.Status.JSON_EXCEPTION, "Invalid parameter");
+ }
+
+ return pluginResult;
+
+ } else if (OWN_ITEMS_STRING.equals(action)){
+ // retrieve already bought items
+
+ ArrayList<String> ownItems = new ArrayList<String>();
+ ownItems = getOwnedItems();
+ // convert the list of strings to a json list of strings
+ JSONArray ownItemsJson = new JSONArray();
+ for (String item : ownItems){
+ ownItemsJson.put(item);
+ }
+
+ // send the result to the app
+ pluginResult = new PluginResult(PluginResult.Status.OK, ownItemsJson);
+ return pluginResult;
+ }
+
+ return null;
+ }
+
+ // Initialize the plugin
+ private void initialize() {
+ BillingController.setDebug(true);
+ // configure the in app billing
+ BillingController
+ .setConfiguration(new BillingController.IConfiguration() {
+
+ public byte[] getObfuscationSalt() {
+ return InAppBillingPlugin.salt;
+ }
+
+ public String getPublicKey() {
+ return InAppBillingPlugin.publicKey;
+ }
+ });
+
+ // Get the activity from the Plugin
+ // Activity test = this.cordova.getContext().
+ activity = cordova.getActivity();
+
+ // create a billingObserver
+ mBillingObserver = new AbstractBillingObserver(activity) {
+
+ public void onBillingChecked(boolean supported) {
+ InAppBillingPlugin.this.onBillingChecked(supported);
+ }
+
+ public void onPurchaseStateChanged(String itemId,
+ PurchaseState state) {
+ InAppBillingPlugin.this.onPurchaseStateChanged(itemId, state);
+ }
+
+ public void onRequestPurchaseResponse(String itemId,
+ ResponseCode response) {
+ InAppBillingPlugin.this.onRequestPurchaseResponse(itemId,
+ response);
+ }
+
+ public void onSubscriptionChecked(boolean supported) {
+ InAppBillingPlugin.this.onSubscriptionChecked(supported);
+ }
+
+ };
+
+ // register observer
+ BillingController.registerObserver(mBillingObserver);
+ BillingController.checkBillingSupported(activity);
+
+ }
+
+ public void onBillingChecked(boolean supported) {
+ PluginResult result;
+ if (supported) {
+ Log.d("BILLING", "In app billing supported");
+ // restores previous transactions, if any.
+ restoreTransactions();
+ result = new PluginResult(PluginResult.Status.OK, "In app billing supported");
+ // stop waiting for the callback
+ result.setKeepCallback(false);
+ // notify the app
+ this.success(result, callbackId);
+ } else {
+ Log.d("BILLING", "In app billing not supported");
+ result = new PluginResult(PluginResult.Status.ERROR,
+ "In app billing not supported");
+ // stop waiting for the callback
+ result.setKeepCallback(false);
+ // notify the app
+ this.error(result, callbackId);
+ }
+
+ }
+
+ // change in the purchase
+ public void onPurchaseStateChanged(String itemId, PurchaseState state) {
+ PluginResult result;
+
+ Log.i(TAG, "onPurchaseStateChanged() itemId: " + itemId);
+
+ // Check the status of the purchase
+ if(state == PurchaseState.PURCHASED){
+ // Item has been purchased :)
+ result = new PluginResult(PluginResult.Status.OK, itemId);
+ result.setKeepCallback(false);
+ this.success(result, callbackId);
+ } else {
+ // purchase issue
+ String message = "";
+ if (state == PurchaseState.CANCELLED){
+ message = "canceled";
+ } else if (state == PurchaseState.REFUNDED){
+ message = "refunded";
+ } else if (state == PurchaseState.EXPIRED){
+ message = "expired";
+ }
+ // send the result to the app
+ result = new PluginResult(PluginResult.Status.ERROR, message);
+ result.setKeepCallback(false);
+ this.error(result, callbackId);
+
+ }
+
+
+ }
+
+ // response from the billing server
+ public void onRequestPurchaseResponse(String itemId, ResponseCode response) {
+ PluginResult result;
+
+ // check the response
+ Log.d(TAG, "response code ");
+
+ if(response == ResponseCode.RESULT_OK){
+ // purchase succeeded
+ result = new PluginResult(PluginResult.Status.OK, itemId);
+ result.setKeepCallback(false);
+ this.success(result, callbackId);
+ } else {
+ // purchase error
+ String message = "";
+
+ // get the error message
+ if (response == ResponseCode.RESULT_USER_CANCELED){
+ message = "canceled";
+
+ } else if (response == ResponseCode.RESULT_SERVICE_UNAVAILABLE){
+ message = "network connection error";
+ } else if (response == ResponseCode.RESULT_BILLING_UNAVAILABLE){
+ message = "in app billing unavailable";
+ } else if (response == ResponseCode.RESULT_ITEM_UNAVAILABLE){
+ message = "cannot find the item";
+ } else if (response == ResponseCode.RESULT_DEVELOPER_ERROR){
+ message = "developer error";
+ } else if (response == ResponseCode.RESULT_ERROR){
+ message = "unexpected server error";
+ }
+ // send the result to the app
+ result = new PluginResult(PluginResult.Status.ERROR, message);
+ result.setKeepCallback(false);
+ this.error(result, callbackId);
+ }
+
+ }
+
+ public void onSubscriptionChecked(boolean supported) {
+
+ }
+
+ /**
+ * Restores previous transactions, if any. This happens if the application
+ * has just been installed or the user wiped data. We do not want to do this
+ * on every startup, rather, we want to do only when the database needs to
+ * be initialized.
+ */
+ private void restoreTransactions() {
+ if (!mBillingObserver.isTransactionsRestored()) {
+ BillingController.restoreTransactions(this.cordova.getContext());
+
+ }
+ }
+
+ // update bought items
+ private ArrayList<String> getOwnedItems() {
+ List<Transaction> transactions = BillingController
+ .getTransactions(this.cordova.getContext());
+ final ArrayList<String> ownedItems = new ArrayList<String>();
+ for (Transaction t : transactions) {
+ if (t.purchaseState == PurchaseState.PURCHASED) {
+ ownedItems.add(t.productId);
+ }
+ }
+ // The list of purchased items is now stored in "ownedItems"
+
+ return ownedItems;
+
+ }
+
+}
View
26 Android/InAppBilling/inappbilling.js
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2012 by Guillaume Charhon
+ */
+var inappbilling = {
+
+ // Initialize the plugin
+ init: function (success, fail) {
+ return cordova.exec( success, fail,
+ "InAppBillingPlugin",
+ "init", ["null"]);
+ },
+ // purchase an item
+ purchase: function (success, fail, productId) {
+ return cordova.exec( success, fail,
+ "InAppBillingPlugin",
+ "purchase", [productId]);
+ },
+ // get already own items
+ getOwnItems: function (success, fail) {
+ return cordova.exec( success, fail,
+ "InAppBillingPlugin",
+ "ownItems", ["null"]);
+ },
+
+
+};
View
746 Android/InAppBilling/net/robotmedia/billing/BillingController.java
@@ -0,0 +1,746 @@
+/* Copyright 2011 Robot Media SL (http://www.robotmedia.net)
+ *
+ * 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 net.robotmedia.billing;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import net.robotmedia.billing.model.Transaction;
+import net.robotmedia.billing.model.TransactionManager;
+import net.robotmedia.billing.security.DefaultSignatureValidator;
+import net.robotmedia.billing.security.ISignatureValidator;
+import net.robotmedia.billing.utils.Compatibility;
+import net.robotmedia.billing.utils.Security;
+
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.app.PendingIntent.CanceledException;
+import android.content.Context;
+import android.content.Intent;
+import android.text.TextUtils;
+import android.util.Log;
+
+public class BillingController {
+
+ public static enum BillingStatus {
+ UNKNOWN, SUPPORTED, UNSUPPORTED
+ }
+
+ /**
+ * Used to provide on-demand values to the billing controller.
+ */
+ public interface IConfiguration {
+
+ /**
+ * Returns a salt for the obfuscation of purchases in local memory.
+ *
+ * @return array of 20 random bytes.
+ */
+ public byte[] getObfuscationSalt();
+
+ /**
+ * Returns the public key used to verify the signature of responses of
+ * the Market Billing service.
+ *
+ * @return Base64 encoded public key.
+ */
+ public String getPublicKey();
+ }
+
+ private static BillingStatus billingStatus = BillingStatus.UNKNOWN;
+ private static BillingStatus subscriptionStatus = BillingStatus.UNKNOWN;
+
+ private static Set<String> automaticConfirmations = new HashSet<String>();
+ private static IConfiguration configuration = null;
+ private static boolean debug = false;
+ private static ISignatureValidator validator = null;
+
+ private static final String JSON_NONCE = "nonce";
+ private static final String JSON_ORDERS = "orders";
+ private static HashMap<String, Set<String>> manualConfirmations = new HashMap<String, Set<String>>();
+
+ private static Set<IBillingObserver> observers = new HashSet<IBillingObserver>();
+
+ public static final String LOG_TAG = "Billing";
+
+ private static HashMap<Long, BillingRequest> pendingRequests = new HashMap<Long, BillingRequest>();
+
+ /**
+ * Adds the specified notification to the set of manual confirmations of the
+ * specified item.
+ *
+ * @param itemId
+ * id of the item.
+ * @param notificationId
+ * id of the notification.
+ */
+ private static final void addManualConfirmation(String itemId, String notificationId) {
+ Set<String> notifications = manualConfirmations.get(itemId);
+ if (notifications == null) {
+ notifications = new HashSet<String>();
+ manualConfirmations.put(itemId, notifications);
+ }
+ notifications.add(notificationId);
+ }
+
+ /**
+ * Returns the in-app product billing support status, and checks it
+ * asynchronously if it is currently unknown. Observers will receive a
+ * {@link IBillingObserver#onBillingChecked(boolean)} notification in either
+ * case.
+ * <p>
+ * In-app product support does not imply subscription support. To check if
+ * subscriptions are supported, use
+ * {@link BillingController#checkSubscriptionSupported(Context)}.
+ * </p>
+ *
+ * @param context
+ * @return the current in-app product billing support status (unknown,
+ * supported or unsupported). If it is unsupported, subscriptions
+ * are also unsupported.
+ * @see IBillingObserver#onBillingChecked(boolean)
+ * @see BillingController#checkSubscriptionSupported(Context)
+ */
+ public static BillingStatus checkBillingSupported(Context context) {
+ if (billingStatus == BillingStatus.UNKNOWN) {
+ BillingService.checkBillingSupported(context);
+ } else {
+ boolean supported = billingStatus == BillingStatus.SUPPORTED;
+ onBillingChecked(supported);
+ }
+ return billingStatus;
+ }
+
+ /**
+ * <p>
+ * Returns the subscription billing support status, and checks it
+ * asynchronously if it is currently unknown. Observers will receive a
+ * {@link IBillingObserver#onSubscriptionChecked(boolean)} notification in
+ * either case.
+ * </p>
+ * <p>
+ * No support for subscriptions does not imply that in-app products are also
+ * unsupported. To check if in-app products are supported, use
+ * {@link BillingController#checkBillingSupported(Context)}.
+ * </p>
+ *
+ * @param context
+ * @return the current subscription billing status (unknown, supported or
+ * unsupported). If it is supported, in-app products are also
+ * supported.
+ * @see IBillingObserver#onSubscriptionChecked(boolean)
+ * @see BillingController#checkBillingSupported(Context)
+ */
+ public static BillingStatus checkSubscriptionSupported(Context context) {
+ if (subscriptionStatus == BillingStatus.UNKNOWN) {
+ BillingService.checkSubscriptionSupported(context);
+ } else {
+ boolean supported = subscriptionStatus == BillingStatus.SUPPORTED;
+ onSubscriptionChecked(supported);
+ }
+ return subscriptionStatus;
+ }
+
+ /**
+ * Requests to confirm all pending notifications for the specified item.
+ *
+ * @param context
+ * @param itemId
+ * id of the item whose purchase must be confirmed.
+ * @return true if pending notifications for this item were found, false
+ * otherwise.
+ */
+ public static boolean confirmNotifications(Context context, String itemId) {
+ final Set<String> notifications = manualConfirmations.get(itemId);
+ if (notifications != null) {
+ confirmNotifications(context, notifications.toArray(new String[] {}));
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Requests to confirm all specified notifications.
+ *
+ * @param context
+ * @param notifyIds
+ * array with the ids of all the notifications to confirm.
+ */
+ private static void confirmNotifications(Context context, String[] notifyIds) {
+ BillingService.confirmNotifications(context, notifyIds);
+ }
+
+ /**
+ * Returns the number of purchases for the specified item. Refunded and
+ * cancelled purchases are not subtracted. See
+ * {@link #countPurchasesNet(Context, String)} if they need to be.
+ *
+ * @param context
+ * @param itemId
+ * id of the item whose purchases will be counted.
+ * @return number of purchases for the specified item.
+ */
+ public static int countPurchases(Context context, String itemId) {
+ final byte[] salt = getSalt();
+ itemId = salt != null ? Security.obfuscate(context, salt, itemId) : itemId;
+ return TransactionManager.countPurchases(context, itemId);
+ }
+
+ protected static void debug(String message) {
+ if (debug) {
+ Log.d(LOG_TAG, message);
+ }
+ }
+
+ /**
+ * Requests purchase information for the specified notification. Immediately
+ * followed by a call to
+ * {@link #onPurchaseInformationResponse(long, boolean)} and later to
+ * {@link #onPurchaseStateChanged(Context, String, String)}, if the request
+ * is successful.
+ *
+ * @param context
+ * @param notifyId
+ * id of the notification whose purchase information is
+ * requested.
+ */
+ private static void getPurchaseInformation(Context context, String notifyId) {
+ final long nonce = Security.generateNonce();
+ BillingService.getPurchaseInformation(context, new String[] { notifyId }, nonce);
+ }
+
+ /**
+ * Gets the salt from the configuration and logs a warning if it's null.
+ *
+ * @return salt.
+ */
+ private static byte[] getSalt() {
+ byte[] salt = null;
+ if (configuration == null || ((salt = configuration.getObfuscationSalt()) == null)) {
+ Log.w(LOG_TAG, "Can't (un)obfuscate purchases without salt");
+ }
+ return salt;
+ }
+
+ /**
+ * Lists all transactions stored locally, including cancellations and
+ * refunds.
+ *
+ * @param context
+ * @return list of transactions.
+ */
+ public static List<Transaction> getTransactions(Context context) {
+ List<Transaction> transactions = TransactionManager.getTransactions(context);
+ unobfuscate(context, transactions);
+ return transactions;
+ }
+
+ /**
+ * Lists all transactions of the specified item, stored locally.
+ *
+ * @param context
+ * @param itemId
+ * id of the item whose transactions will be returned.
+ * @return list of transactions.
+ */
+ public static List<Transaction> getTransactions(Context context, String itemId) {
+ final byte[] salt = getSalt();
+ itemId = salt != null ? Security.obfuscate(context, salt, itemId) : itemId;
+ List<Transaction> transactions = TransactionManager.getTransactions(context, itemId);
+ unobfuscate(context, transactions);
+ return transactions;
+ }
+
+ /**
+ * Returns true if the specified item has been registered as purchased in
+ * local memory, false otherwise. Also note that the item might have been
+ * purchased in another installation, but not yet registered in this one.
+ *
+ * @param context
+ * @param itemId
+ * item id.
+ * @return true if the specified item is purchased, false otherwise.
+ */
+ public static boolean isPurchased(Context context, String itemId) {
+ final byte[] salt = getSalt();
+ itemId = salt != null ? Security.obfuscate(context, salt, itemId) : itemId;
+ return TransactionManager.isPurchased(context, itemId);
+ }
+
+ /**
+ * Notifies observers of the purchase state change of the specified item.
+ *
+ * @param itemId
+ * id of the item whose purchase state has changed.
+ * @param state
+ * new purchase state of the item.
+ */
+ private static void notifyPurchaseStateChange(String itemId, Transaction.PurchaseState state) {
+ for (IBillingObserver o : observers) {
+ o.onPurchaseStateChanged(itemId, state);
+ }
+ }
+
+ /**
+ * Obfuscates the specified purchase. Only the order id, product id and
+ * developer payload are obfuscated.
+ *
+ * @param context
+ * @param purchase
+ * purchase to be obfuscated.
+ * @see #unobfuscate(Context, Transaction)
+ */
+ static void obfuscate(Context context, Transaction purchase) {
+ final byte[] salt = getSalt();
+ if (salt == null) {
+ return;
+ }
+ purchase.orderId = Security.obfuscate(context, salt, purchase.orderId);
+ purchase.productId = Security.obfuscate(context, salt, purchase.productId);
+ purchase.developerPayload = Security.obfuscate(context, salt, purchase.developerPayload);
+ }
+
+ /**
+ * Called after the response to a
+ * {@link net.robotmedia.billing.request.CheckBillingSupported} request is
+ * received.
+ *
+ * @param supported
+ */
+ protected static void onBillingChecked(boolean supported) {
+ billingStatus = supported ? BillingStatus.SUPPORTED : BillingStatus.UNSUPPORTED;
+ if (billingStatus == BillingStatus.UNSUPPORTED) { // Save us the
+ // subscription
+ // check
+ subscriptionStatus = BillingStatus.UNSUPPORTED;
+ }
+ for (IBillingObserver o : observers) {
+ o.onBillingChecked(supported);
+ }
+ }
+
+ /**
+ * Called when an IN_APP_NOTIFY message is received.
+ *
+ * @param context
+ * @param notifyId
+ * notification id.
+ */
+ protected static void onNotify(Context context, String notifyId) {
+ debug("Notification " + notifyId + " available");
+
+ getPurchaseInformation(context, notifyId);
+ }
+
+ /**
+ * Called after the response to a
+ * {@link net.robotmedia.billing.request.RequestPurchase} request is
+ * received.
+ *
+ * @param itemId
+ * id of the item whose purchase was requested.
+ * @param purchaseIntent
+ * intent to purchase the item.
+ */
+ protected static void onPurchaseIntent(String itemId, PendingIntent purchaseIntent) {
+ for (IBillingObserver o : observers) {
+ o.onPurchaseIntent(itemId, purchaseIntent);
+ }
+ }
+
+ /**
+ * Called after the response to a
+ * {@link net.robotmedia.billing.request.GetPurchaseInformation} request is
+ * received. Registers all transactions in local memory and confirms those
+ * who can be confirmed automatically.
+ *
+ * @param context
+ * @param signedData
+ * signed JSON data received from the Market Billing service.
+ * @param signature
+ * data signature.
+ */
+ protected static void onPurchaseStateChanged(Context context, String signedData, String signature) {
+ debug("Purchase state changed");
+
+ if (TextUtils.isEmpty(signedData)) {
+ Log.w(LOG_TAG, "Signed data is empty");
+ return;
+ } else {
+ debug(signedData);
+ }
+
+ if (!debug) {
+ if (TextUtils.isEmpty(signature)) {
+ Log.w(LOG_TAG, "Empty signature requires debug mode");
+ return;
+ }
+ final ISignatureValidator validator = BillingController.validator != null ? BillingController.validator
+ : new DefaultSignatureValidator(BillingController.configuration);
+ if (!validator.validate(signedData, signature)) {
+ Log.w(LOG_TAG, "Signature does not match data.");
+ return;
+ }
+ }
+
+ List<Transaction> purchases;
+ try {
+ JSONObject jObject = new JSONObject(signedData);
+ if (!verifyNonce(jObject)) {
+ Log.w(LOG_TAG, "Invalid nonce");
+ return;
+ }
+ purchases = parsePurchases(jObject);
+ } catch (JSONException e) {
+ Log.e(LOG_TAG, "JSON exception: ", e);
+ return;
+ }
+
+ ArrayList<String> confirmations = new ArrayList<String>();
+ for (Transaction p : purchases) {
+ if (p.notificationId != null && automaticConfirmations.contains(p.productId)) {
+ confirmations.add(p.notificationId);
+ } else {
+ // TODO: Discriminate between purchases, cancellations and
+ // refunds.
+ addManualConfirmation(p.productId, p.notificationId);
+ }
+ storeTransaction(context, p);
+ notifyPurchaseStateChange(p.productId, p.purchaseState);
+ }
+ if (!confirmations.isEmpty()) {
+ final String[] notifyIds = confirmations.toArray(new String[confirmations.size()]);
+ confirmNotifications(context, notifyIds);
+ }
+ }
+
+ /**
+ * Called after a {@link net.robotmedia.billing.BillingRequest} is sent.
+ *
+ * @param requestId
+ * the id the request.
+ * @param request
+ * the billing request.
+ */
+ protected static void onRequestSent(long requestId, BillingRequest request) {
+ debug("Request " + requestId + " of type " + request.getRequestType() + " sent");
+
+ if (request.isSuccess()) {
+ pendingRequests.put(requestId, request);
+ } else if (request.hasNonce()) {
+ Security.removeNonce(request.getNonce());
+ }
+ }
+
+ /**
+ * Called after a {@link net.robotmedia.billing.BillingRequest} is sent.
+ *
+ * @param context
+ * @param requestId
+ * the id of the request.
+ * @param responseCode
+ * the response code.
+ * @see net.robotmedia.billing.request.ResponseCode
+ */
+ protected static void onResponseCode(Context context, long requestId, int responseCode) {
+ final BillingRequest.ResponseCode response = BillingRequest.ResponseCode.valueOf(responseCode);
+ debug("Request " + requestId + " received response " + response);
+
+ final BillingRequest request = pendingRequests.get(requestId);
+ if (request != null) {
+ pendingRequests.remove(requestId);
+ request.onResponseCode(response);
+ }
+ }
+
+ /**
+ * Called after the response to a
+ * {@link net.robotmedia.billing.request.CheckSubscriptionSupported} request
+ * is received.
+ *
+ * @param supported
+ */
+ protected static void onSubscriptionChecked(boolean supported) {
+ subscriptionStatus = supported ? BillingStatus.SUPPORTED : BillingStatus.UNSUPPORTED;
+ if (subscriptionStatus == BillingStatus.SUPPORTED) { // Save us the
+ // billing check
+ billingStatus = BillingStatus.SUPPORTED;
+ }
+ for (IBillingObserver o : observers) {
+ o.onSubscriptionChecked(supported);
+ }
+ }
+
+ protected static void onTransactionsRestored() {
+ for (IBillingObserver o : observers) {
+ o.onTransactionsRestored();
+ }
+ }
+
+ /**
+ * Parse all purchases from the JSON data received from the Market Billing
+ * service.
+ *
+ * @param data
+ * JSON data received from the Market Billing service.
+ * @return list of purchases.
+ * @throws JSONException
+ * if the data couldn't be properly parsed.
+ */
+ private static List<Transaction> parsePurchases(JSONObject data) throws JSONException {
+ ArrayList<Transaction> purchases = new ArrayList<Transaction>();
+ JSONArray orders = data.optJSONArray(JSON_ORDERS);
+ int numTransactions = 0;
+ if (orders != null) {
+ numTransactions = orders.length();
+ }
+ for (int i = 0; i < numTransactions; i++) {
+ JSONObject jElement = orders.getJSONObject(i);
+ Transaction p = Transaction.parse(jElement);
+ purchases.add(p);
+ }
+ return purchases;
+ }
+
+ /**
+ * Registers the specified billing observer.
+ *
+ * @param observer
+ * the billing observer to add.
+ * @return true if the observer wasn't previously registered, false
+ * otherwise.
+ * @see #unregisterObserver(IBillingObserver)
+ */
+ public static boolean registerObserver(IBillingObserver observer) {
+ return observers.add(observer);
+ }
+
+ /**
+ * Requests the purchase of the specified item. The transaction will not be
+ * confirmed automatically.
+ * <p>
+ * For subscriptions, use {@link #requestSubscription(Context, String)}
+ * instead.
+ * </p>
+ *
+ * @param context
+ * @param itemId
+ * id of the item to be purchased.
+ * @see #requestPurchase(Context, String, boolean)
+ */
+ public static void requestPurchase(Context context, String itemId) {
+ requestPurchase(context, itemId, false, null);
+ }
+
+ /**
+ * <p>
+ * Requests the purchase of the specified item with optional automatic
+ * confirmation.
+ * </p>
+ * <p>
+ * For subscriptions, use
+ * {@link #requestSubscription(Context, String, boolean, String)} instead.
+ * </p>
+ *
+ * @param context
+ * @param itemId
+ * id of the item to be purchased.
+ * @param confirm
+ * if true, the transaction will be confirmed automatically. If
+ * false, the transaction will have to be confirmed with a call
+ * to {@link #confirmNotifications(Context, String)}.
+ * @param developerPayload
+ * a developer-specified string that contains supplemental
+ * information about the order.
+ * @see IBillingObserver#onPurchaseIntent(String, PendingIntent)
+ */
+ public static void requestPurchase(Context context, String itemId, boolean confirm, String developerPayload) {
+ if (confirm) {
+ automaticConfirmations.add(itemId);
+ }
+ BillingService.requestPurchase(context, itemId, developerPayload);
+ }
+
+ /**
+ * Requests the purchase of the specified subscription item. The transaction
+ * will not be confirmed automatically.
+ *
+ * @param context
+ * @param itemId
+ * id of the item to be purchased.
+ * @see #requestSubscription(Context, String, boolean, String)
+ */
+ public static void requestSubscription(Context context, String itemId) {
+ requestSubscription(context, itemId, false, null);
+ }
+
+ /**
+ * Requests the purchase of the specified subscription item with optional
+ * automatic confirmation.
+ *
+ * @param context
+ * @param itemId
+ * id of the item to be purchased.
+ * @param confirm
+ * if true, the transaction will be confirmed automatically. If
+ * false, the transaction will have to be confirmed with a call
+ * to {@link #confirmNotifications(Context, String)}.
+ * @param developerPayload
+ * a developer-specified string that contains supplemental
+ * information about the order.
+ * @see IBillingObserver#onPurchaseIntent(String, PendingIntent)
+ */
+ public static void requestSubscription(Context context, String itemId, boolean confirm, String developerPayload) {
+ if (confirm) {
+ automaticConfirmations.add(itemId);
+ }
+ BillingService.requestSubscription(context, itemId, developerPayload);
+ }
+
+ /**
+ * Requests to restore all transactions.
+ *
+ * @param context
+ */
+ public static void restoreTransactions(Context context) {
+ final long nonce = Security.generateNonce();
+ BillingService.restoreTransations(context, nonce);
+ }
+
+ /**
+ * Sets the configuration instance of the controller.
+ *
+ * @param config
+ * configuration instance.
+ */
+ public static void setConfiguration(IConfiguration config) {
+ configuration = config;
+ }
+
+ /**
+ * Sets debug mode.
+ *
+ * @param value
+ */
+ public static final void setDebug(boolean value) {
+ debug = value;
+ }
+
+ /**
+ * Sets a custom signature validator. If no custom signature validator is
+ * provided,
+ * {@link net.robotmedia.billing.signature.DefaultSignatureValidator} will
+ * be used.
+ *
+ * @param validator
+ * signature validator instance.
+ */
+ public static void setSignatureValidator(ISignatureValidator validator) {
+ BillingController.validator = validator;
+ }
+
+ /**
+ * Starts the specified purchase intent with the specified activity.
+ *
+ * @param activity
+ * @param purchaseIntent
+ * purchase intent.
+ * @param intent
+ */
+ public static void startPurchaseIntent(Activity activity, PendingIntent purchaseIntent, Intent intent) {
+ if (Compatibility.isStartIntentSenderSupported()) {
+ // This is on Android 2.0 and beyond. The in-app buy page activity
+ // must be on the activity stack of the application.
+ Compatibility.startIntentSender(activity, purchaseIntent.getIntentSender(), intent);
+ } else {
+ // This is on Android version 1.6. The in-app buy page activity must
+ // be on its own separate activity stack instead of on the activity
+ // stack of the application.
+ try {
+ purchaseIntent.send(activity, 0 /* code */, intent);
+ } catch (CanceledException e) {
+ Log.e(LOG_TAG, "Error starting purchase intent", e);
+ }
+ }
+ }
+
+ static void storeTransaction(Context context, Transaction t) {
+ final Transaction t2 = t.clone();
+ obfuscate(context, t2);
+ TransactionManager.addTransaction(context, t2);
+ }
+
+ static void unobfuscate(Context context, List<Transaction> transactions) {
+ for (Transaction p : transactions) {
+ unobfuscate(context, p);
+ }
+ }
+
+ /**
+ * Unobfuscate the specified purchase.
+ *
+ * @param context
+ * @param purchase
+ * purchase to unobfuscate.
+ * @see #obfuscate(Context, Transaction)
+ */
+ static void unobfuscate(Context context, Transaction purchase) {
+ final byte[] salt = getSalt();
+ if (salt == null) {
+ return;
+ }
+ purchase.orderId = Security.unobfuscate(context, salt, purchase.orderId);
+ purchase.productId = Security.unobfuscate(context, salt, purchase.productId);
+ purchase.developerPayload = Security.unobfuscate(context, salt, purchase.developerPayload);
+ }
+
+ /**
+ * Unregisters the specified billing observer.
+ *
+ * @param observer
+ * the billing observer to unregister.
+ * @return true if the billing observer was unregistered, false otherwise.
+ * @see #registerObserver(IBillingObserver)
+ */
+ public static boolean unregisterObserver(IBillingObserver observer) {
+ return observers.remove(observer);
+ }
+
+ private static boolean verifyNonce(JSONObject data) {
+ long nonce = data.optLong(JSON_NONCE);
+ if (Security.isNonceKnown(nonce)) {
+ Security.removeNonce(nonce);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ protected static void onRequestPurchaseResponse(String itemId, BillingRequest.ResponseCode response) {
+ for (IBillingObserver o : observers) {
+ o.onRequestPurchaseResponse(itemId, response);
+ }
+ }
+
+}
View
70 Android/InAppBilling/net/robotmedia/billing/BillingReceiver.java
@@ -0,0 +1,70 @@
+/* Copyright 2011 Robot Media SL (http://www.robotmedia.net)
+*
+* 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 net.robotmedia.billing;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+public class BillingReceiver extends BroadcastReceiver {
+
+ static final String ACTION_NOTIFY = "com.android.vending.billing.IN_APP_NOTIFY";
+ static final String ACTION_RESPONSE_CODE =
+ "com.android.vending.billing.RESPONSE_CODE";
+ static final String ACTION_PURCHASE_STATE_CHANGED =
+ "com.android.vending.billing.PURCHASE_STATE_CHANGED";
+
+ static final String EXTRA_NOTIFICATION_ID = "notification_id";
+ static final String EXTRA_INAPP_SIGNED_DATA = "inapp_signed_data";
+ static final String EXTRA_INAPP_SIGNATURE = "inapp_signature";
+ static final String EXTRA_REQUEST_ID = "request_id";
+ static final String EXTRA_RESPONSE_CODE = "response_code";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ BillingController.debug("Received " + action);
+
+ if (ACTION_PURCHASE_STATE_CHANGED.equals(action)) {
+ purchaseStateChanged(context, intent);
+ } else if (ACTION_NOTIFY.equals(action)) {
+ notify(context, intent);
+ } else if (ACTION_RESPONSE_CODE.equals(action)) {
+ responseCode(context, intent);
+ } else {
+ Log.w(this.getClass().getSimpleName(), "Unexpected action: " + action);
+ }
+ }
+
+ private void purchaseStateChanged(Context context, Intent intent) {
+ final String signedData = intent.getStringExtra(EXTRA_INAPP_SIGNED_DATA);
+ final String signature = intent.getStringExtra(EXTRA_INAPP_SIGNATURE);
+ BillingController.onPurchaseStateChanged(context, signedData, signature);
+ }
+
+ private void notify(Context context, Intent intent) {
+ String notifyId = intent.getStringExtra(EXTRA_NOTIFICATION_ID);
+ BillingController.onNotify(context, notifyId);
+ }
+
+ private void responseCode(Context context, Intent intent) {
+ final long requestId = intent.getLongExtra(EXTRA_REQUEST_ID, -1);
+ final int responseCode = intent.getIntExtra(EXTRA_RESPONSE_CODE, 0);
+ BillingController.onResponseCode(context, requestId, responseCode);
+ }
+
+}
View
325 Android/InAppBilling/net/robotmedia/billing/BillingRequest.java
@@ -0,0 +1,325 @@
+/* Copyright 2011 Robot Media SL (http://www.robotmedia.net)
+*
+* 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 net.robotmedia.billing;
+
+import android.app.PendingIntent;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.vending.billing.IMarketBillingService;
+
+public abstract class BillingRequest {
+
+ public static class CheckBillingSupported extends BillingRequest {
+
+ public CheckBillingSupported(String packageName, int startId) {
+ super(packageName, startId);
+ }
+
+ @Override
+ public String getRequestType() {
+ return REQUEST_TYPE_CHECK_BILLING_SUPPORTED;
+ }
+
+ @Override
+ protected void processOkResponse(Bundle response) {
+ final boolean supported = this.isSuccess();
+ BillingController.onBillingChecked(supported);
+ }
+
+ }
+
+ public static class CheckSubscriptionSupported extends BillingRequest {
+
+ public CheckSubscriptionSupported(String packageName, int startId) {
+ super(packageName, startId);
+ }
+
+ @Override
+ protected int getAPIVersion() {
+ return 2;
+ };
+
+ @Override
+ public String getRequestType() {
+ return REQUEST_TYPE_CHECK_BILLING_SUPPORTED;
+ }
+
+ @Override
+ protected void processOkResponse(Bundle response) {
+ final boolean supported = this.isSuccess();
+ BillingController.onSubscriptionChecked(supported);
+ }
+
+ @Override
+ protected void addParams(Bundle request) {
+ request.putString(KEY_ITEM_TYPE, ITEM_TYPE_SUBSCRIPTION);
+ }
+
+ }
+
+ public static class ConfirmNotifications extends BillingRequest {
+
+ private String[] notifyIds;
+
+ private static final String KEY_NOTIFY_IDS = "NOTIFY_IDS";
+
+ public ConfirmNotifications(String packageName, int startId, String[] notifyIds) {
+ super(packageName, startId);
+ this.notifyIds = notifyIds;
+ }
+
+ @Override
+ protected void addParams(Bundle request) {
+ request.putStringArray(KEY_NOTIFY_IDS, notifyIds);
+ }
+
+ @Override
+ public String getRequestType() {
+ return "CONFIRM_NOTIFICATIONS";
+ }
+
+ }
+ public static class GetPurchaseInformation extends BillingRequest {
+
+ private String[] notifyIds;
+
+ private static final String KEY_NOTIFY_IDS = "NOTIFY_IDS";
+
+ public GetPurchaseInformation(String packageName, int startId, String[] notifyIds) {
+ super(packageName,startId);
+ this.notifyIds = notifyIds;
+ }
+
+ @Override
+ protected void addParams(Bundle request) {
+ request.putStringArray(KEY_NOTIFY_IDS, notifyIds);
+ }
+
+ @Override
+ public String getRequestType() {
+ return "GET_PURCHASE_INFORMATION";
+ }
+
+ @Override public boolean hasNonce() { return true; }
+
+ }
+
+ public static class RequestPurchase extends BillingRequest {
+
+ private String itemId;
+ private String developerPayload;
+
+ private static final String KEY_ITEM_ID = "ITEM_ID";
+ private static final String KEY_DEVELOPER_PAYLOAD = "DEVELOPER_PAYLOAD";
+ private static final String KEY_PURCHASE_INTENT = "PURCHASE_INTENT";
+
+ public RequestPurchase(String packageName, int startId, String itemId, String developerPayload) {
+ super(packageName, startId);
+ this.itemId = itemId;
+ this.developerPayload = developerPayload;
+ }
+
+ @Override
+ protected void addParams(Bundle request) {
+ request.putString(KEY_ITEM_ID, itemId);
+ if (developerPayload != null) {
+ request.putString(KEY_DEVELOPER_PAYLOAD, developerPayload);
+ }
+ }
+
+ @Override
+ public String getRequestType() {
+ return "REQUEST_PURCHASE";
+ }
+
+ @Override
+ public void onResponseCode(ResponseCode response) {
+ super.onResponseCode(response);
+ BillingController.onRequestPurchaseResponse(itemId, response);
+ }
+
+ @Override
+ protected void processOkResponse(Bundle response) {
+ final PendingIntent purchaseIntent = response.getParcelable(KEY_PURCHASE_INTENT);
+ BillingController.onPurchaseIntent(itemId, purchaseIntent);
+ }
+
+ }
+
+ public static class RequestSubscription extends RequestPurchase {
+
+ public RequestSubscription(String packageName, int startId, String itemId, String developerPayload) {
+ super(packageName, startId, itemId, developerPayload);
+ }
+
+ @Override
+ protected void addParams(Bundle request) {
+ super.addParams(request);
+ request.putString(KEY_ITEM_TYPE, ITEM_TYPE_SUBSCRIPTION);
+ }
+
+ @Override
+ protected int getAPIVersion() {
+ return 2;
+ }
+ }
+
+ public static enum ResponseCode {
+ RESULT_OK, // 0
+ RESULT_USER_CANCELED, // 1
+ RESULT_SERVICE_UNAVAILABLE, // 2
+ RESULT_BILLING_UNAVAILABLE, // 3
+ RESULT_ITEM_UNAVAILABLE, // 4
+ RESULT_DEVELOPER_ERROR, // 5
+ RESULT_ERROR; // 6
+
+ public static boolean isResponseOk(int response) {
+ return ResponseCode.RESULT_OK.ordinal() == response;
+ }
+
+ // Converts from an ordinal value to the ResponseCode
+ public static ResponseCode valueOf(int index) {
+ ResponseCode[] values = ResponseCode.values();
+ if (index < 0 || index >= values.length) {
+ return RESULT_ERROR;
+ }
+ return values[index];
+ }
+ }
+ public static class RestoreTransactions extends BillingRequest {
+
+ public RestoreTransactions(String packageName, int startId) {
+ super(packageName, startId);
+ }
+
+ @Override
+ public String getRequestType() {
+ return "RESTORE_TRANSACTIONS";
+ }
+
+ @Override public boolean hasNonce() { return true; }
+
+ @Override
+ public void onResponseCode(ResponseCode response) {
+ super.onResponseCode(response);
+ if (response == ResponseCode.RESULT_OK) {
+ BillingController.onTransactionsRestored();
+ }
+ }
+
+ }
+
+ public static final String ITEM_TYPE_SUBSCRIPTION = "subs";
+ private static final String KEY_API_VERSION = "API_VERSION";
+ private static final String KEY_BILLING_REQUEST = "BILLING_REQUEST";
+ private static final String KEY_ITEM_TYPE = "ITEM_TYPE";
+ private static final String KEY_NONCE = "NONCE";
+ private static final String KEY_PACKAGE_NAME = "PACKAGE_NAME";
+ protected static final String KEY_REQUEST_ID = "REQUEST_ID";
+ private static final String KEY_RESPONSE_CODE = "RESPONSE_CODE";
+ private static final String REQUEST_TYPE_CHECK_BILLING_SUPPORTED = "CHECK_BILLING_SUPPORTED";
+
+ public static final long IGNORE_REQUEST_ID = -1;
+ private String packageName;
+
+ private int startId;
+ private boolean success;
+ private long nonce;
+ public BillingRequest(String packageName,int startId) {
+ this.packageName = packageName;
+ this.startId=startId;
+ }
+
+ protected void addParams(Bundle request) {
+ // Do nothing by default
+ }
+
+ protected int getAPIVersion() {
+ return 1;
+ }
+
+ public long getNonce() {
+ return nonce;
+ }
+
+ public abstract String getRequestType();
+
+ public boolean hasNonce() {
+ return false;
+ }
+
+ public boolean isSuccess() {
+ return success;
+ }
+
+ protected Bundle makeRequestBundle() {
+ final Bundle request = new Bundle();
+ request.putString(KEY_BILLING_REQUEST, getRequestType());
+ request.putInt(KEY_API_VERSION, getAPIVersion());
+ request.putString(KEY_PACKAGE_NAME, packageName);
+ if (hasNonce()) {
+ request.putLong(KEY_NONCE, nonce);
+ }
+ return request;
+ }
+
+ public void onResponseCode(ResponseCode responde) {
+ // Do nothing by default
+ }
+
+ protected void processOkResponse(Bundle response) {
+ // Do nothing by default
+ }
+
+ public long run(IMarketBillingService mService) throws RemoteException {
+ final Bundle request = makeRequestBundle();
+ addParams(request);
+ final Bundle response;
+ try {
+ response = mService.sendBillingRequest(request);
+ } catch (NullPointerException e) {
+ Log.e(this.getClass().getSimpleName(), "Known IAB bug. See: http://code.google.com/p/marketbilling/issues/detail?id=25", e);
+ return IGNORE_REQUEST_ID;
+ }
+
+ if (validateResponse(response)) {
+ processOkResponse(response);
+ return response.getLong(KEY_REQUEST_ID, IGNORE_REQUEST_ID);
+ } else {
+ return IGNORE_REQUEST_ID;
+ }
+ }
+
+ public void setNonce(long nonce) {
+ this.nonce = nonce;
+ }
+
+ protected boolean validateResponse(Bundle response) {
+ final int responseCode = response.getInt(KEY_RESPONSE_CODE);
+ success = ResponseCode.isResponseOk(responseCode);
+ if (!success) {
+ Log.w(this.getClass().getSimpleName(), "Error with response code " + ResponseCode.valueOf(responseCode));
+ }
+ return success;
+ }
+
+ public int getStartId() {
+ return startId;
+ }
+
+}
View
289 Android/InAppBilling/net/robotmedia/billing/BillingService.java
@@ -0,0 +1,289 @@
+/* Copyright 2011 Robot Media SL (http://www.robotmedia.net)
+*
+* 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 net.robotmedia.billing;
+
+import java.util.LinkedList;
+
+import static net.robotmedia.billing.BillingRequest.*;
+
+import net.robotmedia.billing.utils.Compatibility;
+
+import com.android.vending.billing.IMarketBillingService;
+
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+public class BillingService extends Service implements ServiceConnection {
+
+ private static enum Action {
+ CHECK_BILLING_SUPPORTED, CHECK_SUBSCRIPTION_SUPPORTED, CONFIRM_NOTIFICATIONS, GET_PURCHASE_INFORMATION, REQUEST_PURCHASE, REQUEST_SUBSCRIPTION, RESTORE_TRANSACTIONS
+ }
+
+ private static final String ACTION_MARKET_BILLING_SERVICE = "com.android.vending.billing.MarketBillingService.BIND";
+ private static final String EXTRA_DEVELOPER_PAYLOAD = "DEVELOPER_PAYLOAD";
+ private static final String EXTRA_ITEM_ID = "ITEM_ID";
+ private static final String EXTRA_NONCE = "EXTRA_NONCE";
+ private static final String EXTRA_NOTIFY_IDS = "NOTIFY_IDS";
+ private static LinkedList<BillingRequest> mPendingRequests = new LinkedList<BillingRequest>();
+
+ private static IMarketBillingService mService;
+
+ public static void checkBillingSupported(Context context) {
+ final Intent intent = createIntent(context, Action.CHECK_BILLING_SUPPORTED);
+ context.startService(intent);
+ }
+
+ public static void checkSubscriptionSupported(Context context) {
+ final Intent intent = createIntent(context, Action.CHECK_SUBSCRIPTION_SUPPORTED);
+ context.startService(intent);
+ }
+
+ public static void confirmNotifications(Context context, String[] notifyIds) {
+ final Intent intent = createIntent(context, Action.CONFIRM_NOTIFICATIONS);
+ intent.putExtra(EXTRA_NOTIFY_IDS, notifyIds);
+ context.startService(intent);
+ }
+
+ private static Intent createIntent(Context context, Action action) {
+ final String actionString = getActionForIntent(context, action);
+ final Intent intent = new Intent(actionString);
+ intent.setClass(context, BillingService.class);
+ return intent;
+ }
+
+ private static final String getActionForIntent(Context context, Action action) {
+ return context.getPackageName() + "." + action.toString();
+ }
+
+ public static void getPurchaseInformation(Context context, String[] notifyIds, long nonce) {
+ final Intent intent = createIntent(context, Action.GET_PURCHASE_INFORMATION);
+ intent.putExtra(EXTRA_NOTIFY_IDS, notifyIds);
+ intent.putExtra(EXTRA_NONCE, nonce);
+ context.startService(intent);
+ }
+
+ public static void requestPurchase(Context context, String itemId, String developerPayload) {
+ final Intent intent = createIntent(context, Action.REQUEST_PURCHASE);
+ intent.putExtra(EXTRA_ITEM_ID, itemId);
+ intent.putExtra(EXTRA_DEVELOPER_PAYLOAD, developerPayload);
+ context.startService(intent);
+ }
+
+ public static void requestSubscription(Context context, String itemId, String developerPayload) {
+ final Intent intent = createIntent(context, Action.REQUEST_SUBSCRIPTION);
+ intent.putExtra(EXTRA_ITEM_ID, itemId);
+ intent.putExtra(EXTRA_DEVELOPER_PAYLOAD, developerPayload);
+ context.startService(intent);
+ }
+
+ public static void restoreTransations(Context context, long nonce) {
+ final Intent intent = createIntent(context, Action.RESTORE_TRANSACTIONS);
+ intent.setClass(context, BillingService.class);
+ intent.putExtra(EXTRA_NONCE, nonce);
+ context.startService(intent);
+ }
+
+ private void bindMarketBillingService() {
+ try {
+ final boolean bindResult = bindService(new Intent(ACTION_MARKET_BILLING_SERVICE), this, Context.BIND_AUTO_CREATE);
+ if (!bindResult) {
+ Log.e(this.getClass().getSimpleName(), "Could not bind to MarketBillingService");
+ }
+ } catch (SecurityException e) {
+ Log.e(this.getClass().getSimpleName(), "Could not bind to MarketBillingService", e);
+ }
+ }
+
+ private void checkBillingSupported(int startId) {
+ final String packageName = getPackageName();
+ final CheckBillingSupported request = new CheckBillingSupported(packageName, startId);
+ runRequestOrQueue(request);
+ }
+
+ private void checkSubscriptionSupported(int startId) {
+ final String packageName = getPackageName();
+ final CheckSubscriptionSupported request = new CheckSubscriptionSupported(packageName, startId);
+ runRequestOrQueue(request);
+ }
+
+ private void confirmNotifications(Intent intent, int startId) {
+ final String packageName = getPackageName();
+ final String[] notifyIds = intent.getStringArrayExtra(EXTRA_NOTIFY_IDS);
+ final ConfirmNotifications request = new ConfirmNotifications(packageName, startId, notifyIds);
+ runRequestOrQueue(request);
+ }
+
+ private Action getActionFromIntent(Intent intent) {
+ final String actionString = intent.getAction();
+ if (actionString == null) {
+ return null;
+ }
+ final String[] split = actionString.split("\\.");
+ if (split.length <= 0) {
+ return null;
+ }
+ return Action.valueOf(split[split.length - 1]);
+ }
+
+ private void getPurchaseInformation(Intent intent, int startId) {
+ final String packageName = getPackageName();
+ final long nonce = intent.getLongExtra(EXTRA_NONCE, 0);
+ final String[] notifyIds = intent.getStringArrayExtra(EXTRA_NOTIFY_IDS);
+ final GetPurchaseInformation request = new GetPurchaseInformation(packageName, startId, notifyIds);
+ request.setNonce(nonce);
+ runRequestOrQueue(request);
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ mService = IMarketBillingService.Stub.asInterface(service);
+ runPendingRequests();
+ }
+
+ public void onServiceDisconnected(ComponentName name) {
+ mService = null;
+ }
+
+ // This is the old onStart method that will be called on the pre-2.0
+ // platform. On 2.0 or later we override onStartCommand() so this
+ // method will not be called.
+ @Override
+ public void onStart(Intent intent, int startId) {
+ handleCommand(intent, startId);
+ }
+
+ // @Override // Avoid compile errors on pre-2.0
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ handleCommand(intent, startId);
+ return Compatibility.START_NOT_STICKY;
+ }
+
+ private void handleCommand(Intent intent, int startId) {
+ final Action action = getActionFromIntent(intent);
+ if (action == null) {
+ return;
+ }
+ switch (action) {
+ case CHECK_BILLING_SUPPORTED:
+ checkBillingSupported(startId);
+ break;
+ case CHECK_SUBSCRIPTION_SUPPORTED:
+ checkSubscriptionSupported(startId);
+ break;
+ case REQUEST_PURCHASE:
+ requestPurchase(intent, startId);
+ break;
+ case REQUEST_SUBSCRIPTION:
+ requestSubscription(intent, startId);
+ break;
+ case GET_PURCHASE_INFORMATION:
+ getPurchaseInformation(intent, startId);
+ break;
+ case CONFIRM_NOTIFICATIONS:
+ confirmNotifications(intent, startId);
+ break;
+ case RESTORE_TRANSACTIONS:
+ restoreTransactions(intent, startId);
+ }
+ }
+
+ private void requestPurchase(Intent intent, int startId) {
+ final String packageName = getPackageName();
+ final String itemId = intent.getStringExtra(EXTRA_ITEM_ID);
+ final String developerPayload = intent.getStringExtra(EXTRA_DEVELOPER_PAYLOAD);
+ final RequestPurchase request = new RequestPurchase(packageName, startId, itemId, developerPayload);
+ runRequestOrQueue(request);
+ }
+
+ private void requestSubscription(Intent intent, int startId) {
+ final String packageName = getPackageName();
+ final String itemId = intent.getStringExtra(EXTRA_ITEM_ID);
+ final String developerPayload = intent.getStringExtra(EXTRA_DEVELOPER_PAYLOAD);
+ final RequestPurchase request = new RequestSubscription(packageName, startId, itemId, developerPayload);
+ runRequestOrQueue(request);
+ }
+
+ private void restoreTransactions(Intent intent, int startId) {
+ final String packageName = getPackageName();
+ final long nonce = intent.getLongExtra(EXTRA_NONCE, 0);
+ final RestoreTransactions request = new RestoreTransactions(packageName, startId);
+ request.setNonce(nonce);
+ runRequestOrQueue(request);
+ }
+
+ private void runPendingRequests() {
+ BillingRequest request;
+ int maxStartId = -1;
+ while ((request = mPendingRequests.peek()) != null) {
+ if (mService != null) {
+ runRequest(request);
+ mPendingRequests.remove();
+ if (maxStartId < request.getStartId()) {
+ maxStartId = request.getStartId();
+ }
+ } else {
+ bindMarketBillingService();
+ return;
+ }
+ }
+ if (maxStartId >= 0) {
+ stopSelf(maxStartId);
+ }
+ }
+
+ private void runRequest(BillingRequest request) {
+ try {
+ final long requestId = request.run(mService);
+ BillingController.onRequestSent(requestId, request);
+ } catch (RemoteException e) {
+ Log.w(this.getClass().getSimpleName(), "Remote billing service crashed");
+ // TODO: Retry?
+ }
+ }
+
+ private void runRequestOrQueue(BillingRequest request) {
+ mPendingRequests.add(request);
+ if (mService == null) {
+ bindMarketBillingService();
+ } else {
+ runPendingRequests();
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ // Ensure we're not leaking Android Market billing service
+ if (mService != null) {
+ try {
+ unbindService(this);
+ } catch (IllegalArgumentException e) {
+ // This might happen if the service was disconnected
+ }
+ }
+ }
+
+}
View
85 Android/InAppBilling/net/robotmedia/billing/IBillingObserver.java
@@ -0,0 +1,85 @@
+/* Copyright 2011 Robot Media SL (http://www.robotmedia.net)
+ *
+ * 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 net.robotmedia.billing;
+
+import net.robotmedia.billing.BillingRequest.ResponseCode;
+import net.robotmedia.billing.model.Transaction.PurchaseState;
+import android.app.PendingIntent;
+
+public interface IBillingObserver {
+
+ /**
+ * Called after checking if in-app product billing is supported or not.
+ *
+ * @param supported
+ * if true, in-app product billing is supported. If false, in-app
+ * product billing is not supported, and neither is subscription
+ * billing.
+ * @see BillingController#checkBillingSupported(android.content.Context)
+ */
+ public void onBillingChecked(boolean supported);
+
+ /**
+ * Called after checking if subscription billing is supported or not.
+ *
+ * @param supported
+ * if true, subscription billing is supported, and also is in-app
+ * product billing. Otherwise, subscription billing is not
+ * supported.
+ */
+ public void onSubscriptionChecked(boolean supported);
+
+ /**
+ * Called after requesting the purchase of the specified item.
+ *
+ * @param itemId
+ * id of the item whose purchase was requested.
+ * @param purchaseIntent
+ * a purchase pending intent for the specified item.
+ * @see BillingController#requestPurchase(android.content.Context, String,
+ * boolean)
+ */
+ public void onPurchaseIntent(String itemId, PendingIntent purchaseIntent);
+
+ /**
+ * Called when the specified item is purchased, cancelled or refunded.
+ *
+ * @param itemId
+ * id of the item whose purchase state has changed.
+ * @param state
+ * purchase state of the specified item.
+ */
+ public void onPurchaseStateChanged(String itemId, PurchaseState state);
+
+ /**
+ * Called with the response for the purchase request of the specified item.
+ * This is used for reporting various errors, or if the user backed out and
+ * didn't purchase the item.
+ *
+ * @param itemId
+ * id of the item whose purchase was requested
+ * @param response
+ * response of the purchase request
+ */
+ public void onRequestPurchaseResponse(String itemId, ResponseCode response);
+
+ /**
+ * Called when a restore transactions request has been successfully
+ * received by the server.
+ */
+ public void onTransactionsRestored();
+
+}
View
160 Android/InAppBilling/net/robotmedia/billing/helper/AbstractBillingActivity.java
@@ -0,0 +1,160 @@
+/* Copyright 2011 Robot Media SL (http://www.robotmedia.net)
+ *
+ * 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 net.robotmedia.billing.helper;
+
+import net.robotmedia.billing.BillingController;
+import net.robotmedia.billing.BillingController.BillingStatus;
+import net.robotmedia.billing.BillingRequest.ResponseCode;
+import net.robotmedia.billing.model.Transaction.PurchaseState;
+import android.app.Activity;
+
+public abstract class AbstractBillingActivity extends Activity implements BillingController.IConfiguration {
+
+ protected AbstractBillingObserver mBillingObserver;
+
+ /**
+ * <p>
+ * Returns the in-app product billing support status, and checks it
+ * asynchronously if it is currently unknown.
+ * {@link AbstractBillingActivity#onBillingChecked(boolean)} will be called
+ * eventually with the result.
+ * </p>
+ * <p>
+ * In-app product support does not imply subscription support. To check if
+ * subscriptions are supported, use
+ * {@link AbstractBillingActivity#checkSubscriptionSupported()}.
+ * </p>
+ *
+ * @return the current in-app product billing support status (unknown,
+ * supported or unsupported). If it is unsupported, subscriptions
+ * are also unsupported.
+ * @see AbstractBillingActivity#onBillingChecked(boolean)
+ * @see AbstractBillingActivity#checkSubscriptionSupported()
+ */
+ public BillingStatus checkBillingSupported() {
+ return BillingController.checkBillingSupported(this);
+ }
+
+ /**
+ * <p>
+ * Returns the subscription billing support status, and checks it
+ * asynchronously if it is currently unknown.
+ * {@link AbstractBillingActivity#onSubscriptionChecked(boolean)} will be
+ * called eventually with the result.
+ * </p>
+ * <p>
+ * No support for subscriptions does not imply that in-app products are also
+ * unsupported. To check if subscriptions are supported, use
+ * {@link AbstractBillingActivity#checkSubscriptionSupported()}.
+ * </p>
+ *
+ * @return the current in-app product billing support status (unknown,
+ * supported or unsupported). If it is unsupported, subscriptions
+ * are also unsupported.
+ * @see AbstractBillingActivity#onBillingChecked(boolean)
+ * @see AbstractBillingActivity#checkSubscriptionSupported()
+ */
+ public BillingStatus checkSubscriptionSupported() {
+ return BillingController.checkSubscriptionSupported(this);
+ }
+
+ public abstract void onBillingChecked(boolean supported);
+
+ public abstract void onSubscriptionChecked(boolean supported);
+
+ @Override
+ protected void onCreate(android.os.Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mBillingObserver = new AbstractBillingObserver(this) {
+
+ public void onBillingChecked(boolean supported) {
+ AbstractBillingActivity.this.onBillingChecked(supported);
+ }
+
+ public void onSubscriptionChecked(boolean supported) {
+ AbstractBillingActivity.this.onSubscriptionChecked(supported);
+ }
+
+ public void onPurchaseStateChanged(String itemId, PurchaseState state) {
+ AbstractBillingActivity.this.onPurchaseStateChanged(itemId, state);
+ }
+
+ public void onRequestPurchaseResponse(String itemId, ResponseCode response) {
+ AbstractBillingActivity.this.onRequestPurchaseResponse(itemId, response);
+ }
+ };
+ BillingController.registerObserver(mBillingObserver);
+ BillingController.setConfiguration(this); // This activity will provide
+ // the public key and salt
+ this.checkBillingSupported();
+ if (!mBillingObserver.isTransactionsRestored()) {
+ BillingController.restoreTransactions(this);
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ BillingController.unregisterObserver(mBillingObserver); // Avoid
+ // receiving
+ // notifications after
+ // destroy
+ BillingController.setConfiguration(null);
+ }
+
+ public abstract void onPurchaseStateChanged(String itemId, PurchaseState state);;
+
+ public abstract void onRequestPurchaseResponse(String itemId, ResponseCode response);
+
+ /**
+ * Requests the purchase of the specified item. The transaction will not be
+ * confirmed automatically; such confirmation could be handled in
+ * {@link AbstractBillingActivity#onPurchaseExecuted(String)}. If automatic
+ * confirmation is preferred use
+ * {@link BillingController#requestPurchase(android.content.Context, String, boolean)}
+ * instead.
+ *
+ * @param itemId
+ * id of the item to be purchased.
+ */
+ public void requestPurchase(String itemId) {
+ BillingController.requestPurchase(this, itemId);
+ }
+
+ /**
+ * Requests the purchase of the specified subscription item. The transaction
+ * will not be confirmed automatically; such confirmation could be handled
+ * in {@link AbstractBillingActivity#onPurchaseExecuted(String)}. If
+ * automatic confirmation is preferred use
+ * {@link BillingController#requestPurchase(android.content.Context, String, boolean)}
+ * instead.
+ *
+ * @param itemId
+ * id of the item to be purchased.
+ */
+ public void requestSubscription(String itemId) {
+ BillingController.requestSubscription(this, itemId);
+ }
+
+ /**
+ * Requests to restore all transactions.
+ */
+ public void restoreTransactions() {
+ BillingController.restoreTransactions(this);
+ }
+
+}
View
147 Android/InAppBilling/net/robotmedia/billing/helper/AbstractBillingFragment.java
@@ -0,0 +1,147 @@
+package net.robotmedia.billing.helper;
+
+import net.robotmedia.billing.BillingController;
+import net.robotmedia.billing.BillingController.BillingStatus;
+import net.robotmedia.billing.BillingRequest.ResponseCode;
+import net.robotmedia.billing.model.Transaction.PurchaseState;
+import android.annotation.TargetApi;
+import android.app.Fragment;
+
+@TargetApi(11)
+public abstract class AbstractBillingFragment extends Fragment implements BillingController.IConfiguration {
+
+ protected AbstractBillingObserver mBillingObserver;
+
+ /**
+ * <p>
+ * Returns the in-app product billing support status, and checks it
+ * asynchronously if it is currently unknown.
+ * {@link AbstractBillingActivity#onBillingChecked(boolean)} will be called
+ * eventually with the result.
+ * </p>
+ * <p>
+ * In-app product support does not imply subscription support. To check if
+ * subscriptions are supported, use
+ * {@link AbstractBillingActivity#checkSubscriptionSupported()}.
+ * </p>
+ *
+ * @return the current in-app product billing support status (unknown,
+ * supported or unsupported). If it is unsupported, subscriptions
+ * are also unsupported.
+ * @see AbstractBillingActivity#onBillingChecked(boolean)
+ * @see AbstractBillingActivity#checkSubscriptionSupported()
+ */
+ public BillingStatus checkBillingSupported() {
+ return BillingController.checkBillingSupported(getActivity());
+ }
+
+ /**
+ * <p>
+ * Returns the subscription billing support status, and checks it
+ * asynchronously if it is currently unknown.
+ * {@link AbstractBillingActivity#onSubscriptionChecked(boolean)} will be
+ * called eventually with the result.
+ * </p>
+ * <p>
+ * No support for subscriptions does not imply that in-app products are also
+ * unsupported. To check if subscriptions are supported, use
+ * {@link AbstractBillingActivity#checkSubscriptionSupported()}.
+ * </p>
+ *
+ * @return the current in-app product billing support status (unknown,
+ * supported or unsupported). If it is unsupported, subscriptions
+ * are also unsupported.
+ * @see AbstractBillingActivity#onBillingChecked(boolean)
+ * @see AbstractBillingActivity#checkSubscriptionSupported()
+ */
+ public BillingStatus checkSubscriptionSupported() {
+ return BillingController.checkSubscriptionSupported(getActivity());
+ }
+
+ public abstract void onBillingChecked(boolean supported);
+
+ public abstract void onSubscriptionChecked(boolean supported);
+
+ @Override
+ public void onCreate(android.os.Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mBillingObserver = new AbstractBillingObserver(getActivity()) {
+
+ public void onBillingChecked(boolean supported) {
+ AbstractBillingFragment.this.onBillingChecked(supported);
+ }
+
+ public void onSubscriptionChecked(boolean supported) {
+ AbstractBillingFragment.this.onSubscriptionChecked(supported);
+ }
+
+ public void onPurchaseStateChanged(String itemId, PurchaseState state) {
+ AbstractBillingFragment.this.onPurchaseStateChanged(itemId, state);
+ }
+
+ public void onRequestPurchaseResponse(String itemId, ResponseCode response) {
+ AbstractBillingFragment.this.onRequestPurchaseResponse(itemId, response);
+ }
+ };
+ BillingController.registerObserver(mBillingObserver);
+ BillingController.setConfiguration(this); // This fragment will provide
+ // the public key and salt
+ this.checkBillingSupported();
+ if (!mBillingObserver.isTransactionsRestored()) {
+ BillingController.restoreTransactions(getActivity());
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ BillingController.unregisterObserver(mBillingObserver); // Avoid
+ // receiving
+ // notifications
+ // after destroy
+ BillingController.setConfiguration(null);
+ }
+
+ public abstract void onPurchaseStateChanged(String itemId, PurchaseState state);;
+
+ public abstract void onRequestPurchaseResponse(String itemId, ResponseCode response);
+
+ /**
+ * Requests the purchase of the specified item. The transaction will not be
+ * confirmed automatically; such confirmation could be handled in
+ * {@link AbstractBillingActivity#onPurchaseExecuted(String)}. If automatic
+ * confirmation is preferred use
+ * {@link BillingController#requestPurchase(android.content.Context, String, boolean)}
+ * instead.
+ *
+ * @param itemId
+ * id of the item to be purchased.
+ */
+ public void requestPurchase(String itemId) {
+ BillingController.requestPurchase(getActivity(), itemId);
+ }
+
+ /**
+ * Requests the purchase of the specified subscription item. The transaction
+ * will not be confirmed automatically; such confirmation could be handled
+ * in {@link AbstractBillingActivity#onPurchaseExecuted(String)}. If
+ * automatic confirmation is preferred use
+ * {@link BillingController#requestPurchase(android.content.Context, String, boolean)}
+ * instead.
+ *
+ * @param itemId
+ * id of the item to be purchased.
+ */
+ public void requestSubscription(String itemId) {
+ BillingController.requestSubscription(getActivity(), itemId);
+ }
+
+ /**
+ * Requests to restore all transactions.
+ */
+ public void restoreTransactions() {
+ BillingController.restoreTransactions(getActivity());
+ }
+
+}
View
67 Android/InAppBilling/net/robotmedia/billing/helper/AbstractBillingObserver.java
@@ -0,0 +1,67 @@
+/* Copyright 2011 Robot Media SL (http://www.robotmedia.net)
+ *
+ * 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 net.robotmedia.billing.helper;
+
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.preference.PreferenceManager;
+import net.robotmedia.billing.BillingController;
+import net.robotmedia.billing.IBillingObserver;
+
+/**
+ * Abstract subclass of IBillingObserver that provides default implementations
+ * for {@link IBillingObserver#onPurchaseIntent(String, PendingIntent)} and
+ * {@link IBillingObserver#onTransactionsRestored()}.
+ *
+ */
+public abstract class AbstractBillingObserver implements IBillingObserver {
+
+ protected static final String KEY_TRANSACTIONS_RESTORED = "net.robotmedia.billing.transactionsRestored";
+
+ protected Activity activity;
+
+ public AbstractBillingObserver(Activity activity) {
+ this.activity = activity;
+ }
+
+ public boolean isTransactionsRestored() {
+ final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity);
+ return preferences.getBoolean(KEY_TRANSACTIONS_RESTORED, false);
+ }
+
+ /**
+ * Called after requesting the purchase of the specified item. The default
+ * implementation simply starts the pending intent.
+ *
+ * @param itemId
+ * id of the item whose purchase was requested.
+ * @param purchaseIntent
+ * a purchase pending intent for the specified item.
+ */
+ public void onPurchaseIntent(String itemId, PendingIntent purchaseIntent) {
+ BillingController.startPurchaseIntent(activity, purchaseIntent, null);
+ }
+
+ public void onTransactionsRestored() {
+ final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity);
+ final Editor editor = preferences.edit();
+ editor.putBoolean(KEY_TRANSACTIONS_RESTORED, true);
+ editor.commit();
+ }
+
+}
View
110 Android/InAppBilling/net/robotmedia/billing/model/BillingDB.java
@@ -0,0 +1,110 @@
+/* Copyright 2011 Robot Media SL (http://www.robotmedia.net)
+*
+* 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 net.robotmedia.billing.model;
+
+import net.robotmedia.billing.model.Transaction.PurchaseState;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+
+public class BillingDB {
+ static final String DATABASE_NAME = "billing.db";
+ static final int DATABASE_VERSION = 1;
+ static final String TABLE_TRANSACTIONS = "purchases";
+
+ public static final String COLUMN__ID = "_id";
+ public static final String COLUMN_STATE = "state";
+ public static final String COLUMN_PRODUCT_ID = "productId";
+ public static final String COLUMN_PURCHASE_TIME = "purchaseTime";
+ public static final String COLUMN_DEVELOPER_PAYLOAD = "developerPayload";
+
+ private static final String[] TABLE_TRANSACTIONS_COLUMNS = {
+ COLUMN__ID, COLUMN_PRODUCT_ID, COLUMN_STATE,
+ COLUMN_PURCHASE_TIME, COLUMN_DEVELOPER_PAYLOAD
+ };
+
+ SQLiteDatabase mDb;
+ private DatabaseHelper mDatabaseHelper;
+
+ public BillingDB(Context context) {
+ mDatabaseHelper = new DatabaseHelper(context);
+ mDb = mDatabaseHelper.getWritableDatabase();
+ }
+
+ public void close() {
+ mDatabaseHelper.close();
+ }
+
+ public void insert(Transaction transaction) {
+ ContentValues values = new ContentValues();
+ values.put(COLUMN__ID, transaction.orderId);
+ values.put(COLUMN_PRODUCT_ID, transaction.productId);
+ values.put(COLUMN_STATE, transaction.purchaseState.ordinal());
+ values.put(COLUMN_PURCHASE_TIME, transaction.purchaseTime);
+ values.put(COLUMN_DEVELOPER_PAYLOAD, transaction.developerPayload);
+ mDb.replace(TABLE_TRANSACTIONS, null /* nullColumnHack */, values);
+ }
+
+ public Cursor queryTransactions() {
+ return mDb.query(TABLE_TRANSACTIONS, TABLE_TRANSACTIONS_COLUMNS, null,
+ null, null, null, null);
+ }
+
+ public Cursor queryTransactions(String productId) {
+ return mDb.query(TABLE_TRANSACTIONS, TABLE_TRANSACTIONS_COLUMNS, COLUMN_PRODUCT_ID + " = ?",
+ new String[] {productId}, null, null, null);
+ }
+
+ public Cursor queryTransactions(String productId, PurchaseState state) {
+ return mDb.query(TABLE_TRANSACTIONS, TABLE_TRANSACTIONS_COLUMNS, COLUMN_PRODUCT_ID + " = ? AND " + COLUMN_STATE + " = ?",
+ new String[] {productId, String.valueOf(state.ordinal())}, null, null, null);
+ }
+
+ protected static final Transaction createTransaction(Cursor cursor) {
+ final Transaction purchase = new Transaction();
+ purchase.orderId = cursor.getString(0);
+ purchase.productId = cursor.getString(1);
+ purchase.purchaseState = PurchaseState.valueOf(cursor.getInt(2));
+ purchase.purchaseTime = cursor.getLong(3);
+ purchase.developerPayload = cursor.getString(4);
+ return purchase;
+ }
+
+ private class DatabaseHelper extends SQLiteOpenHelper {
+ public DatabaseHelper(Context context) {
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ createTransactionsTable(db);
+ }
+
+ private void createTransactionsTable(SQLiteDatabase db) {
+ db.execSQL("CREATE TABLE " + TABLE_TRANSACTIONS + "(" +
+ COLUMN__ID + " TEXT PRIMARY KEY, " +
+ COLUMN_PRODUCT_ID + " INTEGER, " +
+ COLUMN_STATE + " TEXT, " +
+ COLUMN_PURCHASE_TIME + " TEXT, " +
+ COLUMN_DEVELOPER_PAYLOAD + " INTEGER)");
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {}
+ }
+}
View
135 Android/InAppBilling/net/robotmedia/billing/model/Transaction.java
@@ -0,0 +1,135 @@
+/* Copyright 2011 Robot Media SL (http://www.robotmedia.net)
+*
+* 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 net.robotmedia.billing.model;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+public class Transaction {
+
+ public enum PurchaseState {
+ // Responses to requestPurchase or restoreTransactions.
+ PURCHASED, // 0: User was charged for the order.
+ CANCELLED, // 1: The charge failed on the server.
+ REFUNDED, // 2: User received a refund for the order.
+ EXPIRED; // 3: Sent at the end of a billing cycle to indicate that the
+ // subscription expired without renewal because of
+ // non-payment or user-cancellation. Your app does not need
+ // to grant continued access to the subscription content.
+
+ // Converts from an ordinal value to the PurchaseState
+ public static PurchaseState valueOf(int index) {
+ PurchaseState[] values = PurchaseState.values();
+ if (index < 0 || index >= values.length) {
+ return CANCELLED;
+ }
+ return values[index];
+ }
+ }
+ static final String DEVELOPER_PAYLOAD = "developerPayload";
+ static final String NOTIFICATION_ID = "notificationId";
+ static final String ORDER_ID = "orderId";
+ static final String PACKAGE_NAME = "packageName";
+ static final String PRODUCT_ID = "productId";
+ static final String PURCHASE_STATE = "purchaseState";
+
+ static final String PURCHASE_TIME = "purchaseTime";
+
+ public static Transaction parse(JSONObject json) throws JSONException {
+ final Transaction transaction = new Transaction();
+ final int response = json.getInt(PURCHASE_STATE);
+ transaction.purchaseState = PurchaseState.valueOf(response);
+ transaction.productId = json.getString(PRODUCT_ID);
+ transaction.packageName = json.getString(PACKAGE_NAME);
+ transaction.purchaseTime = json.getLong(PURCHASE_TIME);
+ transaction.orderId = json.optString(ORDER_ID, null);
+ transaction.notificationId = json.optString(NOTIFICATION_ID, null);