diff --git a/AndroidBillingLibrary/src/net/robotmedia/billing/BillingController.java b/AndroidBillingLibrary/src/net/robotmedia/billing/BillingController.java index 5b9d107..f7f04d6 100644 --- a/AndroidBillingLibrary/src/net/robotmedia/billing/BillingController.java +++ b/AndroidBillingLibrary/src/net/robotmedia/billing/BillingController.java @@ -37,6 +37,7 @@ import android.app.PendingIntent.CanceledException; import android.content.Context; import android.content.Intent; +import android.os.AsyncTask; import android.text.TextUtils; import android.util.Log; @@ -60,9 +61,12 @@ public interface IConfiguration { /** * Returns the public key used to verify the signature of responses of - * the Market Billing service. + * the Google Play Billing service. If you are using a custom signature + * validator with server-side validation this method might not be needed + * and can return null. * * @return Base64 encoded public key. + * @see BillingController#setSignatureValidator(ISignatureValidator) */ public String getPublicKey(); } @@ -296,15 +300,15 @@ public static boolean isPurchased(Context context, String itemId) { * @param state * new purchase state of the item. */ - private static void notifyPurchaseStateChange(String itemId, Transaction.PurchaseState state) { + private static void notifyPurchaseStateChange(String itemId, Transaction.PurchaseState state, String orderId) { for (IBillingObserver o : observers) { - o.onPurchaseStateChanged(itemId, state); + o.onPurchaseStateChanged(itemId, state, orderId); } } /** * Obfuscates the specified purchase. Only the order id, product id and - * developer payload are obfuscated. + * developer payload, signed data and signature are obfuscated. * * @param context * @param purchase @@ -319,6 +323,8 @@ static void obfuscate(Context context, Transaction purchase) { purchase.orderId = Security.obfuscate(context, salt, purchase.orderId); purchase.productId = Security.obfuscate(context, salt, purchase.productId); purchase.developerPayload = Security.obfuscate(context, salt, purchase.developerPayload); + purchase.signedData = Security.obfuscate(context, salt, purchase.signedData); + purchase.signature = Security.obfuscate(context, salt, purchase.signature); } /** @@ -372,8 +378,8 @@ protected static void onPurchaseIntent(String itemId, PendingIntent purchaseInte /** * 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. + * received. Validates the signature asynchronously and calls + * {@link #onSignatureValidated(Context, String)} if successful. * * @param context * @param signedData @@ -381,7 +387,7 @@ protected static void onPurchaseIntent(String itemId, PendingIntent purchaseInte * @param signature * data signature. */ - protected static void onPurchaseStateChanged(Context context, String signedData, String signature) { + protected static void onPurchaseStateChanged(final Context context, final String signedData, final String signature) { debug("Purchase state changed"); if (TextUtils.isEmpty(signedData)) { @@ -391,19 +397,51 @@ protected static void onPurchaseStateChanged(Context context, String signedData, debug(signedData); } - if (!debug) { - if (TextUtils.isEmpty(signature)) { - Log.w(LOG_TAG, "Empty signature requires debug mode"); - return; + if (debug) { + onSignatureValidated(context, signedData, signature); + return; + } + + 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); + + // Use AsyncTask mostly in case the signature is validated remotely + new AsyncTask() { + + @Override + protected Boolean doInBackground(Void... params) { + return validator.validate(signedData, signature); } - 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; + + @Override + protected void onPostExecute(Boolean result) { + if (result) { + onSignatureValidated(context, signedData, signature); + } else { + Log.w(LOG_TAG, "Validation failed"); + } } - } + }.execute(); + } + + /** + * Called after the signature of a response to a + * {@link net.robotmedia.billing.request.GetPurchaseInformation} request has + * been validated. 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. + */ + private static void onSignatureValidated(Context context, String signedData, String signature) { List purchases; try { JSONObject jObject = new JSONObject(signedData); @@ -426,13 +464,18 @@ protected static void onPurchaseStateChanged(Context context, String signedData, // refunds. addManualConfirmation(p.productId, p.notificationId); } + + // Add signedData and signature as receipt to transaction + p.signedData = signedData; + p.signature = signature; + storeTransaction(context, p); - notifyPurchaseStateChange(p.productId, p.purchaseState); + notifyPurchaseStateChange(p.productId, p.purchaseState, p.orderId); } if (!confirmations.isEmpty()) { final String[] notifyIds = confirmations.toArray(new String[confirmations.size()]); confirmNotifications(context, notifyIds); - } + } } /** @@ -537,11 +580,10 @@ public static boolean registerObserver(IBillingObserver observer) { } /** - * Requests the purchase of the specified item. The transaction will not be - * confirmed automatically. + * Requests the purchase of the specified item. The transaction will be + * confirmed automatically. If manual confirmation or a developer payload are required use {@link #requestPurchase(Context, String, boolean, String)} instead. *

- * For subscriptions, use {@link #requestSubscription(Context, String)} - * instead. + * For subscriptions, use {@link #requestSubscription(Context, String)}. *

* * @param context @@ -550,7 +592,7 @@ public static boolean registerObserver(IBillingObserver observer) { * @see #requestPurchase(Context, String, boolean) */ public static void requestPurchase(Context context, String itemId) { - requestPurchase(context, itemId, false, null); + requestPurchase(context, itemId, true /* confirm */, null); } /** @@ -583,8 +625,8 @@ public static void requestPurchase(Context context, String itemId, boolean confi } /** - * Requests the purchase of the specified subscription item. The transaction - * will not be confirmed automatically. + * Requests the purchase of the specified subscription item. The transaction will be + * confirmed automatically. If manual confirmation or a developer payload are required use {@link #requestSubscription(Context, String, boolean, String)} instead. * * @param context * @param itemId @@ -592,7 +634,7 @@ public static void requestPurchase(Context context, String itemId, boolean confi * @see #requestSubscription(Context, String, boolean, String) */ public static void requestSubscription(Context context, String itemId) { - requestSubscription(context, itemId, false, null); + requestSubscription(context, itemId, true /* confirm */, null); } /** @@ -713,6 +755,8 @@ static void unobfuscate(Context context, Transaction purchase) { purchase.orderId = Security.unobfuscate(context, salt, purchase.orderId); purchase.productId = Security.unobfuscate(context, salt, purchase.productId); purchase.developerPayload = Security.unobfuscate(context, salt, purchase.developerPayload); + purchase.signedData = Security.unobfuscate(context, salt, purchase.signedData); + purchase.signature = Security.unobfuscate(context, salt, purchase.signature); } /** diff --git a/AndroidBillingLibrary/src/net/robotmedia/billing/BillingService.java b/AndroidBillingLibrary/src/net/robotmedia/billing/BillingService.java index a37a1ae..d37fb53 100644 --- a/AndroidBillingLibrary/src/net/robotmedia/billing/BillingService.java +++ b/AndroidBillingLibrary/src/net/robotmedia/billing/BillingService.java @@ -153,34 +153,6 @@ private void getPurchaseInformation(Intent intent, int startId) { 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) { @@ -210,6 +182,57 @@ private void handleCommand(Intent intent, int startId) { } } + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @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 + } + } + } + + /** + * Called when a remote exception occurs while trying to execute the + * {@link BillingRequest#run(IMarketBillingService)} method. + * @param e the exception + */ + protected void onRemoteException(RemoteException e) { + Log.w(this.getClass().getSimpleName(), "Remote billing service crashed"); + mService = 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 requestPurchase(Intent intent, int startId) { final String packageName = getPackageName(); final String itemId = intent.getStringExtra(EXTRA_ITEM_ID); @@ -217,7 +240,7 @@ private void requestPurchase(Intent intent, int startId) { 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); @@ -225,21 +248,38 @@ private void requestSubscription(Intent intent, int startId) { final RequestPurchase request = new RequestSubscription(packageName, startId, itemId, developerPayload); runRequestOrQueue(request); } - - private void restoreTransactions(Intent intent, int startId) { + + 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); } - + + /** + * Runs the given billing request if the service is already connected. + * @param request the billing request + * @return true if the request ran successfully; false if the service + * is not connected or there was an error when trying to use it + */ + private boolean runIfConnected(BillingRequest request) { + if (mService == null) return false; + try { + final long requestId = request.run(mService); + BillingController.onRequestSent(requestId, request); + return true; + } catch (RemoteException e) { + onRemoteException(e); + } + return false; + } + private void runPendingRequests() { BillingRequest request; int maxStartId = -1; while ((request = mPendingRequests.peek()) != null) { - if (mService != null) { - runRequest(request); + if (runIfConnected(request)) { mPendingRequests.remove(); if (maxStartId < request.getStartId()) { maxStartId = request.getStartId(); @@ -253,17 +293,7 @@ private void runPendingRequests() { 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) { @@ -272,18 +302,5 @@ private void runRequestOrQueue(BillingRequest request) { 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 - } - } - } } diff --git a/AndroidBillingLibrary/src/net/robotmedia/billing/IBillingObserver.java b/AndroidBillingLibrary/src/net/robotmedia/billing/IBillingObserver.java index 59f3ab4..27d3463 100644 --- a/AndroidBillingLibrary/src/net/robotmedia/billing/IBillingObserver.java +++ b/AndroidBillingLibrary/src/net/robotmedia/billing/IBillingObserver.java @@ -61,8 +61,12 @@ public interface IBillingObserver { * id of the item whose purchase state has changed. * @param state * purchase state of the specified item. + * @param orderId + * id of the corresponding order. This is particularly useful to + * differentiate between different orders of the same unmanaged + * item. */ - public void onPurchaseStateChanged(String itemId, PurchaseState state); + public void onPurchaseStateChanged(String itemId, PurchaseState state, String orderId); /** * Called with the response for the purchase request of the specified item. diff --git a/AndroidBillingLibrary/src/net/robotmedia/billing/helper/AbstractBillingActivity.java b/AndroidBillingLibrary/src/net/robotmedia/billing/helper/AbstractBillingActivity.java index 4415c05..819a3e0 100644 --- a/AndroidBillingLibrary/src/net/robotmedia/billing/helper/AbstractBillingActivity.java +++ b/AndroidBillingLibrary/src/net/robotmedia/billing/helper/AbstractBillingActivity.java @@ -89,8 +89,8 @@ public void onSubscriptionChecked(boolean supported) { AbstractBillingActivity.this.onSubscriptionChecked(supported); } - public void onPurchaseStateChanged(String itemId, PurchaseState state) { - AbstractBillingActivity.this.onPurchaseStateChanged(itemId, state); + public void onPurchaseStateChanged(String itemId, PurchaseState state, String orderId) { + AbstractBillingActivity.this.onPurchaseStateChanged(itemId, state, orderId); } public void onRequestPurchaseResponse(String itemId, ResponseCode response) { @@ -116,16 +116,14 @@ protected void onDestroy() { BillingController.setConfiguration(null); } - public abstract void onPurchaseStateChanged(String itemId, PurchaseState state);; + public abstract void onPurchaseStateChanged(String itemId, PurchaseState state, String orderId); 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)} + * Requests the purchase of the specified item. The transaction will be + * confirmed automatically. If manual confirmation is required use + * {@link BillingController#requestPurchase(android.content.Context, String, boolean, String)} * instead. * * @param itemId @@ -137,10 +135,8 @@ public void requestPurchase(String 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)} + * will be confirmed automatically. If manual confirmation is required use + * {@link BillingController#requestSubscription(android.content.Context, String, boolean, String)} * instead. * * @param itemId diff --git a/AndroidBillingLibrary/src/net/robotmedia/billing/helper/AbstractBillingFragment.java b/AndroidBillingLibrary/src/net/robotmedia/billing/helper/AbstractBillingFragment.java index 01644d2..0406faf 100644 --- a/AndroidBillingLibrary/src/net/robotmedia/billing/helper/AbstractBillingFragment.java +++ b/AndroidBillingLibrary/src/net/robotmedia/billing/helper/AbstractBillingFragment.java @@ -76,8 +76,8 @@ public void onSubscriptionChecked(boolean supported) { AbstractBillingFragment.this.onSubscriptionChecked(supported); } - public void onPurchaseStateChanged(String itemId, PurchaseState state) { - AbstractBillingFragment.this.onPurchaseStateChanged(itemId, state); + public void onPurchaseStateChanged(String itemId, PurchaseState state, String orderId) { + AbstractBillingFragment.this.onPurchaseStateChanged(itemId, state, orderId); } public void onRequestPurchaseResponse(String itemId, ResponseCode response) { @@ -103,16 +103,14 @@ public void onDestroy() { BillingController.setConfiguration(null); } - public abstract void onPurchaseStateChanged(String itemId, PurchaseState state);; + public abstract void onPurchaseStateChanged(String itemId, PurchaseState state, String orderId); 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)} + * Requests the purchase of the specified item. The transaction will be + * confirmed automatically. If manual confirmation is required use + * {@link BillingController#requestPurchase(android.content.Context, String, boolean, String)} * instead. * * @param itemId @@ -124,10 +122,8 @@ public void requestPurchase(String 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)} + * will be confirmed automatically. If manual confirmation is required use + * {@link BillingController#requestSubscription(android.content.Context, String, boolean, String)} * instead. * * @param itemId diff --git a/AndroidBillingLibrary/src/net/robotmedia/billing/model/BillingDB.java b/AndroidBillingLibrary/src/net/robotmedia/billing/model/BillingDB.java index a56a80b..c881b81 100644 --- a/AndroidBillingLibrary/src/net/robotmedia/billing/model/BillingDB.java +++ b/AndroidBillingLibrary/src/net/robotmedia/billing/model/BillingDB.java @@ -24,7 +24,7 @@ public class BillingDB { static final String DATABASE_NAME = "billing.db"; - static final int DATABASE_VERSION = 1; + static final int DATABASE_VERSION = 2; static final String TABLE_TRANSACTIONS = "purchases"; public static final String COLUMN__ID = "_id"; @@ -32,10 +32,13 @@ public class BillingDB { public static final String COLUMN_PRODUCT_ID = "productId"; public static final String COLUMN_PURCHASE_TIME = "purchaseTime"; public static final String COLUMN_DEVELOPER_PAYLOAD = "developerPayload"; + public static final String COLUMN_SIGNED_DATA = "signedData"; + public static final String COLUMN_SIGNATURE = "signature"; private static final String[] TABLE_TRANSACTIONS_COLUMNS = { COLUMN__ID, COLUMN_PRODUCT_ID, COLUMN_STATE, - COLUMN_PURCHASE_TIME, COLUMN_DEVELOPER_PAYLOAD + COLUMN_PURCHASE_TIME, COLUMN_DEVELOPER_PAYLOAD, + COLUMN_SIGNED_DATA, COLUMN_SIGNATURE }; SQLiteDatabase mDb; @@ -57,6 +60,8 @@ public void insert(Transaction transaction) { values.put(COLUMN_STATE, transaction.purchaseState.ordinal()); values.put(COLUMN_PURCHASE_TIME, transaction.purchaseTime); values.put(COLUMN_DEVELOPER_PAYLOAD, transaction.developerPayload); + values.put(COLUMN_SIGNED_DATA, transaction.signedData); + values.put(COLUMN_SIGNATURE, transaction.signature); mDb.replace(TABLE_TRANSACTIONS, null /* nullColumnHack */, values); } @@ -82,10 +87,12 @@ protected static final Transaction createTransaction(Cursor cursor) { purchase.purchaseState = PurchaseState.valueOf(cursor.getInt(2)); purchase.purchaseTime = cursor.getLong(3); purchase.developerPayload = cursor.getString(4); + purchase.signedData = cursor.getString(5); + purchase.signature = cursor.getString(6); return purchase; } - private class DatabaseHelper extends SQLiteOpenHelper { + public static class DatabaseHelper extends SQLiteOpenHelper { public DatabaseHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); } @@ -98,13 +105,28 @@ public void onCreate(SQLiteDatabase db) { private void createTransactionsTable(SQLiteDatabase db) { db.execSQL("CREATE TABLE " + TABLE_TRANSACTIONS + "(" + COLUMN__ID + " TEXT PRIMARY KEY, " + - COLUMN_PRODUCT_ID + " INTEGER, " + + COLUMN_PRODUCT_ID + " TEXT, " + COLUMN_STATE + " TEXT, " + COLUMN_PURCHASE_TIME + " TEXT, " + - COLUMN_DEVELOPER_PAYLOAD + " INTEGER)"); + COLUMN_DEVELOPER_PAYLOAD + " TEXT, " + + COLUMN_SIGNED_DATA + " TEXT," + + COLUMN_SIGNATURE + " TEXT)"); } @Override - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {} + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + if (oldVersion == 1 && newVersion == 2) { + db.beginTransaction(); + try { + db.execSQL("ALTER TABLE " + TABLE_TRANSACTIONS + + " ADD COLUMN " + COLUMN_SIGNED_DATA + " TEXT"); + db.execSQL("ALTER TABLE " + TABLE_TRANSACTIONS + + " ADD COLUMN " + COLUMN_SIGNATURE + " TEXT"); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } + } } } diff --git a/AndroidBillingLibrary/src/net/robotmedia/billing/model/Transaction.java b/AndroidBillingLibrary/src/net/robotmedia/billing/model/Transaction.java index b3b01e8..e7362f6 100644 --- a/AndroidBillingLibrary/src/net/robotmedia/billing/model/Transaction.java +++ b/AndroidBillingLibrary/src/net/robotmedia/billing/model/Transaction.java @@ -39,6 +39,7 @@ public static PurchaseState valueOf(int index) { return values[index]; } } + static final String DEVELOPER_PAYLOAD = "developerPayload"; static final String NOTIFICATION_ID = "notificationId"; static final String ORDER_ID = "orderId"; @@ -61,6 +62,8 @@ public static Transaction parse(JSONObject json) throws JSONException { return transaction; } + public String signedData; + public String signature; public String developerPayload; public String notificationId; public String orderId; @@ -72,7 +75,7 @@ public static Transaction parse(JSONObject json) throws JSONException { public Transaction() {} public Transaction(String orderId, String productId, String packageName, PurchaseState purchaseState, - String notificationId, long purchaseTime, String developerPayload) { + String notificationId, long purchaseTime, String developerPayload, String signature, String signedData) { this.orderId = orderId; this.productId = productId; this.packageName = packageName; @@ -80,10 +83,12 @@ public Transaction(String orderId, String productId, String packageName, Purchas this.notificationId = notificationId; this.purchaseTime = purchaseTime; this.developerPayload = developerPayload; + this.signature = signature; + this.signedData = signedData; } public Transaction clone() { - return new Transaction(orderId, productId, packageName, purchaseState, notificationId, purchaseTime, developerPayload); + return new Transaction(orderId, productId, packageName, purchaseState, notificationId, purchaseTime, developerPayload, signature, signedData); } @Override @@ -124,6 +129,16 @@ public boolean equals(Object obj) { return false; if (purchaseTime != other.purchaseTime) return false; + if (signature == null) { + if (other.signature != null) + return false; + } else if (!signature.equals(other.signature)) + return false; + if (signedData == null) { + if (other.signedData != null) + return false; + } else if (!signedData.equals(other.signedData)) + return false; return true; } @@ -131,5 +146,4 @@ public boolean equals(Object obj) { public String toString() { return String.valueOf(orderId); } - } diff --git a/AndroidBillingLibrary/src/net/robotmedia/billing/security/ISignatureValidator.java b/AndroidBillingLibrary/src/net/robotmedia/billing/security/ISignatureValidator.java index a329f05..04339fd 100644 --- a/AndroidBillingLibrary/src/net/robotmedia/billing/security/ISignatureValidator.java +++ b/AndroidBillingLibrary/src/net/robotmedia/billing/security/ISignatureValidator.java @@ -25,7 +25,8 @@ public interface ISignatureValidator { * signed data * @param signature * signature - * @return true if the data and signature match, false otherwise. + * @return true if the data and signature match, false otherwise or if there + * was an error during validation. */ public boolean validate(String signedData, String signature); diff --git a/AndroidBillingLibraryTest/src/net/robotmedia/billing/helper/MockBillingActivity.java b/AndroidBillingLibraryTest/src/net/robotmedia/billing/helper/MockBillingActivity.java index 4b9599a..78e29ee 100644 --- a/AndroidBillingLibraryTest/src/net/robotmedia/billing/helper/MockBillingActivity.java +++ b/AndroidBillingLibraryTest/src/net/robotmedia/billing/helper/MockBillingActivity.java @@ -46,7 +46,7 @@ public void onRequestPurchaseResponse(String itemId, ResponseCode response) { } @Override - public void onPurchaseStateChanged(String itemId, PurchaseState state) { + public void onPurchaseStateChanged(String itemId, PurchaseState state, String orderId) { // TODO Auto-generated method stub } diff --git a/AndroidBillingLibraryTest/src/net/robotmedia/billing/helper/MockBillingObserver.java b/AndroidBillingLibraryTest/src/net/robotmedia/billing/helper/MockBillingObserver.java index f9bb54b..6984fc4 100644 --- a/AndroidBillingLibraryTest/src/net/robotmedia/billing/helper/MockBillingObserver.java +++ b/AndroidBillingLibraryTest/src/net/robotmedia/billing/helper/MockBillingObserver.java @@ -16,7 +16,7 @@ public void onSubscriptionChecked(boolean supported) { public void onPurchaseIntent(String itemId, PendingIntent purchaseIntent) { } - public void onPurchaseStateChanged(String itemId, PurchaseState state) { + public void onPurchaseStateChanged(String itemId, PurchaseState state, String orderId) { } public void onRequestPurchaseResponse(String itemId, ResponseCode response) { diff --git a/AndroidBillingLibraryTest/src/net/robotmedia/billing/model/BillingDBTest.java b/AndroidBillingLibraryTest/src/net/robotmedia/billing/model/BillingDBTest.java index a410e82..f22a0d9 100644 --- a/AndroidBillingLibraryTest/src/net/robotmedia/billing/model/BillingDBTest.java +++ b/AndroidBillingLibraryTest/src/net/robotmedia/billing/model/BillingDBTest.java @@ -16,13 +16,12 @@ package net.robotmedia.billing.model; import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; import android.test.AndroidTestCase; import android.test.suitebuilder.annotation.SmallTest; public class BillingDBTest extends AndroidTestCase { - private BillingDB mData; - public static void assertEqualsFromDb(Transaction a, Transaction b) { assertEquals(a.orderId, b.orderId); assertEquals(a.productId, b.productId); @@ -31,23 +30,90 @@ public static void assertEqualsFromDb(Transaction a, Transaction b) { assertEquals(a.developerPayload, b.developerPayload); } - @Override - protected void setUp() throws Exception { - super.setUp(); - mData = new BillingDB(getContext()); - } - public static final void deleteDB(BillingDB data) { data.mDb.delete(BillingDB.TABLE_TRANSACTIONS, null, null); data.close(); } + private BillingDB mData; + + private SQLiteDatabase createVersion1Database() { + final SQLiteDatabase db = SQLiteDatabase.create(null); + db.execSQL("CREATE TABLE " + BillingDB.TABLE_TRANSACTIONS + "(" + + BillingDB.COLUMN__ID + " TEXT PRIMARY KEY, " + + BillingDB.COLUMN_PRODUCT_ID + " INTEGER, " + + BillingDB.COLUMN_STATE + " TEXT, " + + BillingDB.COLUMN_PURCHASE_TIME + " TEXT, " + + BillingDB.COLUMN_DEVELOPER_PAYLOAD + " INTEGER)"); + return db; + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + mData = new BillingDB(getContext()); + } + @Override protected void tearDown() throws Exception { super.tearDown(); deleteDB(mData); } - + + private void testColumn(SQLiteDatabase db, String column, String expectedType, boolean result) { + Cursor cursor = db.rawQuery("PRAGMA table_info(" + BillingDB.TABLE_TRANSACTIONS + ")", null); + boolean found = false; + while (cursor.moveToNext()) { + final String name = cursor.getString(1); + if (name.equals(column)) { + final String type = cursor.getString(2); + assertEquals(type, expectedType); + found = true; + } + } + cursor.close(); + assertEquals(found, result); + } + + @SmallTest + public void testDatabaseHelperConstructor() throws Exception { + BillingDB.DatabaseHelper helper = new BillingDB.DatabaseHelper(this.getContext()); + assertEquals(helper.getReadableDatabase().getVersion(), BillingDB.DATABASE_VERSION); + } + + @SmallTest + public void testDatabaseHelperOnCreate() throws Exception { + BillingDB.DatabaseHelper helper = new BillingDB.DatabaseHelper(this.getContext()); + SQLiteDatabase db = SQLiteDatabase.create(null); + helper.onCreate(db); + testTable(db, BillingDB.TABLE_TRANSACTIONS); + testColumn(db, BillingDB.COLUMN__ID, "TEXT", true); + testColumn(db, BillingDB.COLUMN_PRODUCT_ID, "TEXT", true); + testColumn(db, BillingDB.COLUMN_STATE, "TEXT", true); + testColumn(db, BillingDB.COLUMN_PURCHASE_TIME, "TEXT", true); + testColumn(db, BillingDB.COLUMN_DEVELOPER_PAYLOAD, "TEXT", true); + testColumn(db, BillingDB.COLUMN_SIGNED_DATA, "TEXT", true); + testColumn(db, BillingDB.COLUMN_SIGNATURE, "TEXT", true); + } + + @SmallTest + public void testDatabaseHelperOnUpgradeCurrentVersion() throws Exception { + BillingDB.DatabaseHelper helper = new BillingDB.DatabaseHelper(this.getContext()); + SQLiteDatabase db = helper.getWritableDatabase(); + helper.onUpgrade(db, BillingDB.DATABASE_VERSION, BillingDB.DATABASE_VERSION); + } + + @SmallTest + public void testDatabaseHelperOnUpgradeVersion1To2() throws Exception { + BillingDB.DatabaseHelper helper = new BillingDB.DatabaseHelper(this.getContext()); + SQLiteDatabase db = createVersion1Database(); + testColumn(db, BillingDB.COLUMN_SIGNED_DATA, "TEXT", false); + testColumn(db, BillingDB.COLUMN_SIGNATURE, "TEXT", false); + helper.onUpgrade(db, 1, 2); + testColumn(db, BillingDB.COLUMN_SIGNED_DATA, "TEXT", true); + testColumn(db, BillingDB.COLUMN_SIGNATURE, "TEXT", true); + } + @SmallTest public void testInsert() throws Exception { mData.insert(TransactionTest.TRANSACTION_1); @@ -59,14 +125,6 @@ public void testInsert() throws Exception { stored.notificationId = TransactionTest.TRANSACTION_1.notificationId; // Not stored in DB assertEqualsFromDb(TransactionTest.TRANSACTION_1, stored); } - - @SmallTest - public void testUnique() throws Exception { - mData.insert(TransactionTest.TRANSACTION_1); - mData.insert(TransactionTest.TRANSACTION_1); - final Cursor cursor = mData.queryTransactions(); - assertEquals(cursor.getCount(), 1); - } @SmallTest public void testQueryTransactions() throws Exception { @@ -130,4 +188,17 @@ public void testQueryTransactionsStringPurchaseState() throws Exception { cursor2.close(); } + private void testTable(SQLiteDatabase db, String table) { + Cursor cursor = db.query("sqlite_master", new String[] {"name"}, "type='table' AND name='" + table + "'", null, null, null, null); + assertTrue(cursor.getCount() > 0); + cursor.close(); + } + + @SmallTest + public void testUnique() throws Exception { + mData.insert(TransactionTest.TRANSACTION_1); + mData.insert(TransactionTest.TRANSACTION_1); + final Cursor cursor = mData.queryTransactions(); + assertEquals(cursor.getCount(), 1); + } } diff --git a/AndroidBillingLibraryTest/src/net/robotmedia/billing/model/TransactionTest.java b/AndroidBillingLibraryTest/src/net/robotmedia/billing/model/TransactionTest.java index 4275333..ea7bd9f 100644 --- a/AndroidBillingLibraryTest/src/net/robotmedia/billing/model/TransactionTest.java +++ b/AndroidBillingLibraryTest/src/net/robotmedia/billing/model/TransactionTest.java @@ -25,26 +25,39 @@ public class TransactionTest extends TestCase { - public static final Transaction TRANSACTION_1 = new Transaction("order1", "android.test.purchased", "com.example", Transaction.PurchaseState.PURCHASED, "notificationId", new Date().getTime(), "developerPayload"); - public static final Transaction TRANSACTION_2 = new Transaction("order2", "product_2", "com.example", Transaction.PurchaseState.PURCHASED, "notificationId", new Date().getTime(), "developerPayload"); - public static final Transaction TRANSACTION_2_REFUNDED = new Transaction("order4", "product_2", "com.example", Transaction.PurchaseState.REFUNDED, "notificationId", new Date().getTime(), "developerPayload"); - public static final Transaction TRANSACTION_1_REFUNDED = new Transaction("order3", "android.test.purchased", "com.example", Transaction.PurchaseState.REFUNDED, "notificationId", new Date().getTime(), "developerPayload"); + public static final Transaction TRANSACTION_1 = new Transaction("order1", "android.test.purchased", "com.example", Transaction.PurchaseState.PURCHASED, "notificationId", new Date().getTime(), "developerPayload", "signature", "signedData"); + public static final Transaction TRANSACTION_2 = new Transaction("order2", "product_2", "com.example", Transaction.PurchaseState.PURCHASED, "notificationId", new Date().getTime(), "developerPayload", "signature", "signedData"); + public static final Transaction TRANSACTION_2_REFUNDED = new Transaction("order4", "product_2", "com.example", Transaction.PurchaseState.REFUNDED, "notificationId", new Date().getTime(), "developerPayload", "signature", "signedData"); + public static final Transaction TRANSACTION_1_REFUNDED = new Transaction("order3", "android.test.purchased", "com.example", Transaction.PurchaseState.REFUNDED, "notificationId", new Date().getTime(), "developerPayload", "signature", "signedData"); public static void assertEquals(Transaction a, Transaction b) { assertTrue(a.equals(b)); } - @SmallTest - public void testParseAllFields() throws Exception { + private void testParseAllFields(Transaction transaction) throws Exception { JSONObject json = new JSONObject(); - json.put(Transaction.ORDER_ID, TRANSACTION_1.orderId); - json.put(Transaction.PRODUCT_ID, TRANSACTION_1.productId); - json.put(Transaction.PACKAGE_NAME, TRANSACTION_1.packageName); - json.put(Transaction.PURCHASE_STATE, TRANSACTION_1.purchaseState.ordinal()); - json.put(Transaction.NOTIFICATION_ID, TRANSACTION_1.notificationId); - json.put(Transaction.PURCHASE_TIME, TRANSACTION_1.purchaseTime); - json.put(Transaction.DEVELOPER_PAYLOAD, TRANSACTION_1.developerPayload); + json.put(Transaction.ORDER_ID, transaction.orderId); + json.put(Transaction.PRODUCT_ID, transaction.productId); + json.put(Transaction.PACKAGE_NAME, transaction.packageName); + json.put(Transaction.PURCHASE_STATE, transaction.purchaseState.ordinal()); + json.put(Transaction.NOTIFICATION_ID, transaction.notificationId); + json.put(Transaction.PURCHASE_TIME, transaction.purchaseTime); + json.put(Transaction.DEVELOPER_PAYLOAD, transaction.developerPayload); final Transaction parsed = Transaction.parse(json); - assertEquals(TRANSACTION_1, parsed); + assertEquals(transaction.orderId, parsed.orderId); + assertEquals(transaction.productId, parsed.productId); + assertEquals(transaction.packageName, parsed.packageName); + assertEquals(transaction.purchaseState, parsed.purchaseState); + assertEquals(transaction.notificationId, parsed.notificationId); + assertEquals(transaction.purchaseTime, parsed.purchaseTime); + assertEquals(transaction.developerPayload, parsed.developerPayload); + assertNull(parsed.signature); + assertNull(parsed.signedData); + } + + @SmallTest + public void testParseAllFields() throws Exception { + testParseAllFields(TRANSACTION_1); + testParseAllFields(TRANSACTION_2); } @SmallTest @@ -62,6 +75,8 @@ public void testParseOnlyMandatoryFields() throws Exception { assertNull(parsed.notificationId); assertEquals(TRANSACTION_1.purchaseTime, parsed.purchaseTime); assertNull(parsed.developerPayload); + assertNull(parsed.signature); + assertNull(parsed.signedData); } @SmallTest @@ -76,11 +91,14 @@ public void testPurchaseStateOrdinal() throws Exception { public void testEquals() throws Exception { assertTrue(TRANSACTION_1.equals(TRANSACTION_1)); assertTrue(TRANSACTION_1.equals(TRANSACTION_1.clone())); - assertFalse(TRANSACTION_1.equals(TRANSACTION_2_REFUNDED)); + assertTrue(TRANSACTION_1.clone().equals(TRANSACTION_1)); + assertFalse(TRANSACTION_1.equals(TRANSACTION_2)); + assertFalse(TRANSACTION_2.equals(TRANSACTION_1)); } @SmallTest public void testClone() throws Exception { assertEquals(TRANSACTION_1, TRANSACTION_1.clone()); + assertEquals(TRANSACTION_2, TRANSACTION_2.clone()); } } diff --git a/DungeonsRedux/src/net/robotmedia/billing/example/Dungeons.java b/DungeonsRedux/src/net/robotmedia/billing/example/Dungeons.java index 99e0bf6..13b8ed7 100644 --- a/DungeonsRedux/src/net/robotmedia/billing/example/Dungeons.java +++ b/DungeonsRedux/src/net/robotmedia/billing/example/Dungeons.java @@ -73,8 +73,8 @@ public void onBillingChecked(boolean supported) { Dungeons.this.onBillingChecked(supported); } - public void onPurchaseStateChanged(String itemId, PurchaseState state) { - Dungeons.this.onPurchaseStateChanged(itemId, state); + public void onPurchaseStateChanged(String itemId, PurchaseState state, String orderId) { + Dungeons.this.onPurchaseStateChanged(itemId, state, orderId); } public void onRequestPurchaseResponse(String itemId, ResponseCode response) { @@ -112,7 +112,7 @@ protected void onDestroy() { super.onDestroy(); } - public void onPurchaseStateChanged(String itemId, PurchaseState state) { + public void onPurchaseStateChanged(String itemId, PurchaseState state, String orderId) { Log.i(TAG, "onPurchaseStateChanged() itemId: " + itemId); updateOwnedItems(); } @@ -144,9 +144,9 @@ private void setupWidgets() { public void onClick(View v) { if (mSelectedItem.managed != Managed.SUBSCRIPTION) { - BillingController.requestPurchase(Dungeons.this, mSelectedItem.sku, true /* confirm */, null); + BillingController.requestPurchase(Dungeons.this, mSelectedItem.sku); } else { - BillingController.requestSubscription(Dungeons.this, mSelectedItem.sku, true /* confirm */, null); + BillingController.requestSubscription(Dungeons.this, mSelectedItem.sku); } } }); diff --git a/README.mdown b/README.mdown index 34b7bbd..d665073 100644 --- a/README.mdown +++ b/README.mdown @@ -14,6 +14,8 @@ Getting Started * Get acquainted with the [Android In-app Billing][1] documentation. +* No, really. Read the [documentation first][1]. This library saves you from writing code, but not from reading the documentation. + * Add *Android Billing Library* to your project. * Open the *AndroidManifest.xml* of your application and add this permission...