summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorAndrew Dolgov <[email protected]>2012-02-29 18:30:52 +0300
committerAndrew Dolgov <[email protected]>2012-02-29 18:30:52 +0300
commit8e45b14c6158deaab5f0f67c90103896fdc2fdc1 (patch)
tree3e7c360272eeeb2de3dec665ff0c2d10b2eba7fb /src
parent246483193876b9577b6fbd9d8c51caf7bfe2aef9 (diff)
add in-app billing donation stuff
Diffstat (limited to 'src')
-rw-r--r--src/com/android/vending/billing/IMarketBillingService.aidl24
-rw-r--r--src/org/fox/ttrss/BillingConstants.java63
-rw-r--r--src/org/fox/ttrss/BillingHelper.java267
-rw-r--r--src/org/fox/ttrss/BillingReceiver.java57
-rw-r--r--src/org/fox/ttrss/BillingSecurity.java258
-rw-r--r--src/org/fox/ttrss/BillingService.java60
-rw-r--r--src/org/fox/ttrss/MainActivity.java55
-rw-r--r--src/org/fox/ttrss/util/Base64.java582
-rw-r--r--src/org/fox/ttrss/util/Base64DecoderException.java32
9 files changed, 1393 insertions, 5 deletions
diff --git a/src/com/android/vending/billing/IMarketBillingService.aidl b/src/com/android/vending/billing/IMarketBillingService.aidl
new file mode 100644
index 00000000..6884b41f
--- /dev/null
+++ b/src/com/android/vending/billing/IMarketBillingService.aidl
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+
+package com.android.vending.billing;
+
+import android.os.Bundle;
+
+interface IMarketBillingService {
+ /** Given the arguments in bundle form, returns a bundle for results. */
+ Bundle sendBillingRequest(in Bundle bundle);
+}
diff --git a/src/org/fox/ttrss/BillingConstants.java b/src/org/fox/ttrss/BillingConstants.java
new file mode 100644
index 00000000..ea8b454c
--- /dev/null
+++ b/src/org/fox/ttrss/BillingConstants.java
@@ -0,0 +1,63 @@
+package org.fox.ttrss;
+
+
+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/BillingHelper.java b/src/org/fox/ttrss/BillingHelper.java
new file mode 100644
index 00000000..e29fd2f7
--- /dev/null
+++ b/src/org/fox/ttrss/BillingHelper.java
@@ -0,0 +1,267 @@
+package org.fox.ttrss;
+
+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.BillingSecurity.VerifiedPurchase;
+import org.fox.ttrss.BillingConstants.ResponseCode;
+
+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;
+ }
+
+ protected 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
+ */
+ protected 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/BillingReceiver.java b/src/org/fox/ttrss/BillingReceiver.java
new file mode 100644
index 00000000..42cd5df6
--- /dev/null
+++ b/src/org/fox/ttrss/BillingReceiver.java
@@ -0,0 +1,57 @@
+package org.fox.ttrss;
+
+import static org.fox.ttrss.BillingConstants.*;
+
+import java.util.ArrayList;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Message;
+import android.util.Log;
+
+import org.fox.ttrss.BillingConstants;
+import org.fox.ttrss.BillingSecurity.VerifiedPurchase;
+
+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/BillingSecurity.java b/src/org/fox/ttrss/BillingSecurity.java
new file mode 100644
index 00000000..26e19b3e
--- /dev/null
+++ b/src/org/fox/ttrss/BillingSecurity.java
@@ -0,0 +1,258 @@
+// Copyright 2010 Google Inc. All Rights Reserved.
+
+package org.fox.ttrss;
+
+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.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/BillingService.java b/src/org/fox/ttrss/BillingService.java
new file mode 100644
index 00000000..e003df32
--- /dev/null
+++ b/src/org/fox/ttrss/BillingService.java
@@ -0,0 +1,60 @@
+package org.fox.ttrss;
+
+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) {
+
+ }
+
+}
diff --git a/src/org/fox/ttrss/MainActivity.java b/src/org/fox/ttrss/MainActivity.java
index dd3295c8..5d7ecd09 100644
--- a/src/org/fox/ttrss/MainActivity.java
+++ b/src/org/fox/ttrss/MainActivity.java
@@ -23,6 +23,7 @@ import android.database.sqlite.SQLiteDatabase;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Bundle;
+import android.os.Handler;
import android.preference.PreferenceManager;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
@@ -69,6 +70,7 @@ public class MainActivity extends FragmentActivity implements OnlineServices {
private boolean m_isLoggingIn = false;
private boolean m_isOffline = false;
private boolean m_offlineModeReady = false;
+ private int m_selectedProduct = -1;
private SQLiteDatabase m_readableDb;
private SQLiteDatabase m_writableDb;
@@ -443,7 +445,7 @@ public class MainActivity extends FragmentActivity implements OnlineServices {
}
super.onCreate(savedInstanceState);
-
+
m_themeName = m_prefs.getString("theme", "THEME_DARK");
if (savedInstanceState != null) {
@@ -882,6 +884,45 @@ public class MainActivity extends FragmentActivity implements OnlineServices {
.findFragmentById(R.id.headlines_fragment);
switch (item.getItemId()) {
+ case R.id.donate:
+ if (true) {
+ CharSequence[] items = { "Silver Donation ($2)", "Gold Donation ($5)", "Platinum Donation ($10)" };
+
+ Dialog dialog = new Dialog(MainActivity.this);
+ AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this)
+ .setTitle(R.string.donate_select)
+ .setSingleChoiceItems(items, -1, new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ m_selectedProduct = which;
+ }
+ }).setNegativeButton(R.string.dialog_close, new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.cancel();
+ }
+ }).setPositiveButton(R.string.donate_do, new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (m_selectedProduct != -1 && m_selectedProduct < 3) {
+ CharSequence[] products = { "donation_silver", "donation_gold", "donation_platinum2" };
+
+ Log.d(TAG, "Selected product: " + products[m_selectedProduct]);
+
+ BillingHelper.requestPurchase(MainActivity.this, (String) products[m_selectedProduct]);
+
+ dialog.dismiss();
+ }
+ }
+ });
+
+ dialog = builder.create();
+ dialog.show();
+ }
+ return true;
case android.R.id.home:
goBack(false);
return true;
@@ -1130,7 +1171,7 @@ public class MainActivity extends FragmentActivity implements OnlineServices {
Dialog dialog = new Dialog(MainActivity.this);
AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this)
- .setTitle("Set labels")
+ .setTitle(R.string.article_set_labels)
.setMultiChoiceItems(items, checkedItems, new OnMultiChoiceClickListener() {
@Override
@@ -1152,7 +1193,7 @@ public class MainActivity extends FragmentActivity implements OnlineServices {
req.execute(map);
}
- }).setPositiveButton("Close", new OnClickListener() {
+ }).setPositiveButton(R.string.dialog_close, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
@@ -1358,7 +1399,9 @@ public class MainActivity extends FragmentActivity implements OnlineServices {
}
m_menu.findItem(R.id.set_labels).setEnabled(m_apiLevel >= 1);
-
+
+ m_menu.findItem(R.id.donate).setVisible(BillingHelper.isBillingSupported());
+
} else {
m_menu.setGroupVisible(R.id.menu_group_logged_in, false);
m_menu.setGroupVisible(R.id.menu_group_logged_out, true);
@@ -1410,8 +1453,10 @@ public class MainActivity extends FragmentActivity implements OnlineServices {
m_isOffline = false;
+ startService(new Intent(MainActivity.this, BillingService.class));
+
initMainMenu();
-
+
if (m_refreshTask != null) {
m_refreshTask.cancel();
m_refreshTask = null;
diff --git a/src/org/fox/ttrss/util/Base64.java b/src/org/fox/ttrss/util/Base64.java
new file mode 100644
index 00000000..5c8136db
--- /dev/null
+++ b/src/org/fox/ttrss/util/Base64.java
@@ -0,0 +1,582 @@
+// Portions copyright 2002, Google, Inc.
+//
+// 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.
+package org.fox.ttrss.util;
+
+// This code was converted from code at http://iharder.sourceforge.net/base64/
+// Lots of extraneous features were removed.
+/* The original code said:
+ * <p>
+ * I am placing this code in the Public Domain. Do with it as you will.
+ * This software comes with no guarantees or warranties but with
+ * plenty of well-wishing instead!
+ * Please visit
+ * <a href="http://iharder.net/xmlizable">http://iharder.net/xmlizable</a>
+ * periodically to check for updates or to contribute improvements.
+ * </p>
+ *
+ * @author Robert Harder
+ * @author [email protected]
+ * @version 1.3
+ */
+
+/**
+ * Base64 converter class. This code is not a complete MIME encoder; it simply
+ * converts binary data to base64 data and back.
+ *
+ * <p>
+ * Note {@link CharBase64} is a GWT-compatible implementation of this class.
+ */
+public class Base64 {
+ /** Specify encoding (value is {@code true}). */
+ public final static boolean ENCODE = true;
+
+ /** Specify decoding (value is {@code false}). */
+ public final static boolean DECODE = false;
+
+ /** The equals sign (=) as a byte. */
+ private final static byte EQUALS_SIGN = (byte) '=';
+
+ /** The new line character (\n) as a byte. */
+ private final static byte NEW_LINE = (byte) '\n';
+
+ /**
+ * The 64 valid Base64 values.
+ */
+ private final static byte[] ALPHABET = { (byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', (byte) 'G',
+ (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P', (byte) 'Q',
+ (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', (byte) 'a',
+ (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', (byte) 'k',
+ (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o', (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', (byte) 'u',
+ (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3', (byte) '4',
+ (byte) '5', (byte) '6', (byte) '7', (byte) '8', (byte) '9', (byte) '+', (byte) '/' };
+
+ /**
+ * The 64 valid web safe Base64 values.
+ */
+ private final static byte[] WEBSAFE_ALPHABET = { (byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', (byte) 'G',
+ (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P', (byte) 'Q',
+ (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', (byte) 'a',
+ (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', (byte) 'k',
+ (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o', (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', (byte) 'u',
+ (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3', (byte) '4',
+ (byte) '5', (byte) '6', (byte) '7', (byte) '8', (byte) '9', (byte) '-', (byte) '_' };
+
+ /**
+ * Translates a Base64 value to either its 6-bit reconstruction value or a
+ * negative number indicating some other meaning.
+ **/
+ private final static byte[] DECODABET = { -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal
+ // 0
+ // -
+ // 8
+ -5, -5, // Whitespace: Tab and Linefeed
+ -9, -9, // Decimal 11 - 12
+ -5, // Whitespace: Carriage Return
+ -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 -
+ // 26
+ -9, -9, -9, -9, -9, // Decimal 27 - 31
+ -5, // Whitespace: Space
+ -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42
+ 62, // Plus sign at decimal 43
+ -9, -9, -9, // Decimal 44 - 46
+ 63, // Slash at decimal 47
+ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
+ -9, -9, -9, // Decimal 58 - 60
+ -1, // Equals sign at decimal 61
+ -9, -9, -9, // Decimal 62 - 64
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through
+ // 'N'
+ 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O'
+ // through 'Z'
+ -9, -9, -9, -9, -9, -9, // Decimal 91 - 96
+ 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a'
+ // through 'm'
+ 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n'
+ // through 'z'
+ -9, -9, -9, -9, -9 // Decimal 123 - 127
+ /*
+ * ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139
+ * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152
+ * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165
+ * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178
+ * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191
+ * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204
+ * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
+ * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
+ * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
+ * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255
+ */
+ };
+
+ /** The web safe decodabet */
+ private final static byte[] WEBSAFE_DECODABET = { -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal
+ // 0
+ // -
+ // 8
+ -5, -5, // Whitespace: Tab and Linefeed
+ -9, -9, // Decimal 11 - 12
+ -5, // Whitespace: Carriage Return
+ -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 -
+ // 26
+ -9, -9, -9, -9, -9, // Decimal 27 - 31
+ -5, // Whitespace: Space
+ -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 44
+ 62, // Dash '-' sign at decimal 45
+ -9, -9, // Decimal 46-47
+ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
+ -9, -9, -9, // Decimal 58 - 60
+ -1, // Equals sign at decimal 61
+ -9, -9, -9, // Decimal 62 - 64
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through
+ // 'N'
+ 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O'
+ // through 'Z'
+ -9, -9, -9, -9, // Decimal 91-94
+ 63, // Underscore '_' at decimal 95
+ -9, // Decimal 96
+ 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a'
+ // through 'm'
+ 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n'
+ // through 'z'
+ -9, -9, -9, -9, -9 // Decimal 123 - 127
+ /*
+ * ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139
+ * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152
+ * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165
+ * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178
+ * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191
+ * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204
+ * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
+ * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
+ * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
+ * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255
+ */
+ };
+
+ // Indicates white space in encoding
+ private final static byte WHITE_SPACE_ENC = -5;
+ // Indicates equals sign in encoding
+ private final static byte EQUALS_SIGN_ENC = -1;
+
+ /** Defeats instantiation. */
+ private Base64() {
+ }
+
+ /* ******** E N C O D I N G M E T H O D S ******** */
+
+ /**
+ * Encodes up to three bytes of the array <var>source</var> and writes the
+ * resulting four Base64 bytes to <var>destination</var>. The source and
+ * destination arrays can be manipulated anywhere along their length by
+ * specifying <var>srcOffset</var> and <var>destOffset</var>. This method
+ * does not check to make sure your arrays are large enough to accommodate
+ * <var>srcOffset</var> + 3 for the <var>source</var> array or
+ * <var>destOffset</var> + 4 for the <var>destination</var> array. The
+ * actual number of significant bytes in your array is given by
+ * <var>numSigBytes</var>.
+ *
+ * @param source
+ * the array to convert
+ * @param srcOffset
+ * the index where conversion begins
+ * @param numSigBytes
+ * the number of significant bytes in your array
+ * @param destination
+ * the array to hold the conversion
+ * @param destOffset
+ * the index where output will be put
+ * @param alphabet
+ * is the encoding alphabet
+ * @return the <var>destination</var> array
+ * @since 1.3
+ */
+ private static byte[] encode3to4(byte[] source, int srcOffset, int numSigBytes, byte[] destination, int destOffset, byte[] alphabet) {
+ // 1 2 3
+ // 01234567890123456789012345678901 Bit position
+ // --------000000001111111122222222 Array position from threeBytes
+ // --------| || || || | Six bit groups to index alphabet
+ // >>18 >>12 >> 6 >> 0 Right shift necessary
+ // 0x3f 0x3f 0x3f Additional AND
+
+ // Create buffer with zero-padding if there are only one or two
+ // significant bytes passed in the array.
+ // We have to shift left 24 in order to flush out the 1's that appear
+ // when Java treats a value as negative that is cast from a byte to an
+ // int.
+ int inBuff = (numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0)
+ | (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0)
+ | (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0);
+
+ switch (numSigBytes) {
+ case 3:
+ destination[destOffset] = alphabet[(inBuff >>> 18)];
+ destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
+ destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f];
+ destination[destOffset + 3] = alphabet[(inBuff) & 0x3f];
+ return destination;
+ case 2:
+ destination[destOffset] = alphabet[(inBuff >>> 18)];
+ destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
+ destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f];
+ destination[destOffset + 3] = EQUALS_SIGN;
+ return destination;
+ case 1:
+ destination[destOffset] = alphabet[(inBuff >>> 18)];
+ destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
+ destination[destOffset + 2] = EQUALS_SIGN;
+ destination[destOffset + 3] = EQUALS_SIGN;
+ return destination;
+ default:
+ return destination;
+ } // end switch
+ } // end encode3to4
+
+ /**
+ * Encodes a byte array into Base64 notation. Equivalent to calling {@code
+ * encodeBytes(source, 0, source.length)}
+ *
+ * @param source
+ * The data to convert
+ * @since 1.4
+ */
+ public static String encode(byte[] source) {
+ return encode(source, 0, source.length, ALPHABET, true);
+ }
+
+ /**
+ * Encodes a byte array into web safe Base64 notation.
+ *
+ * @param source
+ * The data to convert
+ * @param doPadding
+ * is {@code true} to pad result with '=' chars if it does not
+ * fall on 3 byte boundaries
+ */
+ public static String encodeWebSafe(byte[] source, boolean doPadding) {
+ return encode(source, 0, source.length, WEBSAFE_ALPHABET, doPadding);
+ }
+
+ /**
+ * Encodes a byte array into Base64 notation.
+ *
+ * @param source
+ * the data to convert
+ * @param off
+ * offset in array where conversion should begin
+ * @param len
+ * length of data to convert
+ * @param alphabet
+ * the encoding alphabet
+ * @param doPadding
+ * is {@code true} to pad result with '=' chars if it does not
+ * fall on 3 byte boundaries
+ * @since 1.4
+ */
+ public static String encode(byte[] source, int off, int len, byte[] alphabet, boolean doPadding) {
+ byte[] outBuff = encode(source, off, len, alphabet, Integer.MAX_VALUE);
+ int outLen = outBuff.length;
+
+ // If doPadding is false, set length to truncate '='
+ // padding characters
+ while (doPadding == false && outLen > 0) {
+ if (outBuff[outLen - 1] != '=') {
+ break;
+ }
+ outLen -= 1;
+ }
+
+ return new String(outBuff, 0, outLen);
+ }
+
+ /**
+ * Encodes a byte array into Base64 notation.
+ *
+ * @param source
+ * the data to convert
+ * @param off
+ * offset in array where conversion should begin
+ * @param len
+ * length of data to convert
+ * @param alphabet
+ * is the encoding alphabet
+ * @param maxLineLength
+ * maximum length of one line.
+ * @return the BASE64-encoded byte array
+ */
+ public static byte[] encode(byte[] source, int off, int len, byte[] alphabet, int maxLineLength) {
+ int lenDiv3 = (len + 2) / 3; // ceil(len / 3)
+ int len43 = lenDiv3 * 4;
+ byte[] outBuff = new byte[len43 // Main 4:3
+ + (len43 / maxLineLength)]; // New lines
+
+ int d = 0;
+ int e = 0;
+ int len2 = len - 2;
+ int lineLength = 0;
+ for (; d < len2; d += 3, e += 4) {
+
+ // The following block of code is the same as
+ // encode3to4( source, d + off, 3, outBuff, e, alphabet );
+ // but inlined for faster encoding (~20% improvement)
+ int inBuff = ((source[d + off] << 24) >>> 8) | ((source[d + 1 + off] << 24) >>> 16) | ((source[d + 2 + off] << 24) >>> 24);
+ outBuff[e] = alphabet[(inBuff >>> 18)];
+ outBuff[e + 1] = alphabet[(inBuff >>> 12) & 0x3f];
+ outBuff[e + 2] = alphabet[(inBuff >>> 6) & 0x3f];
+ outBuff[e + 3] = alphabet[(inBuff) & 0x3f];
+
+ lineLength += 4;
+ if (lineLength == maxLineLength) {
+ outBuff[e + 4] = NEW_LINE;
+ e++;
+ lineLength = 0;
+ } // end if: end of line
+ } // end for: each piece of array
+
+ if (d < len) {
+ encode3to4(source, d + off, len - d, outBuff, e, alphabet);
+
+ lineLength += 4;
+ if (lineLength == maxLineLength) {
+ // Add a last newline
+ outBuff[e + 4] = NEW_LINE;
+ e++;
+ }
+ e += 4;
+ }
+
+ assert (e == outBuff.length);
+ return outBuff;
+ }
+
+ /* ******** D E C O D I N G M E T H O D S ******** */
+
+ /**
+ * Decodes four bytes from array <var>source</var> and writes the resulting
+ * bytes (up to three of them) to <var>destination</var>. The source and
+ * destination arrays can be manipulated anywhere along their length by
+ * specifying <var>srcOffset</var> and <var>destOffset</var>. This method
+ * does not check to make sure your arrays are large enough to accommodate
+ * <var>srcOffset</var> + 4 for the <var>source</var> array or
+ * <var>destOffset</var> + 3 for the <var>destination</var> array. This
+ * method returns the actual number of bytes that were converted from the
+ * Base64 encoding.
+ *
+ *
+ * @param source
+ * the array to convert
+ * @param srcOffset
+ * the index where conversion begins
+ * @param destination
+ * the array to hold the conversion
+ * @param destOffset
+ * the index where output will be put
+ * @param decodabet
+ * the decodabet for decoding Base64 content
+ * @return the number of decoded bytes converted
+ * @since 1.3
+ */
+ private static int decode4to3(byte[] source, int srcOffset, byte[] destination, int destOffset, byte[] decodabet) {
+ // Example: Dk==
+ if (source[srcOffset + 2] == EQUALS_SIGN) {
+ int outBuff = ((decodabet[source[srcOffset]] << 24) >>> 6) | ((decodabet[source[srcOffset + 1]] << 24) >>> 12);
+
+ destination[destOffset] = (byte) (outBuff >>> 16);
+ return 1;
+ } else if (source[srcOffset + 3] == EQUALS_SIGN) {
+ // Example: DkL=
+ int outBuff = ((decodabet[source[srcOffset]] << 24) >>> 6) | ((decodabet[source[srcOffset + 1]] << 24) >>> 12)
+ | ((decodabet[source[srcOffset + 2]] << 24) >>> 18);
+
+ destination[destOffset] = (byte) (outBuff >>> 16);
+ destination[destOffset + 1] = (byte) (outBuff >>> 8);
+ return 2;
+ } else {
+ // Example: DkLE
+ int outBuff = ((decodabet[source[srcOffset]] << 24) >>> 6) | ((decodabet[source[srcOffset + 1]] << 24) >>> 12)
+ | ((decodabet[source[srcOffset + 2]] << 24) >>> 18) | ((decodabet[source[srcOffset + 3]] << 24) >>> 24);
+
+ destination[destOffset] = (byte) (outBuff >> 16);
+ destination[destOffset + 1] = (byte) (outBuff >> 8);
+ destination[destOffset + 2] = (byte) (outBuff);
+ return 3;
+ }
+ } // end decodeToBytes
+
+ /**
+ * Decodes data from Base64 notation.
+ *
+ * @param s
+ * the string to decode (decoded in default encoding)
+ * @return the decoded data
+ * @since 1.4
+ */
+ public static byte[] decode(String s) throws Base64DecoderException {
+ byte[] bytes = s.getBytes();
+ return decode(bytes, 0, bytes.length);
+ }
+
+ /**
+ * Decodes data from web safe Base64 notation. Web safe encoding uses '-'
+ * instead of '+', '_' instead of '/'
+ *
+ * @param s
+ * the string to decode (decoded in default encoding)
+ * @return the decoded data
+ */
+ public static byte[] decodeWebSafe(String s) throws Base64DecoderException {
+ byte[] bytes = s.getBytes();
+ return decodeWebSafe(bytes, 0, bytes.length);
+ }
+
+ /**
+ * Decodes Base64 content in byte array format and returns the decoded byte
+ * array.
+ *
+ * @param source
+ * The Base64 encoded data
+ * @return decoded data
+ * @since 1.3
+ * @throws Base64DecoderException
+ */
+ public static byte[] decode(byte[] source) throws Base64DecoderException {
+ return decode(source, 0, source.length);
+ }
+
+ /**
+ * Decodes web safe Base64 content in byte array format and returns the
+ * decoded data. Web safe encoding uses '-' instead of '+', '_' instead of
+ * '/'
+ *
+ * @param source
+ * the string to decode (decoded in default encoding)
+ * @return the decoded data
+ */
+ public static byte[] decodeWebSafe(byte[] source) throws Base64DecoderException {
+ return decodeWebSafe(source, 0, source.length);
+ }
+
+ /**
+ * Decodes Base64 content in byte array format and returns the decoded byte
+ * array.
+ *
+ * @param source
+ * the Base64 encoded data
+ * @param off
+ * the offset of where to begin decoding
+ * @param len
+ * the length of characters to decode
+ * @return decoded data
+ * @since 1.3
+ * @throws Base64DecoderException
+ */
+ public static byte[] decode(byte[] source, int off, int len) throws Base64DecoderException {
+ return decode(source, off, len, DECODABET);
+ }
+
+ /**
+ * Decodes web safe Base64 content in byte array format and returns the
+ * decoded byte array. Web safe encoding uses '-' instead of '+', '_'
+ * instead of '/'
+ *
+ * @param source
+ * the Base64 encoded data
+ * @param off
+ * the offset of where to begin decoding
+ * @param len
+ * the length of characters to decode
+ * @return decoded data
+ */
+ public static byte[] decodeWebSafe(byte[] source, int off, int len) throws Base64DecoderException {
+ return decode(source, off, len, WEBSAFE_DECODABET);
+ }
+
+ /**
+ * Decodes Base64 content using the supplied decodabet and returns the
+ * decoded byte array.
+ *
+ * @param source
+ * the Base64 encoded data
+ * @param off
+ * the offset of where to begin decoding
+ * @param len
+ * the length of characters to decode
+ * @param decodabet
+ * the decodabet for decoding Base64 content
+ * @return decoded data
+ */
+ public static byte[] decode(byte[] source, int off, int len, byte[] decodabet) throws Base64DecoderException {
+ int len34 = len * 3 / 4;
+ byte[] outBuff = new byte[2 + len34]; // Upper limit on size of output
+ int outBuffPosn = 0;
+
+ byte[] b4 = new byte[4];
+ int b4Posn = 0;
+ int i = 0;
+ byte sbiCrop = 0;
+ byte sbiDecode = 0;
+ for (i = 0; i < len; i++) {
+ sbiCrop = (byte) (source[i + off] & 0x7f); // Only the low seven
+ // bits
+ sbiDecode = decodabet[sbiCrop];
+
+ if (sbiDecode >= WHITE_SPACE_ENC) { // White space Equals sign or
+ // better
+ if (sbiDecode >= EQUALS_SIGN_ENC) {
+ // An equals sign (for padding) must not occur at position 0
+ // or 1
+ // and must be the last byte[s] in the encoded value
+ if (sbiCrop == EQUALS_SIGN) {
+ int bytesLeft = len - i;
+ byte lastByte = (byte) (source[len - 1 + off] & 0x7f);
+ if (b4Posn == 0 || b4Posn == 1) {
+ throw new Base64DecoderException("invalid padding byte '=' at byte offset " + i);
+ } else if ((b4Posn == 3 && bytesLeft > 2) || (b4Posn == 4 && bytesLeft > 1)) {
+ throw new Base64DecoderException("padding byte '=' falsely signals end of encoded value " + "at offset " + i);
+ } else if (lastByte != EQUALS_SIGN && lastByte != NEW_LINE) {
+ throw new Base64DecoderException("encoded value has invalid trailing byte");
+ }
+ break;
+ }
+
+ b4[b4Posn++] = sbiCrop;
+ if (b4Posn == 4) {
+ outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet);
+ b4Posn = 0;
+ }
+ }
+ } else {
+ throw new Base64DecoderException("Bad Base64 input character at " + i + ": " + source[i + off] + "(decimal)");
+ }
+ }
+
+ // Because web safe encoding allows non padding base64 encodes, we
+ // need to pad the rest of the b4 buffer with equal signs when
+ // b4Posn != 0. There can be at most 2 equal signs at the end of
+ // four characters, so the b4 buffer must have two or three
+ // characters. This also catches the case where the input is
+ // padded with EQUALS_SIGN
+ if (b4Posn != 0) {
+ if (b4Posn == 1) {
+ // Ensure you have set your public key
+ throw new Base64DecoderException("single trailing character at offset " + (len - 1));
+ }
+ b4[b4Posn++] = EQUALS_SIGN;
+ outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet);
+ }
+
+ byte[] out = new byte[outBuffPosn];
+ System.arraycopy(outBuff, 0, out, 0, outBuffPosn);
+ return out;
+ }
+}
diff --git a/src/org/fox/ttrss/util/Base64DecoderException.java b/src/org/fox/ttrss/util/Base64DecoderException.java
new file mode 100644
index 00000000..7b7be229
--- /dev/null
+++ b/src/org/fox/ttrss/util/Base64DecoderException.java
@@ -0,0 +1,32 @@
+// Copyright 2002, Google, Inc.
+//
+// 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.
+
+package org.fox.ttrss.util;;
+
+/**
+ * Exception thrown when encountering an invalid Base64 input character.
+ *
+ * @author nelson
+ */
+public class Base64DecoderException extends Exception {
+ public Base64DecoderException() {
+ super();
+ }
+
+ public Base64DecoderException(String s) {
+ super(s);
+ }
+
+ private static final long serialVersionUID = 1L;
+}