Merge pull request #782 from poiuytrez/billingPlugin

[Android] Google Play In App Billing plugin
This commit is contained in:
tommy-carlos williams
2012-11-26 02:37:30 -08:00
23 changed files with 3743 additions and 0 deletions

View File

@@ -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 File

@@ -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 File

@@ -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 File

@@ -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 File

@@ -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 File

@@ -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 File

@@ -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 File

@@ -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 File

@@ -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 File

@@ -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 File

@@ -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 File

@@ -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 File

@@ -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);
transaction.developerPayload = json.optString(DEVELOPER_PAYLOAD, null);
return transaction;
}
public String developerPayload;
public String notificationId;
public String orderId;
public String packageName;
public String productId;
public PurchaseState purchaseState;
public long purchaseTime;
public Transaction() {}
public Transaction(String orderId, String productId, String packageName, PurchaseState purchaseState,
String notificationId, long purchaseTime, String developerPayload) {
this.orderId = orderId;
this.productId = productId;
this.packageName = packageName;
this.purchaseState = purchaseState;
this.notificationId = notificationId;
this.purchaseTime = purchaseTime;
this.developerPayload = developerPayload;
}
public Transaction clone() {
return new Transaction(orderId, productId, packageName, purchaseState, notificationId, purchaseTime, developerPayload);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Transaction other = (Transaction) obj;
if (developerPayload == null) {
if (other.developerPayload != null)
return false;
} else if (!developerPayload.equals(other.developerPayload))
return false;
if (notificationId == null) {
if (other.notificationId != null)
return false;
} else if (!notificationId.equals(other.notificationId))
return false;
if (orderId == null) {
if (other.orderId != null)
return false;
} else if (!orderId.equals(other.orderId))
return false;
if (packageName == null) {
if (other.packageName != null)
return false;
} else if (!packageName.equals(other.packageName))
return false;
if (productId == null) {
if (other.productId != null)
return false;
} else if (!productId.equals(other.productId))
return false;
if (purchaseState != other.purchaseState)
return false;
if (purchaseTime != other.purchaseTime)
return false;
return true;
}
@Override
public String toString() {
return String.valueOf(orderId);
}
}

View File

@@ -0,0 +1,77 @@
/* 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 java.util.ArrayList;
import java.util.List;
import net.robotmedia.billing.model.Transaction.PurchaseState;
import android.content.Context;
import android.database.Cursor;
public class TransactionManager {
public synchronized static void addTransaction(Context context, Transaction transaction) {
BillingDB db = new BillingDB(context);
db.insert(transaction);
db.close();
}
public synchronized static boolean isPurchased(Context context, String itemId) {
return countPurchases(context, itemId) > 0;
}
public synchronized static int countPurchases(Context context, String itemId) {
BillingDB db = new BillingDB(context);
final Cursor c = db.queryTransactions(itemId, PurchaseState.PURCHASED);
int count = 0;
if (c != null) {
count = c.getCount();
c.close();
}
db.close();
return count;
}
public synchronized static List<Transaction> getTransactions(Context context) {
BillingDB db = new BillingDB(context);
final Cursor c = db.queryTransactions();
final List<Transaction> transactions = cursorToList(c);
db.close();
return transactions;
}
private static List<Transaction> cursorToList(final Cursor c) {
final List<Transaction> transactions = new ArrayList<Transaction>();
if (c != null) {
while (c.moveToNext()) {
final Transaction purchase = BillingDB.createTransaction(c);
transactions.add(purchase);
}
c.close();
}
return transactions;
}
public synchronized static List<Transaction> getTransactions(Context context, String itemId) {
BillingDB db = new BillingDB(context);
final Cursor c = db.queryTransactions(itemId);
final List<Transaction> transactions = cursorToList(c);
db.close();
return transactions;
}
}

View File

@@ -0,0 +1,106 @@
/* 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.security;
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;
import android.text.TextUtils;
import android.util.Log;
import net.robotmedia.billing.BillingController;
import net.robotmedia.billing.utils.Base64;
import net.robotmedia.billing.utils.Base64DecoderException;
public class DefaultSignatureValidator implements ISignatureValidator {
protected static final String KEY_FACTORY_ALGORITHM = "RSA";
protected static final String SIGNATURE_ALGORITHM = "SHA1withRSA";
/**
* 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
*/
protected 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(BillingController.LOG_TAG, "Invalid key specification.");
throw new IllegalArgumentException(e);
} catch (Base64DecoderException e) {
Log.e(BillingController.LOG_TAG, "Base64 decoding failed.");
throw new IllegalArgumentException(e);
}
}
private BillingController.IConfiguration configuration;
public DefaultSignatureValidator(BillingController.IConfiguration configuration) {
this.configuration = configuration;
}
protected boolean validate(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(BillingController.LOG_TAG, "Signature verification failed.");
return false;
}
return true;
} catch (NoSuchAlgorithmException e) {
Log.e(BillingController.LOG_TAG, "NoSuchAlgorithmException");
} catch (InvalidKeyException e) {
Log.e(BillingController.LOG_TAG, "Invalid key specification");
} catch (SignatureException e) {
Log.e(BillingController.LOG_TAG, "Signature exception");
} catch (Base64DecoderException e) {
Log.e(BillingController.LOG_TAG, "Base64 decoding failed");
}
return false;
}
public boolean validate(String signedData, String signature) {
final String publicKey;
if (configuration == null || TextUtils.isEmpty(publicKey = configuration.getPublicKey())) {
Log.w(BillingController.LOG_TAG, "Please set the public key or turn on debug mode");
return false;
}
if (signedData == null) {
Log.e(BillingController.LOG_TAG, "Data is null");
return false;
}
PublicKey key = generatePublicKey(publicKey);
return validate(key, signedData, signature);
}
}

