mirror of
https://github.com/purplecabbage/phonegap-plugins.git
synced 2026-01-10 16:18:10 -05:00
Merge pull request #782 from poiuytrez/billingPlugin
[Android] Google Play In App Billing plugin
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
26
Android/InAppBilling/inappbilling.js
Normal file
26
Android/InAppBilling/inappbilling.js
Normal 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"]);
|
||||
},
|
||||
|
||||
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
325
Android/InAppBilling/net/robotmedia/billing/BillingRequest.java
Normal file
325
Android/InAppBilling/net/robotmedia/billing/BillingRequest.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
289
Android/InAppBilling/net/robotmedia/billing/BillingService.java
Normal file
289
Android/InAppBilling/net/robotmedia/billing/BillingService.java
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
110
Android/InAppBilling/net/robotmedia/billing/model/BillingDB.java
Normal file
110
Android/InAppBilling/net/robotmedia/billing/model/BillingDB.java
Normal 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) {}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
570
Android/InAppBilling/net/robotmedia/billing/utils/Base64.java
Normal file
570
Android/InAppBilling/net/robotmedia/billing/utils/Base64.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
155
Android/InAppBilling/readme.md
Normal file
155
Android/InAppBilling/readme.md
Normal 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>
|
||||
<service android:name="net.robotmedia.billing.BillingService" />
|
||||
<receiver android:name="net.robotmedia.billing.BillingReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="com.android.vending.billing.IN_APP_NOTIFY" />
|
||||
<action android:name="com.android.vending.billing.RESPONSE_CODE" />
|
||||
<action android:name="com.android.vending.billing.PURCHASE_STATE_CHANGED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
</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.
|
||||
Reference in New Issue
Block a user