aboutsummaryrefslogtreecommitdiff
path: root/src/org/fox/ttrss/billing
diff options
context:
space:
mode:
authorAndrew Dolgov <fox@madoka.volgo-balt.ru>2012-06-19 14:18:00 +0400
committerAndrew Dolgov <fox@madoka.volgo-balt.ru>2012-06-19 14:18:00 +0400
commit08397a47af403d64a012a7961e7444254ccaa9a2 (patch)
treeac3f0a2ad3a391fd60f2da3a69211d5f27c3a79b /src/org/fox/ttrss/billing
parent01151df966ed0006246790645b0f3e06c2ca94b8 (diff)
categorize source files
Diffstat (limited to 'src/org/fox/ttrss/billing')
-rw-r--r--src/org/fox/ttrss/billing/BillingConstants.java63
-rw-r--r--src/org/fox/ttrss/billing/BillingHelper.java268
-rw-r--r--src/org/fox/ttrss/billing/BillingReceiver.java57
-rw-r--r--src/org/fox/ttrss/billing/BillingSecurity.java258
-rw-r--r--src/org/fox/ttrss/billing/BillingService.java60
5 files changed, 706 insertions, 0 deletions
diff --git a/src/org/fox/ttrss/billing/BillingConstants.java b/src/org/fox/ttrss/billing/BillingConstants.java
new file mode 100644
index 00000000..eb440219
--- /dev/null
+++ b/src/org/fox/ttrss/billing/BillingConstants.java
@@ -0,0 +1,63 @@
+package org.fox.ttrss.billing;
+
+
+public class BillingConstants {
+
+ // The response codes for a request, defined by Android Market.
+ public enum ResponseCode {
+ RESULT_OK,
+ RESULT_USER_CANCELED,
+ RESULT_SERVICE_UNAVAILABLE,
+ RESULT_BILLING_UNAVAILABLE,
+ RESULT_ITEM_UNAVAILABLE,
+ RESULT_DEVELOPER_ERROR,
+ RESULT_ERROR;
+
+ // Converts from an ordinal value to the ResponseCode
+ public static ResponseCode valueOf(int index) {
+ ResponseCode[] values = ResponseCode.values();
+ if (index < 0 || index >= values.length) {
+ return RESULT_ERROR;
+ }
+ return values[index];
+ }
+ }
+
+ // The possible states of an in-app purchase, as defined by Android Market.
+ public enum PurchaseState {
+ // Responses to requestPurchase or restoreTransactions.
+ PURCHASED, // User was charged for the order.
+ CANCELED, // The charge failed on the server.
+ REFUNDED; // User received a refund for the order.
+
+ // Converts from an ordinal value to the PurchaseState
+ public static PurchaseState valueOf(int index) {
+ PurchaseState[] values = PurchaseState.values();
+ if (index < 0 || index >= values.length) {
+ return CANCELED;
+ }
+ return values[index];
+ }
+ }
+
+ // These are the names of the extras that are passed in an intent from
+ // Market to this application and cannot be changed.
+ public static final String NOTIFICATION_ID = "notification_id";
+ public static final String INAPP_SIGNED_DATA = "inapp_signed_data";
+ public static final String INAPP_SIGNATURE = "inapp_signature";
+ public static final String INAPP_REQUEST_ID = "request_id";
+ public static final String INAPP_RESPONSE_CODE = "response_code";
+
+ // Intent actions that we send from the BillingReceiver to the
+ // BillingService. Defined by this application.
+ public static final String ACTION_CONFIRM_NOTIFICATION = "com.example.dungeons.CONFIRM_NOTIFICATION";
+ public static final String ACTION_GET_PURCHASE_INFORMATION = "com.example.dungeons.GET_PURCHASE_INFORMATION";
+ public static final String ACTION_RESTORE_TRANSACTIONS = "com.example.dungeons.RESTORE_TRANSACTIONS";
+
+ // Intent actions that we receive in the BillingReceiver from Market.
+ // These are defined by Market and cannot be changed.
+ public static final String ACTION_NOTIFY = "com.android.vending.billing.IN_APP_NOTIFY";
+ public static final String ACTION_RESPONSE_CODE = "com.android.vending.billing.RESPONSE_CODE";
+ public static final String ACTION_PURCHASE_STATE_CHANGED = "com.android.vending.billing.PURCHASE_STATE_CHANGED";
+
+}
diff --git a/src/org/fox/ttrss/billing/BillingHelper.java b/src/org/fox/ttrss/billing/BillingHelper.java
new file mode 100644
index 00000000..dcf29322
--- /dev/null
+++ b/src/org/fox/ttrss/billing/BillingHelper.java
@@ -0,0 +1,268 @@
+package org.fox.ttrss.billing;
+
+import java.util.ArrayList;
+
+import android.app.PendingIntent;
+import android.app.PendingIntent.CanceledException;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.vending.billing.IMarketBillingService;
+
+import org.fox.ttrss.billing.BillingConstants.ResponseCode;
+import org.fox.ttrss.billing.BillingSecurity.VerifiedPurchase;
+
+public class BillingHelper {
+
+ private static final String TAG = "BillingService";
+
+ private static IMarketBillingService mService;
+ private static Context mContext;
+ private static Handler mCompletedHandler;
+
+ protected static VerifiedPurchase latestPurchase;
+
+ protected static void instantiateHelper(Context context, IMarketBillingService service) {
+ mService = service;
+ mContext = context;
+ }
+
+ protected static void setCompletedHandler(Handler handler){
+ mCompletedHandler = handler;
+ }
+
+ public static boolean isBillingSupported() {
+ if (amIDead()) {
+ return false;
+ }
+ Bundle request = makeRequestBundle("CHECK_BILLING_SUPPORTED");
+ if (mService != null) {
+ try {
+ Bundle response = mService.sendBillingRequest(request);
+ ResponseCode code = ResponseCode.valueOf((Integer) response.get("RESPONSE_CODE"));
+ Log.i(TAG, "isBillingSupported response was: " + code.toString());
+ if (ResponseCode.RESULT_OK.equals(code)) {
+ return true;
+ } else {
+ return false;
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "isBillingSupported response was: RemoteException", e);
+ return false;
+ }
+ } else {
+ Log.i(TAG, "isBillingSupported response was: BillingService.mService = null");
+ return false;
+ }
+ }
+
+ /**
+ * A REQUEST_PURCHASE request also triggers two asynchronous responses (broadcast intents).
+ * First, the Android Market application sends a RESPONSE_CODE broadcast intent, which provides error information about the request. (which I ignore)
+ * Next, if the request was successful, the Android Market application sends an IN_APP_NOTIFY broadcast intent.
+ * This message contains a notification ID, which you can use to retrieve the transaction details for the REQUEST_PURCHASE
+ * @param activityContext
+ * @param itemId
+ */
+ public static void requestPurchase(Context activityContext, String itemId){
+ if (amIDead()) {
+ return;
+ }
+ Log.i(TAG, "requestPurchase()");
+ Bundle request = makeRequestBundle("REQUEST_PURCHASE");
+ request.putString("ITEM_ID", itemId);
+ try {
+ Bundle response = mService.sendBillingRequest(request);
+
+ //The RESPONSE_CODE key provides you with the status of the request
+ Integer responseCodeIndex = (Integer) response.get("RESPONSE_CODE");
+ //The PURCHASE_INTENT key provides you with a PendingIntent, which you can use to launch the checkout UI
+ PendingIntent pendingIntent = (PendingIntent) response.get("PURCHASE_INTENT");
+ //The REQUEST_ID key provides you with a unique request identifier for the request
+ Long requestIndentifier = (Long) response.get("REQUEST_ID");
+ Log.i(TAG, "current request is:" + requestIndentifier);
+ BillingConstants.ResponseCode responseCode = BillingConstants.ResponseCode.valueOf(responseCodeIndex);
+ Log.i(TAG, "REQUEST_PURCHASE Sync Response code: "+responseCode.toString());
+
+ startBuyPageActivity(pendingIntent, new Intent(), activityContext);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed, internet error maybe", e);
+ Log.e(TAG, "Billing supported: "+isBillingSupported());
+ }
+ }
+
+ /**
+ * A GET_PURCHASE_INFORMATION request also triggers two asynchronous responses (broadcast intents).
+ * First, the Android Market application sends a RESPONSE_CODE broadcast intent, which provides status and error information about the request. (which I ignore)
+ * Next, if the request was successful, the Android Market application sends a PURCHASE_STATE_CHANGED broadcast intent.
+ * This message contains detailed transaction information.
+ * The transaction information is contained in a signed JSON string (unencrypted).
+ * The message includes the signature so you can verify the integrity of the signed string
+ * @param notifyIds
+ */
+ protected static void getPurchaseInformation(String[] notifyIds){
+ if (amIDead()) {
+ return;
+ }
+ Log.i(TAG, "getPurchaseInformation()");
+ Bundle request = makeRequestBundle("GET_PURCHASE_INFORMATION");
+ // The REQUEST_NONCE key contains a cryptographically secure nonce (number used once) that you must generate.
+ // The Android Market application returns this nonce with the PURCHASE_STATE_CHANGED broadcast intent so you can verify the integrity of the transaction information.
+ request.putLong("NONCE", BillingSecurity.generateNonce());
+ // The NOTIFY_IDS key contains an array of notification IDs, which you received in the IN_APP_NOTIFY broadcast intent.
+ request.putStringArray("NOTIFY_IDS", notifyIds);
+ try {
+ Bundle response = mService.sendBillingRequest(request);
+
+ //The REQUEST_ID key provides you with a unique request identifier for the request
+ Long requestIndentifier = (Long) response.get("REQUEST_ID");
+ Log.i(TAG, "current request is:" + requestIndentifier);
+ //The RESPONSE_CODE key provides you with the status of the request
+ Integer responseCodeIndex = (Integer) response.get("RESPONSE_CODE");
+ BillingConstants.ResponseCode responseCode = BillingConstants.ResponseCode.valueOf(responseCodeIndex);
+ Log.i(TAG, "GET_PURCHASE_INFORMATION Sync Response code: "+responseCode.toString());
+
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed, internet error maybe", e);
+ Log.e(TAG, "Billing supported: "+isBillingSupported());
+ }
+ }
+
+ /**
+ * To acknowledge that you received transaction information you send a
+ * CONFIRM_NOTIFICATIONS request.
+ *
+ * A CONFIRM_NOTIFICATIONS request triggers a single asynchronous response�a RESPONSE_CODE broadcast intent.
+ * This broadcast intent provides status and error information about the request.
+ *
+ * Note: As a best practice, you should not send a CONFIRM_NOTIFICATIONS request for a purchased item until you have delivered the item to the user.
+ * This way, if your application crashes or something else prevents your application from delivering the product,
+ * your application will still receive an IN_APP_NOTIFY broadcast intent from Android Market indicating that you need to deliver the product
+ * @param notifyIds
+ */
+ protected static void confirmTransaction(String[] notifyIds) {
+ if (amIDead()) {
+ return;
+ }
+ Log.i(TAG, "confirmTransaction()");
+ Bundle request = makeRequestBundle("CONFIRM_NOTIFICATIONS");
+ request.putStringArray("NOTIFY_IDS", notifyIds);
+ try {
+ Bundle response = mService.sendBillingRequest(request);
+
+ //The REQUEST_ID key provides you with a unique request identifier for the request
+ Long requestIndentifier = (Long) response.get("REQUEST_ID");
+ Log.i(TAG, "current request is:" + requestIndentifier);
+
+ //The RESPONSE_CODE key provides you with the status of the request
+ Integer responseCodeIndex = (Integer) response.get("RESPONSE_CODE");
+ BillingConstants.ResponseCode responseCode = BillingConstants.ResponseCode.valueOf(responseCodeIndex);
+
+ Log.i(TAG, "CONFIRM_NOTIFICATIONS Sync Response code: "+responseCode.toString());
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed, internet error maybe", e);
+ Log.e(TAG, "Billing supported: " + isBillingSupported());
+ }
+ }
+
+ /**
+ *
+ * Can be used for when a user has reinstalled the app to give back prior purchases.
+ * if an item for sale's purchase type is "managed per user account" this means google will have a record ofthis transaction
+ *
+ * A RESTORE_TRANSACTIONS request also triggers two asynchronous responses (broadcast intents).
+ * First, the Android Market application sends a RESPONSE_CODE broadcast intent, which provides status and error information about the request.
+ * Next, if the request was successful, the Android Market application sends a PURCHASE_STATE_CHANGED broadcast intent.
+ * This message contains the detailed transaction information. The transaction information is contained in a signed JSON string (unencrypted).
+ * The message includes the signature so you can verify the integrity of the signed string
+ * @param nonce
+ */
+ protected static void restoreTransactionInformation(Long nonce) {
+ if (amIDead()) {
+ return;
+ }
+ Log.i(TAG, "confirmTransaction()");
+ Bundle request = makeRequestBundle("RESTORE_TRANSACTIONS");
+ // The REQUEST_NONCE key contains a cryptographically secure nonce (number used once) that you must generate
+ request.putLong("NONCE", nonce);
+ try {
+ Bundle response = mService.sendBillingRequest(request);
+
+ //The REQUEST_ID key provides you with a unique request identifier for the request
+ Long requestIndentifier = (Long) response.get("REQUEST_ID");
+ Log.i(TAG, "current request is:" + requestIndentifier);
+
+ //The RESPONSE_CODE key provides you with the status of the request
+ Integer responseCodeIndex = (Integer) response.get("RESPONSE_CODE");
+ BillingConstants.ResponseCode responseCode = BillingConstants.ResponseCode.valueOf(responseCodeIndex);
+ Log.i(TAG, "RESTORE_TRANSACTIONS Sync Response code: "+responseCode.toString());
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed, internet error maybe", e);
+ Log.e(TAG, "Billing supported: " + isBillingSupported());
+ }
+ }
+
+ private static boolean amIDead() {
+ if (mService == null || mContext == null) {
+ Log.e(TAG, "BillingHelper not fully instantiated");
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ private static Bundle makeRequestBundle(String method) {
+ Bundle request = new Bundle();
+ request.putString("BILLING_REQUEST", method);
+ request.putInt("API_VERSION", 1);
+ request.putString("PACKAGE_NAME", mContext.getPackageName());
+ return request;
+ }
+
+ /**
+ *
+ *
+ * You must launch the pending intent from an activity context and not an application context
+ * You cannot use the singleTop launch mode to launch the pending intent
+ * @param pendingIntent
+ * @param intent
+ * @param context
+ */
+ private static void startBuyPageActivity(PendingIntent pendingIntent, Intent intent, Context context){
+ //TODO add above 2.0 implementation with reflection, for now just using 1.6 implem
+
+ // This is on Android 1.6. The in-app checkout page activity will be on its
+ // own separate activity stack instead of on the activity stack of
+ // the application.
+ try {
+ pendingIntent.send(context, 0, intent);
+ } catch (CanceledException e){
+ Log.e(TAG, "startBuyPageActivity CanceledException");
+ }
+ }
+
+ protected static void verifyPurchase(String signedData, String signature) {
+ ArrayList<VerifiedPurchase> purchases = BillingSecurity.verifyPurchase(signedData, signature);
+ latestPurchase = purchases.get(0);
+
+ confirmTransaction(new String[]{latestPurchase.notificationId});
+
+ if(mCompletedHandler != null){
+ mCompletedHandler.sendEmptyMessage(0);
+ } else {
+ Log.e(TAG, "verifyPurchase error. Handler not instantiated. Have you called setCompletedHandler()?");
+ }
+ }
+
+ public static void stopService(){
+ mContext.stopService(new Intent(mContext, BillingService.class));
+ mService = null;
+ mContext = null;
+ mCompletedHandler = null;
+ Log.i(TAG, "Stopping Service");
+ }
+}
diff --git a/src/org/fox/ttrss/billing/BillingReceiver.java b/src/org/fox/ttrss/billing/BillingReceiver.java
new file mode 100644
index 00000000..9b772054
--- /dev/null
+++ b/src/org/fox/ttrss/billing/BillingReceiver.java
@@ -0,0 +1,57 @@
+package org.fox.ttrss.billing;
+
+import static org.fox.ttrss.billing.BillingConstants.ACTION_NOTIFY;
+import static org.fox.ttrss.billing.BillingConstants.ACTION_PURCHASE_STATE_CHANGED;
+import static org.fox.ttrss.billing.BillingConstants.ACTION_RESPONSE_CODE;
+import static org.fox.ttrss.billing.BillingConstants.INAPP_REQUEST_ID;
+import static org.fox.ttrss.billing.BillingConstants.INAPP_RESPONSE_CODE;
+import static org.fox.ttrss.billing.BillingConstants.INAPP_SIGNATURE;
+import static org.fox.ttrss.billing.BillingConstants.INAPP_SIGNED_DATA;
+import static org.fox.ttrss.billing.BillingConstants.NOTIFICATION_ID;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+public class BillingReceiver extends BroadcastReceiver {
+
+ private static final String TAG = "BillingService";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ Log.i(TAG, "Received action: " + action);
+ if (ACTION_PURCHASE_STATE_CHANGED.equals(action)) {
+ String signedData = intent.getStringExtra(INAPP_SIGNED_DATA);
+ String signature = intent.getStringExtra(INAPP_SIGNATURE);
+ purchaseStateChanged(context, signedData, signature);
+ } else if (ACTION_NOTIFY.equals(action)) {
+ String notifyId = intent.getStringExtra(NOTIFICATION_ID);
+ notify(context, notifyId);
+ } else if (ACTION_RESPONSE_CODE.equals(action)) {
+ long requestId = intent.getLongExtra(INAPP_REQUEST_ID, -1);
+ int responseCodeIndex = intent.getIntExtra(INAPP_RESPONSE_CODE, BillingConstants.ResponseCode.RESULT_ERROR.ordinal());
+ checkResponseCode(context, requestId, responseCodeIndex);
+ } else {
+ Log.e(TAG, "unexpected action: " + action);
+ }
+ }
+
+
+ private void purchaseStateChanged(Context context, String signedData, String signature) {
+ Log.i(TAG, "purchaseStateChanged got signedData: " + signedData);
+ Log.i(TAG, "purchaseStateChanged got signature: " + signature);
+ BillingHelper.verifyPurchase(signedData, signature);
+ }
+
+ private void notify(Context context, String notifyId) {
+ Log.i(TAG, "notify got id: " + notifyId);
+ String[] notifyIds = {notifyId};
+ BillingHelper.getPurchaseInformation(notifyIds);
+ }
+
+ private void checkResponseCode(Context context, long requestId, int responseCodeIndex) {
+ Log.i(TAG, "checkResponseCode got requestId: " + requestId);
+ Log.i(TAG, "checkResponseCode got responseCode: " + BillingConstants.ResponseCode.valueOf(responseCodeIndex));
+ }
+} \ No newline at end of file
diff --git a/src/org/fox/ttrss/billing/BillingSecurity.java b/src/org/fox/ttrss/billing/BillingSecurity.java
new file mode 100644
index 00000000..513d6f34
--- /dev/null
+++ b/src/org/fox/ttrss/billing/BillingSecurity.java
@@ -0,0 +1,258 @@
+// Copyright 2010 Google Inc. All Rights Reserved.
+
+package org.fox.ttrss.billing;
+
+import java.security.InvalidKeyException;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.SecureRandom;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.ArrayList;
+import java.util.HashSet;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.text.TextUtils;
+import android.util.Log;
+
+import org.fox.ttrss.billing.BillingConstants.PurchaseState;
+import org.fox.ttrss.util.Base64;
+import org.fox.ttrss.util.Base64DecoderException;
+
+/**
+ * Security-related methods. For a secure implementation, all of this code
+ * should be implemented on a server that communicates with the application on
+ * the device. For the sake of simplicity and clarity of this example, this code
+ * is included here and is executed on the device. If you must verify the
+ * purchases on the phone, you should obfuscate this code to make it harder for
+ * an attacker to replace the code with stubs that treat all purchases as
+ * verified.
+ */
+public class BillingSecurity {
+ private static final String TAG = "BillingService";
+
+ private static final String KEY_FACTORY_ALGORITHM = "RSA";
+ private static final String SIGNATURE_ALGORITHM = "SHA1withRSA";
+ private static final SecureRandom RANDOM = new SecureRandom();
+
+ /**
+ * This keeps track of the nonces that we generated and sent to the server.
+ * We need to keep track of these until we get back the purchase state and
+ * send a confirmation message back to Android Market. If we are killed and
+ * lose this list of nonces, it is not fatal. Android Market will send us a
+ * new "notify" message and we will re-generate a new nonce. This has to be
+ * "static" so that the {@link BillingReceiver} can check if a nonce exists.
+ */
+ private static HashSet<Long> sKnownNonces = new HashSet<Long>();
+
+ /**
+ * A class to hold the verified purchase information.
+ */
+ public static class VerifiedPurchase {
+ public PurchaseState purchaseState;
+ public String notificationId;
+ public String productId;
+ public String orderId;
+ public long purchaseTime;
+ public String developerPayload;
+
+ public VerifiedPurchase(PurchaseState purchaseState, String notificationId, String productId, String orderId, long purchaseTime,
+ String developerPayload) {
+ this.purchaseState = purchaseState;
+ this.notificationId = notificationId;
+ this.productId = productId;
+ this.orderId = orderId;
+ this.purchaseTime = purchaseTime;
+ this.developerPayload = developerPayload;
+ }
+
+ public boolean isPurchased(){
+ return purchaseState.equals(PurchaseState.PURCHASED);
+ }
+
+
+ }
+
+ /** Generates a nonce (a random number used once). */
+ public static long generateNonce() {
+ long nonce = RANDOM.nextLong();
+ Log.i(TAG, "Nonce generateD: "+nonce);
+ sKnownNonces.add(nonce);
+ return nonce;
+ }
+
+ public static void removeNonce(long nonce) {
+ sKnownNonces.remove(nonce);
+ }
+
+ public static boolean isNonceKnown(long nonce) {
+ return sKnownNonces.contains(nonce);
+ }
+
+ /**
+ * Verifies that the data was signed with the given signature, and returns
+ * the list of verified purchases. The data is in JSON format and contains a
+ * nonce (number used once) that we generated and that was signed (as part
+ * of the whole data string) with a private key. The data also contains the
+ * {@link PurchaseState} and product ID of the purchase. In the general
+ * case, there can be an array of purchase transactions because there may be
+ * delays in processing the purchase on the backend and then several
+ * purchases can be batched together.
+ *
+ * @param signedData
+ * the signed JSON string (signed, not encrypted)
+ * @param signature
+ * the signature for the data, signed with the private key
+ */
+ public static ArrayList<VerifiedPurchase> verifyPurchase(String signedData, String signature) {
+ if (signedData == null) {
+ Log.e(TAG, "data is null");
+ return null;
+ }
+ Log.i(TAG, "signedData: " + signedData);
+ boolean verified = false;
+ if (!TextUtils.isEmpty(signature)) {
+ /**
+ * Compute your public key (that you got from the Android Market
+ * publisher site).
+ *
+ * Instead of just storing the entire literal string here embedded
+ * in the program, construct the key at runtime from pieces or use
+ * bit manipulation (for example, XOR with some other string) to
+ * hide the actual key. The key itself is not secret information,
+ * but we don't want to make it easy for an adversary to replace the
+ * public key with one of their own and then fake messages from the
+ * server.
+ *
+ * Generally, encryption keys / passwords should only be kept in
+ * memory long enough to perform the operation they need to perform.
+ */
+ String base64EncodedPublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApLWBv8eFC4f7h6gz3VE87XX2nqJB2KL2yNnNawmgaL/0nd6nXvVRiZ3iXLLP9k8RpLJ6rZPV778z8WzDLZATV3b2nh21KgjSNoG4em1oSf7pW4+AujqjLfNVRsXoJIWG+OMMd9o9l/D2YJTCXzSvgFIfF5EJRg6APZHEVrVJo8iXwnYM1tFfLjPfp10MtjLmD5tZW8o3hTmXJ3ZMDI12PL22G4KaE+BuQqI6PZ22m/pA85R6AuhNo2IUSE4XFUE8i7ANWDvdfDzQ5J0TTWAeHmUQCstdZ48z+6AjqD3L2omS/dKoBnlYxEUZms3iUa1/Co40nWU7sc2hqpmfNiG5oQIDAQAB";
+ PublicKey key = BillingSecurity.generatePublicKey(base64EncodedPublicKey);
+ verified = BillingSecurity.verify(key, signedData, signature);
+ if (!verified) {
+ Log.w(TAG, "signature does not match data.");
+ return null;
+ }
+ }
+
+ JSONObject jObject;
+ JSONArray jTransactionsArray = null;
+ int numTransactions = 0;
+ long nonce = 0L;
+ try {
+ jObject = new JSONObject(signedData);
+
+ // The nonce might be null if the user backed out of the buy page.
+ nonce = jObject.optLong("nonce");
+ jTransactionsArray = jObject.optJSONArray("orders");
+ if (jTransactionsArray != null) {
+ numTransactions = jTransactionsArray.length();
+ }
+ } catch (JSONException e) {
+ return null;
+ }
+
+ if (!BillingSecurity.isNonceKnown(nonce)) {
+ Log.w(TAG, "Nonce not found: " + nonce);
+ return null;
+ }
+
+ ArrayList<VerifiedPurchase> purchases = new ArrayList<VerifiedPurchase>();
+ try {
+ for (int i = 0; i < numTransactions; i++) {
+ JSONObject jElement = jTransactionsArray.getJSONObject(i);
+ int response = jElement.getInt("purchaseState");
+ PurchaseState purchaseState = PurchaseState.valueOf(response);
+ String productId = jElement.getString("productId");
+ String packageName = jElement.getString("packageName");
+ long purchaseTime = jElement.getLong("purchaseTime");
+ String orderId = jElement.optString("orderId", "");
+ String notifyId = null;
+ if (jElement.has("notificationId")) {
+ notifyId = jElement.getString("notificationId");
+ }
+ String developerPayload = jElement.optString("developerPayload", null);
+
+ // If the purchase state is PURCHASED, then we require a
+ // verified nonce.
+ if (purchaseState == PurchaseState.PURCHASED && !verified) {
+ continue;
+ }
+ purchases.add(new VerifiedPurchase(purchaseState, notifyId, productId, orderId, purchaseTime, developerPayload));
+ }
+ } catch (JSONException e) {
+ Log.e(TAG, "JSON exception: ", e);
+ return null;
+ }
+ removeNonce(nonce);
+ return purchases;
+ }
+
+ /**
+ * Generates a PublicKey instance from a string containing the
+ * Base64-encoded public key.
+ *
+ * @param encodedPublicKey
+ * Base64-encoded public key
+ * @throws IllegalArgumentException
+ * if encodedPublicKey is invalid
+ */
+ public static PublicKey generatePublicKey(String encodedPublicKey) {
+ try {
+ byte[] decodedKey = Base64.decode(encodedPublicKey);
+ KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
+ return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey));
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e);
+ } catch (InvalidKeySpecException e) {
+ Log.e(TAG, "Invalid key specification.");
+ throw new IllegalArgumentException(e);
+ } catch (Base64DecoderException e) {
+ Log.e(TAG, "Base64DecoderException.", e);
+ return null;
+ }
+ }
+
+ /**
+ * Verifies that the signature from the server matches the computed
+ * signature on the data. Returns true if the data is correctly signed.
+ *
+ * @param publicKey
+ * public key associated with the developer account
+ * @param signedData
+ * signed data from server
+ * @param signature
+ * server signature
+ * @return true if the data and signature match
+ */
+ public static boolean verify(PublicKey publicKey, String signedData, String signature) {
+ Log.i(TAG, "signature: " + signature);
+ Signature sig;
+ try {
+ sig = Signature.getInstance(SIGNATURE_ALGORITHM);
+ sig.initVerify(publicKey);
+ sig.update(signedData.getBytes());
+ if (!sig.verify(Base64.decode(signature))) {
+ Log.e(TAG, "Signature verification failed.");
+ return false;
+ }
+ return true;
+ } catch (NoSuchAlgorithmException e) {
+ Log.e(TAG, "NoSuchAlgorithmException.");
+ } catch (InvalidKeyException e) {
+ Log.e(TAG, "Invalid key specification.");
+ } catch (SignatureException e) {
+ Log.e(TAG, "Signature exception.");
+ } catch (Base64DecoderException e) {
+ Log.e(TAG, "Base64DecoderException.", e);
+ }
+ return false;
+ }
+}
diff --git a/src/org/fox/ttrss/billing/BillingService.java b/src/org/fox/ttrss/billing/BillingService.java
new file mode 100644
index 00000000..2ae53234
--- /dev/null
+++ b/src/org/fox/ttrss/billing/BillingService.java
@@ -0,0 +1,60 @@
+package org.fox.ttrss.billing;
+
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+import android.util.Log;
+
+import com.android.vending.billing.IMarketBillingService;
+
+public class BillingService extends Service implements ServiceConnection{
+
+ private static final String TAG = "BillingService";
+
+ /** The service connection to the remote MarketBillingService. */
+ private IMarketBillingService mService;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ Log.i(TAG, "Service starting with onCreate");
+
+ try {
+ boolean bindResult = bindService(new Intent("com.android.vending.billing.MarketBillingService.BIND"), this, Context.BIND_AUTO_CREATE);
+ if(bindResult){
+ Log.i(TAG,"Market Billing Service Successfully Bound");
+ } else {
+ Log.e(TAG,"Market Billing Service could not be bound.");
+ //TODO stop user continuing
+ }
+ } catch (SecurityException e){
+ Log.e(TAG,"Market Billing Service could not be bound. SecurityException: "+e);
+ //TODO stop user continuing
+ }
+ }
+
+ public void setContext(Context context) {
+ attachBaseContext(context);
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ Log.i(TAG, "Market Billing Service Connected.");
+ mService = IMarketBillingService.Stub.asInterface(service);
+ BillingHelper.instantiateHelper(getBaseContext(), mService);
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+
+ }
+
+}