From e23bc72c6178b8f03f75f3211b14deadb050c198 Mon Sep 17 00:00:00 2001 From: Hermes Pique Date: Sat, 15 Sep 2012 12:54:28 +0200 Subject: [PATCH 01/16] Asynchronous signature validation --- .../robotmedia/billing/BillingController.java | 64 ++++++++++++++----- 1 file changed, 49 insertions(+), 15 deletions(-) diff --git a/AndroidBillingLibrary/src/net/robotmedia/billing/BillingController.java b/AndroidBillingLibrary/src/net/robotmedia/billing/BillingController.java index 5b9d107..9c08639 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(); } @@ -372,8 +376,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 +385,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 +395,49 @@ 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); + 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); + } else { + Log.w(LOG_TAG, "Signature does not match data."); + } } - } + }.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. + */ + private static void onSignatureValidated(Context context, String signedData) { List purchases; try { JSONObject jObject = new JSONObject(signedData); @@ -432,7 +466,7 @@ protected static void onPurchaseStateChanged(Context context, String signedData, if (!confirmations.isEmpty()) { final String[] notifyIds = confirmations.toArray(new String[confirmations.size()]); confirmNotifications(context, notifyIds); - } + } } /** From dc903e47a1bac8586443e90655adf5dd1a6eccdd Mon Sep 17 00:00:00 2001 From: Stefan Reinhard Date: Mon, 25 Jun 2012 15:38:48 +0200 Subject: [PATCH 02/16] Save signedData and signature on transaction --- .../robotmedia/billing/BillingController.java | 11 ++++++- .../robotmedia/billing/model/BillingDB.java | 30 ++++++++++++++--- .../robotmedia/billing/model/Transaction.java | 32 +++++++++++++++++-- 3 files changed, 66 insertions(+), 7 deletions(-) diff --git a/AndroidBillingLibrary/src/net/robotmedia/billing/BillingController.java b/AndroidBillingLibrary/src/net/robotmedia/billing/BillingController.java index 5b9d107..16c0065 100644 --- a/AndroidBillingLibrary/src/net/robotmedia/billing/BillingController.java +++ b/AndroidBillingLibrary/src/net/robotmedia/billing/BillingController.java @@ -304,7 +304,7 @@ private static void notifyPurchaseStateChange(String itemId, Transaction.Purchas /** * 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 +319,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); } /** @@ -426,6 +428,11 @@ 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); } @@ -713,6 +720,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/model/BillingDB.java b/AndroidBillingLibrary/src/net/robotmedia/billing/model/BillingDB.java index a56a80b..11f7e0a 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,6 +87,8 @@ 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; } @@ -101,10 +108,25 @@ private void createTransactionsTable(SQLiteDatabase db) { COLUMN_PRODUCT_ID + " INTEGER, " + COLUMN_STATE + " TEXT, " + COLUMN_PURCHASE_TIME + " TEXT, " + - COLUMN_DEVELOPER_PAYLOAD + " INTEGER)"); + COLUMN_DEVELOPER_PAYLOAD + " INTEGER, " + + 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..30c08d8 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; @@ -82,8 +85,21 @@ public Transaction(String orderId, String productId, String packageName, Purchas this.developerPayload = developerPayload; } + public Transaction(String orderId, String productId, String packageName, PurchaseState purchaseState, + String notificationId, long purchaseTime, String developerPayload, String signature, String signedData) { + this.orderId = orderId; + this.productId = productId; + this.packageName = packageName; + this.purchaseState = purchaseState; + 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 +140,19 @@ 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 +160,4 @@ public boolean equals(Object obj) { public String toString() { return String.valueOf(orderId); } - } From 362fec2e63df8678710da8ade3a1fe0d547500aa Mon Sep 17 00:00:00 2001 From: Hermes Pique Date: Sat, 15 Sep 2012 12:54:28 +0200 Subject: [PATCH 03/16] Asynchronous signature validation --- .../robotmedia/billing/BillingController.java | 64 ++++++++++++++----- 1 file changed, 49 insertions(+), 15 deletions(-) diff --git a/AndroidBillingLibrary/src/net/robotmedia/billing/BillingController.java b/AndroidBillingLibrary/src/net/robotmedia/billing/BillingController.java index 16c0065..597d802 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(); } @@ -374,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 @@ -383,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)) { @@ -393,19 +397,49 @@ 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); + 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); + } else { + Log.w(LOG_TAG, "Signature does not match data."); + } } - } + }.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. + */ + private static void onSignatureValidated(Context context, String signedData) { List purchases; try { JSONObject jObject = new JSONObject(signedData); @@ -439,7 +473,7 @@ protected static void onPurchaseStateChanged(Context context, String signedData, if (!confirmations.isEmpty()) { final String[] notifyIds = confirmations.toArray(new String[confirmations.size()]); confirmNotifications(context, notifyIds); - } + } } /** From 53b952a756825a7a29c65e596ca2f02b5d2ae561 Mon Sep 17 00:00:00 2001 From: Hermes Pique Date: Wed, 17 Oct 2012 12:51:53 +0200 Subject: [PATCH 04/16] Remove previous constructor and style changes --- .../net/robotmedia/billing/model/Transaction.java | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/AndroidBillingLibrary/src/net/robotmedia/billing/model/Transaction.java b/AndroidBillingLibrary/src/net/robotmedia/billing/model/Transaction.java index 30c08d8..e7362f6 100644 --- a/AndroidBillingLibrary/src/net/robotmedia/billing/model/Transaction.java +++ b/AndroidBillingLibrary/src/net/robotmedia/billing/model/Transaction.java @@ -74,17 +74,6 @@ 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) { - this.orderId = orderId; - this.productId = productId; - this.packageName = packageName; - this.purchaseState = purchaseState; - this.notificationId = notificationId; - this.purchaseTime = purchaseTime; - this.developerPayload = developerPayload; - } - public Transaction(String orderId, String productId, String packageName, PurchaseState purchaseState, String notificationId, long purchaseTime, String developerPayload, String signature, String signedData) { this.orderId = orderId; @@ -140,19 +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; } From 6e6d83d0e50c0a75ca2c63212756a3df87530cb8 Mon Sep 17 00:00:00 2001 From: Hermes Pique Date: Wed, 17 Oct 2012 12:52:08 +0200 Subject: [PATCH 05/16] Add signature to onSignatureValidated --- .../src/net/robotmedia/billing/BillingController.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/AndroidBillingLibrary/src/net/robotmedia/billing/BillingController.java b/AndroidBillingLibrary/src/net/robotmedia/billing/BillingController.java index 597d802..8204017 100644 --- a/AndroidBillingLibrary/src/net/robotmedia/billing/BillingController.java +++ b/AndroidBillingLibrary/src/net/robotmedia/billing/BillingController.java @@ -398,7 +398,7 @@ protected static void onPurchaseStateChanged(final Context context, final String } if (debug) { - onSignatureValidated(context, signedData); + onSignatureValidated(context, signedData, signature); return; } @@ -420,7 +420,7 @@ protected Boolean doInBackground(Void... params) { @Override protected void onPostExecute(Boolean result) { if (result) { - onSignatureValidated(context, signedData); + onSignatureValidated(context, signedData, signature); } else { Log.w(LOG_TAG, "Signature does not match data."); } @@ -438,8 +438,10 @@ protected void onPostExecute(Boolean result) { * @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) { + private static void onSignatureValidated(Context context, String signedData, String signature) { List purchases; try { JSONObject jObject = new JSONObject(signedData); From 95867b585db761b528c04242fb00643df686e6c6 Mon Sep 17 00:00:00 2001 From: Hermes Pique Date: Wed, 17 Oct 2012 12:52:46 +0200 Subject: [PATCH 06/16] Fix types of product id and developer payload (INTEGER > TEXT) --- .../src/net/robotmedia/billing/model/BillingDB.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/AndroidBillingLibrary/src/net/robotmedia/billing/model/BillingDB.java b/AndroidBillingLibrary/src/net/robotmedia/billing/model/BillingDB.java index 11f7e0a..c881b81 100644 --- a/AndroidBillingLibrary/src/net/robotmedia/billing/model/BillingDB.java +++ b/AndroidBillingLibrary/src/net/robotmedia/billing/model/BillingDB.java @@ -92,7 +92,7 @@ protected static final Transaction createTransaction(Cursor cursor) { return purchase; } - private class DatabaseHelper extends SQLiteOpenHelper { + public static class DatabaseHelper extends SQLiteOpenHelper { public DatabaseHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); } @@ -105,10 +105,10 @@ 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)"); } From c0ac93e900122016a39f31319b36932ef312e75c Mon Sep 17 00:00:00 2001 From: Hermes Pique Date: Wed, 17 Oct 2012 12:53:29 +0200 Subject: [PATCH 07/16] More test cases --- .../billing/model/TransactionTest.java | 48 +++++++++++++------ 1 file changed, 33 insertions(+), 15 deletions(-) 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()); } } From a26d27414c5b0ca561d40e48703485b9f43369ae Mon Sep 17 00:00:00 2001 From: Hermes Pique Date: Wed, 17 Oct 2012 12:53:47 +0200 Subject: [PATCH 08/16] Test DatabaseHelper --- .../billing/model/BillingDBTest.java | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/AndroidBillingLibraryTest/src/net/robotmedia/billing/model/BillingDBTest.java b/AndroidBillingLibraryTest/src/net/robotmedia/billing/model/BillingDBTest.java index a410e82..7e60dca 100644 --- a/AndroidBillingLibraryTest/src/net/robotmedia/billing/model/BillingDBTest.java +++ b/AndroidBillingLibraryTest/src/net/robotmedia/billing/model/BillingDBTest.java @@ -16,6 +16,7 @@ package net.robotmedia.billing.model; import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; import android.test.AndroidTestCase; import android.test.suitebuilder.annotation.SmallTest; @@ -130,4 +131,71 @@ public void testQueryTransactionsStringPurchaseState() throws Exception { cursor2.close(); } + @SmallTest + public void testDatabaseHelperConstructor() throws Exception { + BillingDB.DatabaseHelper helper = new BillingDB.DatabaseHelper(this.getContext()); + assertEquals(helper.getReadableDatabase().getVersion(), BillingDB.DATABASE_VERSION); + } + + 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 testDatabaseHelperOnCreate() throws Exception { + BillingDB.DatabaseHelper helper = new BillingDB.DatabaseHelper(this.getContext()); + SQLiteDatabase db = SQLiteDatabase.create(null); + helper.onCreate(db); + Cursor cursor = db.query("sqlite_master", new String[] {"name"}, "type='table' AND name='" + BillingDB.TABLE_TRANSACTIONS + "'", null, null, null, null); + assertTrue(cursor.getCount() > 0); + cursor.close(); + 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); + + } + + 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; + } + + @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); + } } From b9c6abfc34a3d301e2ade41450d9553723196da6 Mon Sep 17 00:00:00 2001 From: Hermes Pique Date: Wed, 17 Oct 2012 13:25:07 +0200 Subject: [PATCH 09/16] IMPORTANT: automatic confirmations are now the default --- .../net/robotmedia/billing/BillingController.java | 15 +++++++-------- .../billing/helper/AbstractBillingActivity.java | 14 +++++--------- .../billing/helper/AbstractBillingFragment.java | 14 +++++--------- .../net/robotmedia/billing/example/Dungeons.java | 4 ++-- 4 files changed, 19 insertions(+), 28 deletions(-) diff --git a/AndroidBillingLibrary/src/net/robotmedia/billing/BillingController.java b/AndroidBillingLibrary/src/net/robotmedia/billing/BillingController.java index 8204017..ba51b4d 100644 --- a/AndroidBillingLibrary/src/net/robotmedia/billing/BillingController.java +++ b/AndroidBillingLibrary/src/net/robotmedia/billing/BillingController.java @@ -580,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 @@ -593,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); } /** @@ -626,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 @@ -635,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); } /** diff --git a/AndroidBillingLibrary/src/net/robotmedia/billing/helper/AbstractBillingActivity.java b/AndroidBillingLibrary/src/net/robotmedia/billing/helper/AbstractBillingActivity.java index 4415c05..131c88d 100644 --- a/AndroidBillingLibrary/src/net/robotmedia/billing/helper/AbstractBillingActivity.java +++ b/AndroidBillingLibrary/src/net/robotmedia/billing/helper/AbstractBillingActivity.java @@ -121,11 +121,9 @@ protected void onDestroy() { 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..64c9107 100644 --- a/AndroidBillingLibrary/src/net/robotmedia/billing/helper/AbstractBillingFragment.java +++ b/AndroidBillingLibrary/src/net/robotmedia/billing/helper/AbstractBillingFragment.java @@ -108,11 +108,9 @@ public void onDestroy() { 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/DungeonsRedux/src/net/robotmedia/billing/example/Dungeons.java b/DungeonsRedux/src/net/robotmedia/billing/example/Dungeons.java index 99e0bf6..b6ef8aa 100644 --- a/DungeonsRedux/src/net/robotmedia/billing/example/Dungeons.java +++ b/DungeonsRedux/src/net/robotmedia/billing/example/Dungeons.java @@ -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); } } }); From a62593764172d0fd650b1dca1d15c59e79aecd49 Mon Sep 17 00:00:00 2001 From: Hermes Pique Date: Wed, 17 Oct 2012 15:34:20 +0200 Subject: [PATCH 10/16] Rebind service on remote exception. Closes #52 --- .../robotmedia/billing/BillingService.java | 43 +++++++++++++------ 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/AndroidBillingLibrary/src/net/robotmedia/billing/BillingService.java b/AndroidBillingLibrary/src/net/robotmedia/billing/BillingService.java index a37a1ae..be01f9f 100644 --- a/AndroidBillingLibrary/src/net/robotmedia/billing/BillingService.java +++ b/AndroidBillingLibrary/src/net/robotmedia/billing/BillingService.java @@ -238,8 +238,7 @@ 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 +252,35 @@ 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? - } - } - + + /** + * 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; + } + + /** + * 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 runRequestOrQueue(BillingRequest request) { mPendingRequests.add(request); if (mService == null) { From 3cb03eedf96ee0a34e22653b57ebb16b33f143d1 Mon Sep 17 00:00:00 2001 From: Hermes Pique Date: Wed, 17 Oct 2012 15:34:39 +0200 Subject: [PATCH 11/16] Sort members --- .../robotmedia/billing/BillingService.java | 146 +++++++++--------- 1 file changed, 73 insertions(+), 73 deletions(-) diff --git a/AndroidBillingLibrary/src/net/robotmedia/billing/BillingService.java b/AndroidBillingLibrary/src/net/robotmedia/billing/BillingService.java index be01f9f..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,43 +248,14 @@ 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); } - - private void runPendingRequests() { - BillingRequest request; - int maxStartId = -1; - while ((request = mPendingRequests.peek()) != null) { - if (runIfConnected(request)) { - mPendingRequests.remove(); - if (maxStartId < request.getStartId()) { - maxStartId = request.getStartId(); - } - } else { - bindMarketBillingService(); - return; - } - } - if (maxStartId >= 0) { - stopSelf(maxStartId); - } - } - - /** - * 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; - } /** * Runs the given billing request if the service is already connected. @@ -281,6 +275,25 @@ private boolean runIfConnected(BillingRequest request) { return false; } + private void runPendingRequests() { + BillingRequest request; + int maxStartId = -1; + while ((request = mPendingRequests.peek()) != null) { + if (runIfConnected(request)) { + mPendingRequests.remove(); + if (maxStartId < request.getStartId()) { + maxStartId = request.getStartId(); + } + } else { + bindMarketBillingService(); + return; + } + } + if (maxStartId >= 0) { + stopSelf(maxStartId); + } + } + private void runRequestOrQueue(BillingRequest request) { mPendingRequests.add(request); if (mService == null) { @@ -289,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 - } - } - } } From ea03da9c3131175fa115d66e9fa9037fa6dc7a43 Mon Sep 17 00:00:00 2001 From: Hermes Pique Date: Wed, 17 Oct 2012 15:57:41 +0200 Subject: [PATCH 12/16] Changed error message and doc to better reflect that validation might fail for other reasons --- .../src/net/robotmedia/billing/BillingController.java | 2 +- .../net/robotmedia/billing/security/ISignatureValidator.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/AndroidBillingLibrary/src/net/robotmedia/billing/BillingController.java b/AndroidBillingLibrary/src/net/robotmedia/billing/BillingController.java index ba51b4d..e86c81a 100644 --- a/AndroidBillingLibrary/src/net/robotmedia/billing/BillingController.java +++ b/AndroidBillingLibrary/src/net/robotmedia/billing/BillingController.java @@ -422,7 +422,7 @@ protected void onPostExecute(Boolean result) { if (result) { onSignatureValidated(context, signedData, signature); } else { - Log.w(LOG_TAG, "Signature does not match data."); + Log.w(LOG_TAG, "Validation failed"); } } 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); From 58079a48e0608876072e6e38d5d30374521eac21 Mon Sep 17 00:00:00 2001 From: Hermes Pique Date: Wed, 17 Oct 2012 16:00:07 +0200 Subject: [PATCH 13/16] Refactor --- .../net/robotmedia/billing/model/BillingDBTest.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/AndroidBillingLibraryTest/src/net/robotmedia/billing/model/BillingDBTest.java b/AndroidBillingLibraryTest/src/net/robotmedia/billing/model/BillingDBTest.java index 7e60dca..f276b13 100644 --- a/AndroidBillingLibraryTest/src/net/robotmedia/billing/model/BillingDBTest.java +++ b/AndroidBillingLibraryTest/src/net/robotmedia/billing/model/BillingDBTest.java @@ -152,14 +152,18 @@ private void testColumn(SQLiteDatabase db, String column, String expectedType, b assertEquals(found, result); } + 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 testDatabaseHelperOnCreate() throws Exception { BillingDB.DatabaseHelper helper = new BillingDB.DatabaseHelper(this.getContext()); SQLiteDatabase db = SQLiteDatabase.create(null); helper.onCreate(db); - Cursor cursor = db.query("sqlite_master", new String[] {"name"}, "type='table' AND name='" + BillingDB.TABLE_TRANSACTIONS + "'", null, null, null, null); - assertTrue(cursor.getCount() > 0); - cursor.close(); + 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); @@ -174,7 +178,6 @@ 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); - } private SQLiteDatabase createVersion1Database() { From add81bf4b92a29a61344d4940b8606b0dbcd48ab Mon Sep 17 00:00:00 2001 From: Hermes Pique Date: Wed, 17 Oct 2012 16:00:31 +0200 Subject: [PATCH 14/16] Sort members --- .../billing/model/BillingDBTest.java | 158 +++++++++--------- 1 file changed, 79 insertions(+), 79 deletions(-) diff --git a/AndroidBillingLibraryTest/src/net/robotmedia/billing/model/BillingDBTest.java b/AndroidBillingLibraryTest/src/net/robotmedia/billing/model/BillingDBTest.java index f276b13..f22a0d9 100644 --- a/AndroidBillingLibraryTest/src/net/robotmedia/billing/model/BillingDBTest.java +++ b/AndroidBillingLibraryTest/src/net/robotmedia/billing/model/BillingDBTest.java @@ -22,8 +22,6 @@ 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); @@ -32,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); @@ -60,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 { @@ -131,27 +188,6 @@ public void testQueryTransactionsStringPurchaseState() throws Exception { cursor2.close(); } - @SmallTest - public void testDatabaseHelperConstructor() throws Exception { - BillingDB.DatabaseHelper helper = new BillingDB.DatabaseHelper(this.getContext()); - assertEquals(helper.getReadableDatabase().getVersion(), BillingDB.DATABASE_VERSION); - } - - 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); - } - 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); @@ -159,46 +195,10 @@ private void testTable(SQLiteDatabase db, String table) { } @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); - } - - 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; - } - - @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); + public void testUnique() throws Exception { + mData.insert(TransactionTest.TRANSACTION_1); + mData.insert(TransactionTest.TRANSACTION_1); + final Cursor cursor = mData.queryTransactions(); + assertEquals(cursor.getCount(), 1); } } From 4f11f9da0ef4101f1fe85275a94c56fdf4309ec1 Mon Sep 17 00:00:00 2001 From: Hermes Pique Date: Wed, 17 Oct 2012 16:12:59 +0200 Subject: [PATCH 15/16] RTFM --- README.mdown | 2 ++ 1 file changed, 2 insertions(+) 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... From e4711821361fbf48d74f8596ad52cac9c710f7e5 Mon Sep 17 00:00:00 2001 From: Hermes Pique Date: Wed, 17 Oct 2012 18:44:14 +0200 Subject: [PATCH 16/16] Add orderId to onPurchaseStateChanged --- .../src/net/robotmedia/billing/BillingController.java | 6 +++--- .../src/net/robotmedia/billing/IBillingObserver.java | 6 +++++- .../robotmedia/billing/helper/AbstractBillingActivity.java | 6 +++--- .../robotmedia/billing/helper/AbstractBillingFragment.java | 6 +++--- .../net/robotmedia/billing/helper/MockBillingActivity.java | 2 +- .../net/robotmedia/billing/helper/MockBillingObserver.java | 2 +- .../src/net/robotmedia/billing/example/Dungeons.java | 6 +++--- 7 files changed, 19 insertions(+), 15 deletions(-) diff --git a/AndroidBillingLibrary/src/net/robotmedia/billing/BillingController.java b/AndroidBillingLibrary/src/net/robotmedia/billing/BillingController.java index e86c81a..f7f04d6 100644 --- a/AndroidBillingLibrary/src/net/robotmedia/billing/BillingController.java +++ b/AndroidBillingLibrary/src/net/robotmedia/billing/BillingController.java @@ -300,9 +300,9 @@ 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); } } @@ -470,7 +470,7 @@ private static void onSignatureValidated(Context context, String signedData, Str 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()]); 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 131c88d..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,7 +116,7 @@ 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); diff --git a/AndroidBillingLibrary/src/net/robotmedia/billing/helper/AbstractBillingFragment.java b/AndroidBillingLibrary/src/net/robotmedia/billing/helper/AbstractBillingFragment.java index 64c9107..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,7 +103,7 @@ 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); 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/DungeonsRedux/src/net/robotmedia/billing/example/Dungeons.java b/DungeonsRedux/src/net/robotmedia/billing/example/Dungeons.java index b6ef8aa..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(); }