View File

@@ -0,0 +1,32 @@
/* 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.security;
public interface ISignatureValidator {
/**
* Validates that the specified signature matches the computed signature on
* the specified signed data. Returns true if the data is correctly signed.
*
* @param signedData
* signed data
* @param signature
* signature
* @return true if the data and signature match, false otherwise.
*/
public boolean validate(String signedData, String signature);
}

View File

@@ -0,0 +1,114 @@
/*
* 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 net.robotmedia.billing.utils;
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import java.security.spec.KeySpec;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
/**
* An obfuscator that uses AES to encrypt data.
*/
public class AESObfuscator {
private static final String UTF8 = "UTF-8";
private static final String KEYGEN_ALGORITHM = "PBEWITHSHAAND256BITAES-CBC-BC";
private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding";
private static final byte[] IV =
{ 16, 74, 71, -80, 32, 101, -47, 72, 117, -14, 0, -29, 70, 65, -12, 74 };
private static final String header = "net.robotmedia.billing.utils.AESObfuscator-1|";
private Cipher mEncryptor;
private Cipher mDecryptor;
public AESObfuscator(byte[] salt, String password) {
try {
SecretKeyFactory factory = SecretKeyFactory.getInstance(KEYGEN_ALGORITHM);
KeySpec keySpec =
new PBEKeySpec(password.toCharArray(), salt, 1024, 256);
SecretKey tmp = factory.generateSecret(keySpec);
SecretKey secret = new SecretKeySpec(tmp.getEncoded(), "AES");
mEncryptor = Cipher.getInstance(CIPHER_ALGORITHM);
mEncryptor.init(Cipher.ENCRYPT_MODE, secret, new IvParameterSpec(IV));
mDecryptor = Cipher.getInstance(CIPHER_ALGORITHM);
mDecryptor.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(IV));
} catch (GeneralSecurityException e) {
// This can't happen on a compatible Android device.
throw new RuntimeException("Invalid environment", e);
}
}
public String obfuscate(String original) {
if (original == null) {
return null;
}
try {
// Header is appended as an integrity check
return Base64.encode(mEncryptor.doFinal((header + original).getBytes(UTF8)));
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("Invalid environment", e);
} catch (GeneralSecurityException e) {
throw new RuntimeException("Invalid environment", e);
}
}
public String unobfuscate(String obfuscated) throws ValidationException {
if (obfuscated == null) {
return null;
}
try {
String result = new String(mDecryptor.doFinal(Base64.decode(obfuscated)), UTF8);
// Check for presence of header. This serves as a final integrity check, for cases
// where the block size is correct during decryption.
int headerIndex = result.indexOf(header);
if (headerIndex != 0) {
throw new ValidationException("Header not found (invalid data or key)" + ":" +
obfuscated);
}
return result.substring(header.length(), result.length());
} catch (Base64DecoderException e) {
throw new ValidationException(e.getMessage() + ":" + obfuscated);
} catch (IllegalBlockSizeException e) {
throw new ValidationException(e.getMessage() + ":" + obfuscated);
} catch (BadPaddingException e) {
throw new ValidationException(e.getMessage() + ":" + obfuscated);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("Invalid environment", e);
}
}
public class ValidationException extends Exception {
public ValidationException() {
super();
}
public ValidationException(String s) {
super(s);
}
private static final long serialVersionUID = 1L;
}
}

