From 3a5e5f96a67635e8d42f8503c8a124e1367a1505 Mon Sep 17 00:00:00 2001 From: Robot Media Date: Wed, 13 Jul 2011 13:07:47 +0200 Subject: [PATCH] Closes #17. Closes #13. Closes #6. --- .../robotmedia/billing/BillingController.java | 24 +- .../robotmedia/billing/BillingService.java | 5 +- .../robotmedia/billing/IBillingObserver.java | 5 + .../robotmedia/billing/model/Transaction.java | 4 +- .../billing/request/BillingRequest.java | 71 +++--- .../request/CheckBillingSupported.java | 3 - .../billing/request/ConfirmNotifications.java | 3 - .../request/GetPurchaseInformation.java | 4 - .../billing/request/RestoreTransactions.java | 14 +- .../billing/utils/Compatibility.java | 11 +- .../billing/BillingControllerTest.java | 28 +++ .../billing/MockBillingActivity.java | 5 + .../billing/utils/CompatibilityTest.java | 14 ++ DungeonsRedux/.classpath | 8 + DungeonsRedux/.gitignore | 2 + DungeonsRedux/.project | 40 ++++ DungeonsRedux/AndroidManifest.xml | 30 +++ DungeonsRedux/README | 56 +++++ DungeonsRedux/default.properties | 12 + DungeonsRedux/res/drawable-hdpi/icon.png | Bin 0 -> 4432 bytes DungeonsRedux/res/drawable-ldpi/icon.png | Bin 0 -> 1502 bytes DungeonsRedux/res/drawable-mdpi/icon.png | Bin 0 -> 2591 bytes DungeonsRedux/res/layout/item_row.xml | 32 +++ DungeonsRedux/res/layout/main.xml | 74 ++++++ DungeonsRedux/res/values/colors.xml | 23 ++ DungeonsRedux/res/values/strings.xml | 42 ++++ .../billing/example/CatalogAdapter.java | 72 ++++++ .../billing/example/CatalogEntry.java | 28 +++ .../robotmedia/billing/example/Dungeons.java | 210 ++++++++++++++++++ README.mdown | 2 +- 30 files changed, 767 insertions(+), 55 deletions(-) create mode 100644 AndroidBillingLibraryTest/src/net/robotmedia/billing/utils/CompatibilityTest.java create mode 100644 DungeonsRedux/.classpath create mode 100644 DungeonsRedux/.gitignore create mode 100644 DungeonsRedux/.project create mode 100644 DungeonsRedux/AndroidManifest.xml create mode 100755 DungeonsRedux/README create mode 100644 DungeonsRedux/default.properties create mode 100644 DungeonsRedux/res/drawable-hdpi/icon.png create mode 100644 DungeonsRedux/res/drawable-ldpi/icon.png create mode 100644 DungeonsRedux/res/drawable-mdpi/icon.png create mode 100644 DungeonsRedux/res/layout/item_row.xml create mode 100644 DungeonsRedux/res/layout/main.xml create mode 100644 DungeonsRedux/res/values/colors.xml create mode 100644 DungeonsRedux/res/values/strings.xml create mode 100644 DungeonsRedux/src/net/robotmedia/billing/example/CatalogAdapter.java create mode 100644 DungeonsRedux/src/net/robotmedia/billing/example/CatalogEntry.java create mode 100644 DungeonsRedux/src/net/robotmedia/billing/example/Dungeons.java diff --git a/AndroidBillingLibrary/src/net/robotmedia/billing/BillingController.java b/AndroidBillingLibrary/src/net/robotmedia/billing/BillingController.java index 430ea83..61e898f 100644 --- a/AndroidBillingLibrary/src/net/robotmedia/billing/BillingController.java +++ b/AndroidBillingLibrary/src/net/robotmedia/billing/BillingController.java @@ -29,6 +29,7 @@ import net.robotmedia.billing.model.TransactionManager; import net.robotmedia.billing.request.BillingRequest; import net.robotmedia.billing.request.ResponseCode; +import net.robotmedia.billing.request.RestoreTransactions; import net.robotmedia.billing.utils.Compatibility; import net.robotmedia.billing.utils.Security; @@ -427,6 +428,8 @@ protected static void onPurchaseStateChanged(Context context, String signedData, } } + private static HashMap pendingRequests = new HashMap(); + /** * Called after a {@link net.robotmedia.billing.request.BillingRequest} is * sent. @@ -440,14 +443,16 @@ protected static void onRequestSent(long requestId, BillingRequest request) { if (debug) { Log.d(BillingController.class.getSimpleName(), "Request " + requestId + " of type " + request.getRequestType() + " sent"); } - if (!request.isSuccess() && request.hasNonce()) { + if (request.isSuccess()) { + pendingRequests.put(requestId, request); + } else if (request.hasNonce()) { Security.removeNonce(request.getNonce()); } } /** * Called after a {@link net.robotmedia.billing.request.BillingRequest} is - * sent. Mostly used for debugging purposes. + * sent. * * @param context * @param requestId @@ -460,6 +465,11 @@ protected static void onResponseCode(Context context, long requestId, int respon if (debug) { Log.d(BillingController.class.getSimpleName(), "Request " + requestId + " received response " + ResponseCode.valueOf(responseCode)); } + final BillingRequest request = pendingRequests.get(requestId); + if (request != null) { + pendingRequests.remove(requestId); + request.onResponseCode(responseCode); + } } /** @@ -639,4 +649,14 @@ private static boolean verifyNonce(JSONObject data) { } } + /** + * + * @param restoreTransactions + */ + public static void onTransactionsRestored(RestoreTransactions restoreTransactions) { + for (IBillingObserver o : observers) { + o.onTransactionsRestored(); + } + } + } diff --git a/AndroidBillingLibrary/src/net/robotmedia/billing/BillingService.java b/AndroidBillingLibrary/src/net/robotmedia/billing/BillingService.java index 6438520..a217cea 100644 --- a/AndroidBillingLibrary/src/net/robotmedia/billing/BillingService.java +++ b/AndroidBillingLibrary/src/net/robotmedia/billing/BillingService.java @@ -23,6 +23,7 @@ import net.robotmedia.billing.request.RequestPurchase; import net.robotmedia.billing.request.RestoreTransactions; import net.robotmedia.billing.request.BillingRequest; +import net.robotmedia.billing.utils.Compatibility; import com.android.vending.billing.IMarketBillingService; @@ -163,10 +164,10 @@ public void onStart(Intent intent, int startId) { handleCommand(intent); } - @Override + // @Override // Avoid compile errors on pre-2.0 public int onStartCommand(Intent intent, int flags, int startId) { handleCommand(intent); - return START_NOT_STICKY; + return Compatibility.START_NOT_STICKY; } private void handleCommand(Intent intent) { diff --git a/AndroidBillingLibrary/src/net/robotmedia/billing/IBillingObserver.java b/AndroidBillingLibrary/src/net/robotmedia/billing/IBillingObserver.java index 2dbadc9..f8b8380 100644 --- a/AndroidBillingLibrary/src/net/robotmedia/billing/IBillingObserver.java +++ b/AndroidBillingLibrary/src/net/robotmedia/billing/IBillingObserver.java @@ -55,4 +55,9 @@ public interface IBillingObserver { */ public void onPurchaseRefunded(String itemId); + /** + * Called after a restore transactions request has been successfully received by the server. + */ + public void onTransactionsRestored(); + } diff --git a/AndroidBillingLibrary/src/net/robotmedia/billing/model/Transaction.java b/AndroidBillingLibrary/src/net/robotmedia/billing/model/Transaction.java index f81e173..54f2daa 100644 --- a/AndroidBillingLibrary/src/net/robotmedia/billing/model/Transaction.java +++ b/AndroidBillingLibrary/src/net/robotmedia/billing/model/Transaction.java @@ -22,8 +22,8 @@ public class Transaction { public enum PurchaseState { // Responses to requestPurchase or restoreTransactions. - PURCHASED, // 0: The charge failed on the server. - CANCELLED, // 1: User was charged for the order. + PURCHASED, // 0: User was charged for the order. + CANCELLED, // 1: The charge failed on the server. REFUNDED; // 2: User received a refund for the order. // Converts from an ordinal value to the PurchaseState diff --git a/AndroidBillingLibrary/src/net/robotmedia/billing/request/BillingRequest.java b/AndroidBillingLibrary/src/net/robotmedia/billing/request/BillingRequest.java index b7d5b65..45a254c 100644 --- a/AndroidBillingLibrary/src/net/robotmedia/billing/request/BillingRequest.java +++ b/AndroidBillingLibrary/src/net/robotmedia/billing/request/BillingRequest.java @@ -27,33 +27,53 @@ public abstract class BillingRequest { private static final String KEY_API_VERSION = "API_VERSION"; private static final String KEY_PACKAGE_NAME = "PACKAGE_NAME"; private static final String KEY_RESPONSE_CODE = "RESPONSE_CODE"; - private static final String KEY_REQUEST_ID = "REQUEST_ID"; + protected static final String KEY_REQUEST_ID = "REQUEST_ID"; private static final String KEY_NONCE = "NONCE"; public static final long IGNORE_REQUEST_ID = -1; - public abstract String getRequestType(); - protected abstract void addParams(Bundle request); - protected abstract void processOkResponse(Bundle response); + private String packageName; + private boolean success; + private long nonce; - public boolean hasNonce() { - return false; + public BillingRequest(String packageName) { + this.packageName = packageName; } - private String packageName; - private boolean success; - private long nonce; + protected void addParams(Bundle request) { + // Do nothing by default + } public long getNonce() { return nonce; } - public void setNonce(long nonce) { - this.nonce = nonce; - } - public BillingRequest(String packageName) { - this.packageName = packageName; + public abstract String getRequestType(); + + public boolean hasNonce() { + return false; + } + public boolean isSuccess() { + return success; + } + protected Bundle makeRequestBundle() { + final Bundle request = new Bundle(); + request.putString(KEY_BILLING_REQUEST, getRequestType()); + request.putInt(KEY_API_VERSION, 1); + request.putString(KEY_PACKAGE_NAME, packageName); + if (hasNonce()) { + request.putLong(KEY_NONCE, nonce); + } + return request; } - public long run(IMarketBillingService mService) throws RemoteException { + public void onResponseCode(int responseCode) { + // Do nothing by default + } + + protected void processOkResponse(Bundle response) { + // Do nothing by default + } + + public long run(IMarketBillingService mService) throws RemoteException { final Bundle request = makeRequestBundle(); addParams(request); final Bundle response = mService.sendBillingRequest(request); @@ -64,18 +84,11 @@ public long run(IMarketBillingService mService) throws RemoteException { return IGNORE_REQUEST_ID; } } - - protected Bundle makeRequestBundle() { - final Bundle request = new Bundle(); - request.putString(KEY_BILLING_REQUEST, getRequestType()); - request.putInt(KEY_API_VERSION, 1); - request.putString(KEY_PACKAGE_NAME, packageName); - if (hasNonce()) { - request.putLong(KEY_NONCE, nonce); - } - return request; - } - + + public void setNonce(long nonce) { + this.nonce = nonce; + } + protected boolean validateResponse(Bundle response) { final int responseCode = response.getInt(KEY_RESPONSE_CODE); success = ResponseCode.isResponseOk(responseCode); @@ -85,8 +98,4 @@ protected boolean validateResponse(Bundle response) { return success; } - public boolean isSuccess() { - return success; - } - } \ No newline at end of file diff --git a/AndroidBillingLibrary/src/net/robotmedia/billing/request/CheckBillingSupported.java b/AndroidBillingLibrary/src/net/robotmedia/billing/request/CheckBillingSupported.java index c4b773b..05fb7ed 100644 --- a/AndroidBillingLibrary/src/net/robotmedia/billing/request/CheckBillingSupported.java +++ b/AndroidBillingLibrary/src/net/robotmedia/billing/request/CheckBillingSupported.java @@ -29,9 +29,6 @@ public String getRequestType() { return "CHECK_BILLING_SUPPORTED"; } - @Override - protected void addParams(Bundle request) {} - @Override protected void processOkResponse(Bundle response) { final boolean supported = this.isSuccess(); diff --git a/AndroidBillingLibrary/src/net/robotmedia/billing/request/ConfirmNotifications.java b/AndroidBillingLibrary/src/net/robotmedia/billing/request/ConfirmNotifications.java index c10eaed..0034ece 100644 --- a/AndroidBillingLibrary/src/net/robotmedia/billing/request/ConfirmNotifications.java +++ b/AndroidBillingLibrary/src/net/robotmedia/billing/request/ConfirmNotifications.java @@ -37,8 +37,5 @@ public String getRequestType() { protected void addParams(Bundle request) { request.putStringArray(KEY_NOTIFY_IDS, notifyIds); } - - @Override - protected void processOkResponse(Bundle response) {} } diff --git a/AndroidBillingLibrary/src/net/robotmedia/billing/request/GetPurchaseInformation.java b/AndroidBillingLibrary/src/net/robotmedia/billing/request/GetPurchaseInformation.java index 1feab1b..b602c72 100644 --- a/AndroidBillingLibrary/src/net/robotmedia/billing/request/GetPurchaseInformation.java +++ b/AndroidBillingLibrary/src/net/robotmedia/billing/request/GetPurchaseInformation.java @@ -39,9 +39,5 @@ public String getRequestType() { protected void addParams(Bundle request) { request.putStringArray(KEY_NOTIFY_IDS, notifyIds); } - - @Override - protected void processOkResponse(Bundle response) { - } } diff --git a/AndroidBillingLibrary/src/net/robotmedia/billing/request/RestoreTransactions.java b/AndroidBillingLibrary/src/net/robotmedia/billing/request/RestoreTransactions.java index 36a6238..12cd522 100644 --- a/AndroidBillingLibrary/src/net/robotmedia/billing/request/RestoreTransactions.java +++ b/AndroidBillingLibrary/src/net/robotmedia/billing/request/RestoreTransactions.java @@ -15,7 +15,7 @@ package net.robotmedia.billing.request; -import android.os.Bundle; +import net.robotmedia.billing.BillingController; public class RestoreTransactions extends BillingRequest { @@ -29,11 +29,13 @@ public RestoreTransactions(String packageName) { public String getRequestType() { return "RESTORE_TRANSACTIONS"; } - - @Override - protected void addParams(Bundle request) {} - + @Override - protected void processOkResponse(Bundle response) {} + public void onResponseCode(int responseCode) { + super.onResponseCode(responseCode); + if (ResponseCode.isResponseOk(responseCode)) { + BillingController.onTransactionsRestored(this); + } + } } diff --git a/AndroidBillingLibrary/src/net/robotmedia/billing/utils/Compatibility.java b/AndroidBillingLibrary/src/net/robotmedia/billing/utils/Compatibility.java index 40f1f10..9942636 100644 --- a/AndroidBillingLibrary/src/net/robotmedia/billing/utils/Compatibility.java +++ b/AndroidBillingLibrary/src/net/robotmedia/billing/utils/Compatibility.java @@ -15,15 +15,18 @@ package net.robotmedia.billing.utils; +import java.lang.reflect.Field; import java.lang.reflect.Method; import android.app.Activity; +import android.app.Service; import android.content.Intent; import android.content.IntentSender; import android.util.Log; public class Compatibility { private static Method startIntentSender; + public static int START_NOT_STICKY; @SuppressWarnings("rawtypes") private static final Class[] START_INTENT_SENDER_SIG = new Class[] { IntentSender.class, Intent.class, int.class, int.class, int.class @@ -34,7 +37,13 @@ public class Compatibility { }; private static void initCompatibility() { - try { + try { + final Field field = Service.class.getField("START_NOT_STICKY"); + START_NOT_STICKY = field.getInt(null); + } catch (Exception e) { + START_NOT_STICKY = 2; + } + try { startIntentSender = Activity.class.getMethod("startIntentSender", START_INTENT_SENDER_SIG); } catch (SecurityException e) { diff --git a/AndroidBillingLibraryTest/src/net/robotmedia/billing/BillingControllerTest.java b/AndroidBillingLibraryTest/src/net/robotmedia/billing/BillingControllerTest.java index 570656c..87ff194 100644 --- a/AndroidBillingLibraryTest/src/net/robotmedia/billing/BillingControllerTest.java +++ b/AndroidBillingLibraryTest/src/net/robotmedia/billing/BillingControllerTest.java @@ -15,14 +15,19 @@ package net.robotmedia.billing; +import java.util.HashSet; import java.util.List; +import java.util.Set; import net.robotmedia.billing.model.BillingDB; import net.robotmedia.billing.model.BillingDBTest; import net.robotmedia.billing.model.Transaction; import net.robotmedia.billing.model.TransactionTest; +import net.robotmedia.billing.request.RestoreTransactions; +import android.app.PendingIntent; import android.test.AndroidTestCase; import android.test.suitebuilder.annotation.MediumTest; +import android.test.suitebuilder.annotation.SmallTest; public class BillingControllerTest extends AndroidTestCase { @@ -99,4 +104,27 @@ public void testGetTransactionsString() throws Exception { final List transactions2 = BillingController.getTransactions(getContext(), TransactionTest.TRANSACTION_1.productId); assertEquals(transactions2.size(), 1); } + + @SmallTest + public void testOnTransactionRestored() throws Exception { + final Set flags = new HashSet(); + final IBillingObserver observer = new IBillingObserver() { + + @Override + public void onTransactionsRestored() { + flags.add(true); + } + + public void onPurchaseRefunded(String itemId) {} + public void onPurchaseIntent(String itemId, PendingIntent purchaseIntent) {} + public void onPurchaseExecuted(String itemId) {} + public void onPurchaseCancelled(String itemId) {} + public void onBillingChecked(boolean supported) {} + }; + BillingController.registerObserver(observer); + final RestoreTransactions request = new RestoreTransactions(getContext().getPackageName()); + BillingController.onTransactionsRestored(request); + assertEquals(flags.size(), 1); + BillingController.unregisterObserver(observer); + } } diff --git a/AndroidBillingLibraryTest/src/net/robotmedia/billing/MockBillingActivity.java b/AndroidBillingLibraryTest/src/net/robotmedia/billing/MockBillingActivity.java index 0bf1388..7069753 100644 --- a/AndroidBillingLibraryTest/src/net/robotmedia/billing/MockBillingActivity.java +++ b/AndroidBillingLibraryTest/src/net/robotmedia/billing/MockBillingActivity.java @@ -53,4 +53,9 @@ public String getPublicKey() { return null; } + @Override + public void onTransactionsRestored() { + // TODO Auto-generated method stub + } + } diff --git a/AndroidBillingLibraryTest/src/net/robotmedia/billing/utils/CompatibilityTest.java b/AndroidBillingLibraryTest/src/net/robotmedia/billing/utils/CompatibilityTest.java new file mode 100644 index 0000000..24448c8 --- /dev/null +++ b/AndroidBillingLibraryTest/src/net/robotmedia/billing/utils/CompatibilityTest.java @@ -0,0 +1,14 @@ +package net.robotmedia.billing.utils; + +import android.app.Service; +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.SmallTest; + +public class CompatibilityTest extends AndroidTestCase { + + @SmallTest + public void testStartNotSticky() throws Exception { + assertEquals(Compatibility.START_NOT_STICKY, Service.START_NOT_STICKY); + } + +} diff --git a/DungeonsRedux/.classpath b/DungeonsRedux/.classpath new file mode 100644 index 0000000..430cfd8 --- /dev/null +++ b/DungeonsRedux/.classpath @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/DungeonsRedux/.gitignore b/DungeonsRedux/.gitignore new file mode 100644 index 0000000..703f4e4 --- /dev/null +++ b/DungeonsRedux/.gitignore @@ -0,0 +1,2 @@ +bin +gen \ No newline at end of file diff --git a/DungeonsRedux/.project b/DungeonsRedux/.project new file mode 100644 index 0000000..d02b7a2 --- /dev/null +++ b/DungeonsRedux/.project @@ -0,0 +1,40 @@ + + + DungeonsRedux + + + + + + com.android.ide.eclipse.adt.ResourceManagerBuilder + + + + + com.android.ide.eclipse.adt.PreCompilerBuilder + + + + + org.eclipse.jdt.core.javabuilder + + + + + com.android.ide.eclipse.adt.ApkBuilder + + + + + + com.android.ide.eclipse.adt.AndroidNature + org.eclipse.jdt.core.javanature + + + + AndroidBillingLibrary_src + 2 + _android_AndroidBillingLibrary_70afcdef/src + + + diff --git a/DungeonsRedux/AndroidManifest.xml b/DungeonsRedux/AndroidManifest.xml new file mode 100644 index 0000000..014a946 --- /dev/null +++ b/DungeonsRedux/AndroidManifest.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DungeonsRedux/README b/DungeonsRedux/README new file mode 100755 index 0000000..7bd7bab --- /dev/null +++ b/DungeonsRedux/README @@ -0,0 +1,56 @@ +This package contains a sample app -Dungeons Redux- that shows how to use the Android Billing +Library (https://github.com/robotmedia/AndroidBillingLibrary). It does not intend to be an +example of how to use in-app billing in general. + +Dungeons Redux is a simplified version of the Dungeons sample app provided by Google. + +------------------------------------- +Configuring the sample application +------------------------------------- + +Before you can run the sample application, you need to add your Android Market public key to the +sample application code. This enables the application to verify the signature of the transaction +information that's returned from Android Market. To add your public key to the sample application +code, do the following: + +1. Log in to your Android Market publisher account (http://market.android.com/publish). +2. On the upper left part of the page, under your name, click Edit Profile. +3. On the Edit Profile page, scroll down to the Licensing & In-app Billing panel. +4. Copy your public key to the clipboard. +5. Open src/net/robotmedia/billing/example/Dungeons.java in the editor of your choice. +6. Search for the following line: + return "your public key here"; +7. Replace the string with your public key. +8. Save the file. + +After you add your public key to the Dungeons.java file, you can compile the application as you +normally would. + +------------------------------------- +Running the sample application +------------------------------------- + +You cannot run the sample application in the emulator. You must load the application onto a device +to run it. + +In-app billing requires version 2.3.0 of the Android Market application. To run the sample +application you must have this version (or a newer version) installed on your device. You can check +the version of the Android Market application by doing the following: + +1. Open Settings on your device and touch Applications. +2. In Application Settings, touch Manage applications. +3. Touch All to list all applications. +4. Scroll down and touch the Market application. +5. The version number appears under Market at the top of the screen. + +------------------------------------- +Additional information and resources +------------------------------------- + +To learn more about the in-app billing service, see the online documentation: + + http://developer.android.com/guide/market/billing/index.html + +To learn more about Android Billing Library, see: + + https://github.com/robotmedia/AndroidBillingLibrary \ No newline at end of file diff --git a/DungeonsRedux/default.properties b/DungeonsRedux/default.properties new file mode 100644 index 0000000..4271070 --- /dev/null +++ b/DungeonsRedux/default.properties @@ -0,0 +1,12 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system use, +# "build.properties", and override values to adapt the script to your +# project structure. + +# Project target. +target=android-4 +android.library.reference.1=../AndroidBillingLibrary diff --git a/DungeonsRedux/res/drawable-hdpi/icon.png b/DungeonsRedux/res/drawable-hdpi/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..2fe7c018e49d4eb8782a69d2044aca75919a8a60 GIT binary patch literal 4432 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 Eg0OJnUH||9 literal 0 HcmV?d00001 diff --git a/DungeonsRedux/res/drawable-mdpi/icon.png b/DungeonsRedux/res/drawable-mdpi/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f69aa5108f6aa48985d8125f40f686ea6c31aa4c GIT binary patch literal 2591 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 + + + + + diff --git a/DungeonsRedux/res/layout/main.xml b/DungeonsRedux/res/layout/main.xml new file mode 100644 index 0000000..40a2b9a --- /dev/null +++ b/DungeonsRedux/res/layout/main.xml @@ -0,0 +1,74 @@ + + + + + + + + + + +