Browse files

Closes #17. Closes #13. Closes #6.

  • Loading branch information...
1 parent 43c874f commit 3a5e5f96a67635e8d42f8503c8a124e1367a1505 Robot Media committed Jul 13, 2011
Showing with 767 additions and 55 deletions.
  1. +22 −2 AndroidBillingLibrary/src/net/robotmedia/billing/BillingController.java
  2. +3 −2 AndroidBillingLibrary/src/net/robotmedia/billing/BillingService.java
  3. +5 −0 AndroidBillingLibrary/src/net/robotmedia/billing/IBillingObserver.java
  4. +2 −2 AndroidBillingLibrary/src/net/robotmedia/billing/model/Transaction.java
  5. +40 −31 AndroidBillingLibrary/src/net/robotmedia/billing/request/BillingRequest.java
  6. +0 −3 AndroidBillingLibrary/src/net/robotmedia/billing/request/CheckBillingSupported.java
  7. +0 −3 AndroidBillingLibrary/src/net/robotmedia/billing/request/ConfirmNotifications.java
  8. +0 −4 AndroidBillingLibrary/src/net/robotmedia/billing/request/GetPurchaseInformation.java
  9. +8 −6 AndroidBillingLibrary/src/net/robotmedia/billing/request/RestoreTransactions.java
  10. +10 −1 AndroidBillingLibrary/src/net/robotmedia/billing/utils/Compatibility.java
  11. +28 −0 AndroidBillingLibraryTest/src/net/robotmedia/billing/BillingControllerTest.java
  12. +5 −0 AndroidBillingLibraryTest/src/net/robotmedia/billing/MockBillingActivity.java
  13. +14 −0 AndroidBillingLibraryTest/src/net/robotmedia/billing/utils/CompatibilityTest.java
  14. +8 −0 DungeonsRedux/.classpath
  15. +2 −0 DungeonsRedux/.gitignore
  16. +40 −0 DungeonsRedux/.project
  17. +30 −0 DungeonsRedux/AndroidManifest.xml
  18. +56 −0 DungeonsRedux/README
  19. +12 −0 DungeonsRedux/default.properties
  20. BIN DungeonsRedux/res/drawable-hdpi/icon.png
  21. BIN DungeonsRedux/res/drawable-ldpi/icon.png
  22. BIN DungeonsRedux/res/drawable-mdpi/icon.png
  23. +32 −0 DungeonsRedux/res/layout/item_row.xml
  24. +74 −0 DungeonsRedux/res/layout/main.xml
  25. +23 −0 DungeonsRedux/res/values/colors.xml
  26. +42 −0 DungeonsRedux/res/values/strings.xml
  27. +72 −0 DungeonsRedux/src/net/robotmedia/billing/example/CatalogAdapter.java
  28. +28 −0 DungeonsRedux/src/net/robotmedia/billing/example/CatalogEntry.java
  29. +210 −0 DungeonsRedux/src/net/robotmedia/billing/example/Dungeons.java
  30. +1 −1 README.mdown