View File

@@ -0,0 +1,570 @@
// Portions copyright 2002, Google, Inc.
//
// 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.utils;
// 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;
}
}

View File

@@ -0,0 +1,32 @@
// Copyright 2002, Google, Inc.
//
// 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.utils;
/**
* 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;
}

View File

@@ -0,0 +1,75 @@
/* 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.utils;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import android.app.Activity;
import android.app.Service;
import android.content.Intent;
import android.content.IntentSender;
import android.util.Log;
public class Compatibility {
private static Method startIntentSender;
public static int START_NOT_STICKY;
@SuppressWarnings("rawtypes")
private static final Class[] START_INTENT_SENDER_SIG = new Class[] {
IntentSender.class, Intent.class, int.class, int.class, int.class
};
static {
initCompatibility();
};
private static void initCompatibility() {
try {
final Field field = Service.class.getField("START_NOT_STICKY");
START_NOT_STICKY = field.getInt(null);
} catch (Exception e) {
START_NOT_STICKY = 2;
}
try {
startIntentSender = Activity.class.getMethod("startIntentSender",
START_INTENT_SENDER_SIG);
} catch (SecurityException e) {
startIntentSender = null;
} catch (NoSuchMethodException e) {
startIntentSender = null;
}
}
public static void startIntentSender(Activity activity, IntentSender intentSender, Intent intent) {
if (startIntentSender != null) {
final Object[] args = new Object[5];
args[0] = intentSender;
args[1] = intent;
args[2] = Integer.valueOf(0);
args[3] = Integer.valueOf(0);
args[4] = Integer.valueOf(0);
try {
startIntentSender.invoke(activity, args);
} catch (Exception e) {
Log.e(Compatibility.class.getSimpleName(), "startIntentSender", e);
}
}
}
public static boolean isStartIntentSenderSupported() {
return startIntentSender != null;
}
}

View File

@@ -0,0 +1,59 @@
/* 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.utils;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.UUID;
import android.content.Context;
public class Installation {
private static final String INSTALLATION = "INSTALLATION";
private static String sID = null;
public synchronized static String id(Context context) {
if (sID == null) {
File installation = new File(context.getFilesDir(), INSTALLATION);
try {
if (!installation.exists()) {
writeInstallationFile(installation);
}
sID = readInstallationFile(installation);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
return sID;
}
private static String readInstallationFile(File installation) throws IOException {
RandomAccessFile f = new RandomAccessFile(installation, "r");
byte[] bytes = new byte[(int) f.length()];
f.readFully(bytes);
f.close();
return new String(bytes);
}
private static void writeInstallationFile(File installation) throws IOException {
FileOutputStream out = new FileOutputStream(installation);
String id = UUID.randomUUID().toString();
out.write(id.getBytes());
out.close();
}
}

View File

@@ -0,0 +1,75 @@
/* 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.utils;
import java.security.SecureRandom;
import java.util.HashSet;
import net.robotmedia.billing.utils.AESObfuscator.ValidationException;
import android.content.Context;
import android.provider.Settings;
import android.util.Log;
public class Security {
private static HashSet<Long> knownNonces = new HashSet<Long>();
private static final SecureRandom RANDOM = new SecureRandom();
private static final String TAG = Security.class.getSimpleName();
/** Generates a nonce (a random number used once). */
public static long generateNonce() {
long nonce = RANDOM.nextLong();
knownNonces.add(nonce);
return nonce;
}
public static boolean isNonceKnown(long nonce) {
return knownNonces.contains(nonce);
}
public static void removeNonce(long nonce) {
knownNonces.remove(nonce);
}
public static String obfuscate(Context context, byte[] salt, String original) {
final AESObfuscator obfuscator = getObfuscator(context, salt);
return obfuscator.obfuscate(original);
}
private static AESObfuscator _obfuscator = null;
private static AESObfuscator getObfuscator(Context context, byte[] salt) {
if (_obfuscator == null) {
final String installationId = Installation.id(context);
final String deviceId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
final String password = installationId + deviceId + context.getPackageName();
_obfuscator = new AESObfuscator(salt, password);
}
return _obfuscator;
}
public static String unobfuscate(Context context, byte[] salt, String obfuscated) {
final AESObfuscator obfuscator = getObfuscator(context, salt);
try {
return obfuscator.unobfuscate(obfuscated);
} catch (ValidationException e) {
Log.w(TAG, "Invalid obfuscated data or key");
}
return null;
}
}