View
24 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<Long, BillingRequest> pendingRequests = new HashMap<Long, BillingRequest>();
+
/**
* 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();
+ }
+ }
+
}
View
5 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) {
View
5 AndroidBillingLibrary/src/net/robotmedia/billing/IBillingObserver.java
@@ -55,4 +55,9 @@
*/
public void onPurchaseRefunded(String itemId);
+ /**
+ * Called after a restore transactions request has been successfully received by the server.
+ */
+ public void onTransactionsRestored();
+
}
View
4 AndroidBillingLibrary/src/net/robotmedia/billing/model/Transaction.java
@@ -22,8 +22,8 @@
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
View
71 AndroidBillingLibrary/src/net/robotmedia/billing/request/BillingRequest.java
@@ -27,33 +27,53 @@
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;
- }
-
}
View
3 AndroidBillingLibrary/src/net/robotmedia/billing/request/CheckBillingSupported.java
@@ -30,9 +30,6 @@ public String getRequestType() {
}
@Override
- protected void addParams(Bundle request) {}
-
- @Override
protected void processOkResponse(Bundle response) {
final boolean supported = this.isSuccess();
BillingController.onBillingChecked(supported);
View
3 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) {}
}
View
4 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) {
- }
}
View
14 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);
+ }
+ }
}
View
11 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 @@
};
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) {
View
28 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<Transaction> transactions2 = BillingController.getTransactions(getContext(), TransactionTest.TRANSACTION_1.productId);
assertEquals(transactions2.size(), 1);
}
+
+ @SmallTest
+ public void testOnTransactionRestored() throws Exception {
+ final Set<Boolean> flags = new HashSet<Boolean>();
+ 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);
+ }
}
View
5 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
+ }
+
}
View
14 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);
+ }
+
+}
View
8 DungeonsRedux/.classpath
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+ <classpathentry kind="src" path="src"/>
+ <classpathentry kind="src" path="gen"/>
+ <classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
+ <classpathentry kind="src" path="AndroidBillingLibrary_src"/>
+ <classpathentry kind="output" path="bin"/>
+</classpath>
View
2 DungeonsRedux/.gitignore
@@ -0,0 +1,2 @@
+bin
+gen
View
40 DungeonsRedux/.project
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>DungeonsRedux</name>
+ <comment></comment>
+ <projects>
+ </projects>
+ <buildSpec>
+ <buildCommand>
+ <name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>org.eclipse.jdt.core.javabuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>com.android.ide.eclipse.adt.ApkBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>com.android.ide.eclipse.adt.AndroidNature</nature>
+ <nature>org.eclipse.jdt.core.javanature</nature>
+ </natures>
+ <linkedResources>
+ <link>
+ <name>AndroidBillingLibrary_src</name>
+ <type>2</type>
+ <locationURI>_android_AndroidBillingLibrary_70afcdef/src</locationURI>
+ </link>
+ </linkedResources>
+</projectDescription>
View
30 DungeonsRedux/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ android:versionCode="1" android:versionName="1.0"
+ package="net.robotmedia.billing.example">
+
+ <!-- Add this permission to your manifest -->
+ <uses-permission android:name="com.android.vending.BILLING" />
+
+ <application android:icon="@drawable/icon" android:label="@string/app_name">
+ <activity android:name=".Dungeons" android:label="@string/app_name">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+
+ <!-- Add this service and receiver to your application -->
+ <service android:name="net.robotmedia.billing.BillingService" />
+ <receiver android:name="net.robotmedia.billing.BillingReceiver">
+ <intent-filter>
+ <action android:name="com.android.vending.billing.IN_APP_NOTIFY" />
+ <action android:name="com.android.vending.billing.RESPONSE_CODE" />
+ <action android:name="com.android.vending.billing.PURCHASE_STATE_CHANGED" />
+ </intent-filter>
+ </receiver>
+
+ </application>
+ <uses-sdk android:minSdkVersion="4" />
+
+</manifest>
View
56 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
View
12 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
View
BIN DungeonsRedux/res/drawable-hdpi/icon.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN DungeonsRedux/res/drawable-ldpi/icon.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN DungeonsRedux/res/drawable-mdpi/icon.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
32 DungeonsRedux/res/layout/item_row.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/screen_background">
+ <TextView
+ android:id="@+id/item_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingLeft="10dip"
+ android:layout_alignParentLeft="true"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:textColor="@android:color/black" />
+</RelativeLayout>
View
74 DungeonsRedux/res/layout/main.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:paddingLeft="4dip"
+ android:paddingRight="4dip"
+ android:background="@color/screen_background">
+
+ <TextView
+ android:id="@+id/billing_supported"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="@color/error_message" />
+
+ <TextView
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="10dip"
+ android:textColor="@android:color/black"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:textStyle="bold"
+ android:text="@string/items_for_sale" />
+
+ <LinearLayout
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+ <Button
+ android:id="@+id/buy_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/buy" />
+ <Spinner
+ android:id="@+id/item_choices"
+ android:layout_width="0dip"
+ android:layout_weight="1"
+ android:layout_height="wrap_content"
+ android:prompt="@string/select_item" />
+ </LinearLayout>
+ <TextView
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="10dip"
+ android:textColor="@android:color/black"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:textStyle="bold"
+ android:text="@string/items_you_own" />
+ <ListView
+ android:id="@+id/owned_items"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="4dip"
+ android:paddingBottom="4dip">
+ </ListView>
+</LinearLayout>
View
23 DungeonsRedux/res/values/colors.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+
+<resources>
+ <color name="screen_background">#ffc0f0c0</color>
+ <color name="error_message">#ffc00000</color>
+</resources>
View
42 DungeonsRedux/res/values/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+
+<resources>
+ <string name="welcome">Welcome to the Sample store!</string>
+ <string name="app_name">Dungeons Redux</string>
+ <string name="billing_not_supported_title">Can\'t make purchases</string>
+ <string name="billing_not_supported_message">The Market billing
+ service is not available at this time. You can continue to use this app but you
+ won\'t be able to make purchases.</string>
+ <string name="restoring_transactions">Restoring transactions</string>
+ <string name="buy">Buy</string>
+ <string name="select_item">Select an item</string>
+ <string name="items_for_sale">Items for sale</string>
+ <string name="items_you_own">Items you own</string>
+ <string name="recent_transactions">Recent activity</string>
+
+ <string name="two_handed_sword">Two-handed sword</string>
+ <string name="potions">Potions</string>
+
+ <!-- The following are reserved skus defined by Market for testing. -->
+ <string name="android_test_canceled">android.test.canceled</string>
+ <string name="android_test_purchased">android.test.purchased</string>
+ <string name="android_test_item_unavailable">android.test.item unavailable</string>
+ <string name="android_test_refunded">android.test.refunded</string>
+</resources>
View
72 DungeonsRedux/src/net/robotmedia/billing/example/CatalogAdapter.java
@@ -0,0 +1,72 @@
+package net.robotmedia.billing.example;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import net.robotmedia.billing.example.CatalogEntry.Managed;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+
+/**
+ * An adapter used for displaying a catalog of products. If a product is
+ * managed by Android Market and already purchased, then it will be
+ * "grayed-out" in the list and not selectable.
+ */
+public class CatalogAdapter extends ArrayAdapter<String> {
+
+ private CatalogEntry[] mCatalog;
+ private List<String> mOwnedItems = new ArrayList<String>();
+
+ public CatalogAdapter(Context context, CatalogEntry[] catalog) {
+ super(context, android.R.layout.simple_spinner_item);
+ mCatalog = catalog;
+ for (CatalogEntry element : catalog) {
+ add(context.getString(element.nameId));
+ }
+ setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ }
+
+ @Override
+ public boolean areAllItemsEnabled() {
+ // Return false to have the adapter call isEnabled()
+ return false;
+ }
+
+ @Override
+ public View getDropDownView(int position, View convertView, ViewGroup parent) {
+ // If the item at the given list position is not purchasable, then
+ // "gray out" the list item.
+ View view = super.getDropDownView(position, convertView, parent);
+ view.setEnabled(isEnabled(position));
+ return view;
+ }
+
+ private boolean isPurchased(String sku) {
+ for (int i = 0; i < mOwnedItems.size(); i++) {
+ if (sku.equals(mOwnedItems.get(i))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ // If the item at the given list position is not purchasable,
+ // then prevent the list item from being selected.
+ CatalogEntry entry = mCatalog[position];
+ if (entry.managed == Managed.MANAGED && isPurchased(entry.sku)) {
+ return false;
+ }
+ return true;
+ }
+
+ public void setOwnedItems(List<String> ownedItems) {
+ mOwnedItems = ownedItems;
+ notifyDataSetChanged();
+ }
+
+}
View
28 DungeonsRedux/src/net/robotmedia/billing/example/CatalogEntry.java
@@ -0,0 +1,28 @@
+package net.robotmedia.billing.example;
+
+public class CatalogEntry {
+
+ /**
+ * Each product in the catalog is either MANAGED or UNMANAGED. MANAGED means
+ * that the product can be purchased only once per user (such as a new level
+ * in a game). The purchase is remembered by Android Market and can be
+ * restored if this application is uninstalled and then re-installed.
+ * UNMANAGED is used for products that can be used up and purchased multiple
+ * times (such as poker chips). It is up to the application to keep track of
+ * UNMANAGED products for the user.
+ */
+ public enum Managed {
+ MANAGED, UNMANAGED
+ }
+
+ public String sku;
+ public int nameId;
+ public Managed managed;
+
+ public CatalogEntry(String sku, int nameId, Managed managed) {
+ this.sku = sku;
+ this.nameId = nameId;
+ this.managed = managed;
+ }
+
+}
View
210 DungeonsRedux/src/net/robotmedia/billing/example/Dungeons.java
@@ -0,0 +1,210 @@
+package net.robotmedia.billing.example;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.ListView;
+import android.widget.Spinner;
+import android.widget.Toast;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import net.robotmedia.billing.BillingController;
+import net.robotmedia.billing.IBillingObserver;
+import net.robotmedia.billing.BillingController.IConfiguration;
+import net.robotmedia.billing.example.R;
+import net.robotmedia.billing.example.CatalogEntry.Managed;
+import net.robotmedia.billing.model.Transaction;
+import net.robotmedia.billing.model.Transaction.PurchaseState;
+
+/**
+ * A sample application based on the original Dungeons to demonstrate how to use
+ * BillingController and implement IBillingObserver.
+ */
+public class Dungeons extends Activity implements IBillingObserver, IConfiguration {
+
+ private static final String TAG = "Dungeons";
+
+ /**
+ * The SharedPreferences key for recording whether we initialized the
+ * database. If false, then we perform a RestoreTransactions request to get
+ * all the purchases for this user.
+ */
+ private static final String KEY_TRANSACTIONS_RESTORED = "transactionsRestored";
+ private Button mBuyButton;
+ private Spinner mSelectItemSpinner;
+ private ListView mOwnedItemsTable;
+
+ private static final int DIALOG_BILLING_NOT_SUPPORTED_ID = 2;
+
+ /** An array of product list entries for the products that can be purchased. */
+ private static final CatalogEntry[] CATALOG = new CatalogEntry[] {
+ new CatalogEntry("sword_001", R.string.two_handed_sword, Managed.MANAGED),
+ new CatalogEntry("potion_001", R.string.potions, Managed.UNMANAGED),
+ new CatalogEntry("android.test.purchased", R.string.android_test_purchased, Managed.UNMANAGED),
+ new CatalogEntry("android.test.canceled", R.string.android_test_canceled, Managed.UNMANAGED),
+ new CatalogEntry("android.test.refunded", R.string.android_test_refunded, Managed.UNMANAGED),
+ new CatalogEntry("android.test.item_unavailable", R.string.android_test_item_unavailable, Managed.UNMANAGED), };
+ private String mSku;
+
+ private CatalogAdapter mCatalogAdapter;
+
+ private Dialog createDialog(int titleId, int messageId) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(titleId).setIcon(android.R.drawable.stat_sys_warning).setMessage(messageId).setCancelable(
+ false).setPositiveButton(android.R.string.ok, null);
+ return builder.create();
+ }
+
+ private void updateOwnedItems() {
+ List<Transaction> transactions = BillingController.getTransactions(this);
+ final ArrayList<String> ownedItems = new ArrayList<String>();
+ for (Transaction t : transactions) {
+ if (t.purchaseState == PurchaseState.PURCHASED) {
+ ownedItems.add(t.productId);
+ }
+ }
+
+ mCatalogAdapter.setOwnedItems(ownedItems);
+ final ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, R.layout.item_row, R.id.item_name, ownedItems);
+ mOwnedItemsTable.setAdapter(adapter);
+ }
+
+ @Override
+ public void onBillingChecked(boolean supported) {
+ if (supported) {
+ restoreTransactions();
+ mBuyButton.setEnabled(true);
+ } else {
+ showDialog(DIALOG_BILLING_NOT_SUPPORTED_ID);
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.main);
+
+ setupWidgets();
+ BillingController.setDebug(true);
+ BillingController.setConfiguration(this);
+ BillingController.registerObserver(this);
+ BillingController.checkBillingSupported(this);
+ updateOwnedItems();
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int id) {
+ switch (id) {
+ case DIALOG_BILLING_NOT_SUPPORTED_ID:
+ return createDialog(R.string.billing_not_supported_title, R.string.billing_not_supported_message);
+ default:
+ return null;
+ }
+ }
+
+ @Override
+ public void onPurchaseCancelled(String itemId) {
+ Log.i(TAG, "onPurchaseCancelled() itemId: " + itemId);
+ }
+
+ @Override
+ public void onPurchaseExecuted(String itemId) {
+ Log.i(TAG, "onPurchaseExecuted() itemId: " + itemId);
+ updateOwnedItems();
+ }
+
+ @Override
+ public void onPurchaseIntent(String itemId, PendingIntent purchaseIntent) {
+ BillingController.startPurchaseIntent(this, purchaseIntent, new Intent());
+ }
+
+ @Override
+ public void onPurchaseRefunded(String itemId) {
+ Log.i(TAG, "onPurchaseRefunded() itemId: " + itemId);
+ }
+
+ @Override
+ protected void onDestroy() {
+ BillingController.unregisterObserver(this);
+ BillingController.setConfiguration(null);
+ super.onDestroy();
+ }
+
+ @Override
+ public void onTransactionsRestored() {
+ Log.d(TAG, "completed RestoreTransactions request");
+ // Update the shared preferences so that we don't perform
+ // a restore transactions again.
+ SharedPreferences prefs = getPreferences(Context.MODE_PRIVATE);
+ SharedPreferences.Editor edit = prefs.edit();
+ edit.putBoolean(KEY_TRANSACTIONS_RESTORED, true);
+ edit.commit();
+ }
+
+ /**
+ * Restores previous transactions, if any. This
+ * happens if the application has just been installed or the user wiped
+ * data. We do not want to do this on every startup, rather, we want to do
+ * only when the database needs to be initialized.
+ */
+ private void restoreTransactions() {
+ SharedPreferences prefs = getPreferences(MODE_PRIVATE);
+ boolean initialized = prefs.getBoolean(KEY_TRANSACTIONS_RESTORED, false);
+ if (!initialized) {
+ BillingController.restoreTransactions(this);
+ Toast.makeText(this, R.string.restoring_transactions, Toast.LENGTH_LONG).show();
+ }
+ }
+
+ private void setupWidgets() {
+ mBuyButton = (Button) findViewById(R.id.buy_button);
+ mBuyButton.setEnabled(false);
+ mBuyButton.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ BillingController.requestPurchase(Dungeons.this, mSku, true /*confirm*/);
+ }
+ });
+
+ mSelectItemSpinner = (Spinner) findViewById(R.id.item_choices);
+ mCatalogAdapter = new CatalogAdapter(this, CATALOG);
+ mSelectItemSpinner.setAdapter(mCatalogAdapter);
+ mSelectItemSpinner.setOnItemSelectedListener(new OnItemSelectedListener() {
+
+ public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+ mSku = CATALOG[position].sku;
+ }
+
+ public void onNothingSelected(AdapterView<?> arg0) {
+ }
+
+ });
+
+ mOwnedItemsTable = (ListView) findViewById(R.id.owned_items);
+ }
+
+ @Override
+ public byte[] getObfuscationSalt() {
+ return new byte[] {41, -90, -116, -41, 66, -53, 122, -110, -127, -96, -88, 77, 127, 115, 1, 73, 57, 110, 48, -116};
+ }
+
+ @Override
+ public String getPublicKey() {
+ return "your public key here";
+ }
+}
View
2 README.mdown
@@ -54,7 +54,7 @@ BillingController
[BillingController][3] provides high-level functions to interact with the Billing service and to query an obfuscated local transaction database.
-Since most billing functions are asynchronous, [BillingController][3] notifies all registered [IBillingObserver][4] of the responses. Additionally, [BillingController][3] requires a _BillingController.IConfiguration_ instance from which the public key required to validate signed messages and a salt to obfuscate transactions are obtained.
+Since most billing functions are asynchronous, [BillingController][3] notifies all registered [IBillingObserver][4] of the responses. Additionally, [BillingController][3] requires a `BillingController.IConfiguration` instance from which the public key required to validate signed messages and a salt to obfuscate transactions are obtained.
Disclaimer
==========

0 comments on commit 3a5e5f9

Please sign in to comment.