View File

@@ -0,0 +1,155 @@
In app billing documentation
===================================
Requirements
-------------
Tested on Cordova 2.0
Installation
-------------
* Get acquainted with the Android In-app Billing documentation.
* Add in your src folder the *com* folder
It contains:
* [Google Play In-app Billing library]( http://developer.android.com/guide/google/play/billing/billing_overview.html)
* Cordova InAppBillingPlugin
* Add in your src folder the *net* folder
It contains the [Android Billing Library](https://github.com/robotmedia/AndroidBillingLibrary)
* Add inappbilling.js in your www folder
* Add in your index.html
`<script type="text/javascript" charset="utf-8" src="inappbilling.js"></script>`
* In res/xml/config.xml, add
`<plugin name="InAppBillingPlugin" value="com.smartmobilesoftware.inappbilling.InAppBillingPlugin"/>`
* Open the AndroidManifest.xml of your application
* add this permission
`<uses-permission android:name="com.android.vending.BILLING" />`
* this service and receiver inside the application element:
<pre>
&lt;service android:name="net.robotmedia.billing.BillingService" /&gt;
&lt;receiver android:name="net.robotmedia.billing.BillingReceiver"&gt;
&lt;intent-filter&gt;
&lt;action android:name="com.android.vending.billing.IN_APP_NOTIFY" /&gt;
&lt;action android:name="com.android.vending.billing.RESPONSE_CODE" /&gt;
&lt;action android:name="com.android.vending.billing.PURCHASE_STATE_CHANGED" /&gt;
&lt;/intent-filter&gt;
&lt;/receiver&gt;
</pre>
* In com.smartmobilesoftware.inappbilling open InAppBillingPlugin.java
* Add you public key (can be found in your Google Play account)
* Modify the salt with random numbers
* Read the google testing guide to learn how to test your app : http://developer.android.com/guide/google/play/billing/billing_testing.html
Usage
-------
#### Initialization
inappbilling.init(success,error)
parameters
* success : The success callback.
* error : The error callback.
#### Purchase
inappbilling.purchase(success, fail,productId)
parameters
* success : The success callback.
* error : The error callback.
* productId : The in app billing porduct id (example "sword_001")
#### Retrieve own products
inappbilling.getOwnItems(success, fail)
parameters
* success : The success callback. It provides a json array of the list of owned products as a parameter.
* error : The error callback.
Quick example
---------------
```javascript
inappbilling.init(successInit,errorCallback)
function successInit(result) {
// display the extracted text
alert(result);
inappbilling.purchase(successPurchase,errorCallback, "sword_001");
}
function errorCallback(error) {
alert(error);
}
function successPurchase(productId) {
alert("Your item has been purchased!");
}
```
Full example
----------------
```html
<!DOCTYPE HTML>
<html>
<head>
<title>Cordova</title>
<script type="text/javascript" charset="utf-8" src="cordova-2.0.0.js"></script>
<script type="text/javascript" charset="utf-8" src="inappbilling.js"></script>
<script type="text/javascript" charset="utf-8">
function successHandler (result) {
alert("SUCCESS: \r\n"+result );
}
function errorHandler (error) {
alert("ERROR: \r\n"+error );
}
// Click on init button
function init(){
// Initialize the billing plugin
inappbilling.init(successHandler, errorHandler);
}
// Click on purchase button
function purchase(){
// make the purchase
inappbilling.purchase(successHandler, errorHandler,"sword_001");
}
// Click on ownedProducts button
function ownedProducts(){
// Initialize the billing plugin
inappbilling.getOwnItems(successHandler, errorHandler);
}
</script>
</head>
<body>
<h1>Hello Billing</h1>
<button onclick="init();">Initalize the billing plugin</button>
<button onclick="purchase();">Purchase</button>
<button onclick="ownedProducts();">Owned products</button>
</body>
</html>
```
MIT License
----------------
Copyright (c) 2012 Guillaume Charhon - Smart Mobile Software
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.