add in-app billing donation stuff
This commit is contained in:
parent
2464831938
commit
8e45b14c61
@ -1,14 +1,15 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="org.fox.ttrss"
|
package="org.fox.ttrss"
|
||||||
android:versionCode="64"
|
android:versionCode="66"
|
||||||
android:versionName="0.5.1" >
|
android:versionName="0.5.2" >
|
||||||
|
|
||||||
<uses-sdk android:minSdkVersion="7" />
|
<uses-sdk android:minSdkVersion="7" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="com.android.vending.BILLING" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
@ -49,6 +50,8 @@
|
|||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:name=".ImageCacheService" />
|
android:name=".ImageCacheService" />
|
||||||
|
|
||||||
|
<service android:name="BillingService" />
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.google.android.backup.api_key"
|
android:name="com.google.android.backup.api_key"
|
||||||
android:value="AEdPqrEAAAAIwG6zsGB4qo6ZhjfwIJpm9WI7AqmWaoRXm6ZJnA" />
|
android:value="AEdPqrEAAAAIwG6zsGB4qo6ZhjfwIJpm9WI7AqmWaoRXm6ZJnA" />
|
||||||
|
@ -136,6 +136,11 @@
|
|||||||
android:showAsAction=""
|
android:showAsAction=""
|
||||||
android:title="@string/preferences"/>
|
android:title="@string/preferences"/>
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/donate"
|
||||||
|
android:showAsAction=""
|
||||||
|
android:title="@string/donate"/>
|
||||||
|
|
||||||
<group android:id="@+id/menu_group_logged_out" >
|
<group android:id="@+id/menu_group_logged_out" >
|
||||||
|
|
||||||
<item
|
<item
|
||||||
|
@ -122,4 +122,8 @@
|
|||||||
<string name="font_size_medium">Medium</string>
|
<string name="font_size_medium">Medium</string>
|
||||||
<string name="font_size_large">Large</string>
|
<string name="font_size_large">Large</string>
|
||||||
<string name="pref_font_size">Article text size</string>
|
<string name="pref_font_size">Article text size</string>
|
||||||
|
<string name="donate">Donate</string>
|
||||||
|
<string name="dialog_close">Close</string>
|
||||||
|
<string name="donate_select">Please select the donation</string>
|
||||||
|
<string name="donate_do">Donate!</string>
|
||||||
</resources>
|
</resources>
|
24
src/com/android/vending/billing/IMarketBillingService.aidl
Normal file
24
src/com/android/vending/billing/IMarketBillingService.aidl
Normal file
@ -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);
|
||||||
|
}
|
63
src/org/fox/ttrss/BillingConstants.java
Normal file
63
src/org/fox/ttrss/BillingConstants.java
Normal file
@ -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";
|
||||||
|
|
||||||
|
}
|
267
src/org/fox/ttrss/BillingHelper.java
Normal file
267
src/org/fox/ttrss/BillingHelper.java
Normal file
@ -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<EFBFBD>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");
|
||||||
|
}
|
||||||
|
}
|
57
src/org/fox/ttrss/BillingReceiver.java
Normal file
57
src/org/fox/ttrss/BillingReceiver.java
Normal file
@ -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));
|
||||||
|
}
|
||||||
|
}
|
258
src/org/fox/ttrss/BillingSecurity.java
Normal file
258
src/org/fox/ttrss/BillingSecurity.java
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
60
src/org/fox/ttrss/BillingService.java
Normal file
60
src/org/fox/ttrss/BillingService.java
Normal file
@ -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) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -23,6 +23,7 @@ import android.database.sqlite.SQLiteDatabase;
|
|||||||
import android.net.ConnectivityManager;
|
import android.net.ConnectivityManager;
|
||||||
import android.net.NetworkInfo;
|
import android.net.NetworkInfo;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.support.v4.app.Fragment;
|
import android.support.v4.app.Fragment;
|
||||||
import android.support.v4.app.FragmentActivity;
|
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_isLoggingIn = false;
|
||||||
private boolean m_isOffline = false;
|
private boolean m_isOffline = false;
|
||||||
private boolean m_offlineModeReady = false;
|
private boolean m_offlineModeReady = false;
|
||||||
|
private int m_selectedProduct = -1;
|
||||||
|
|
||||||
private SQLiteDatabase m_readableDb;
|
private SQLiteDatabase m_readableDb;
|
||||||
private SQLiteDatabase m_writableDb;
|
private SQLiteDatabase m_writableDb;
|
||||||
@ -882,6 +884,45 @@ public class MainActivity extends FragmentActivity implements OnlineServices {
|
|||||||
.findFragmentById(R.id.headlines_fragment);
|
.findFragmentById(R.id.headlines_fragment);
|
||||||
|
|
||||||
switch (item.getItemId()) {
|
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:
|
case android.R.id.home:
|
||||||
goBack(false);
|
goBack(false);
|
||||||
return true;
|
return true;
|
||||||
@ -1130,7 +1171,7 @@ public class MainActivity extends FragmentActivity implements OnlineServices {
|
|||||||
|
|
||||||
Dialog dialog = new Dialog(MainActivity.this);
|
Dialog dialog = new Dialog(MainActivity.this);
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this)
|
AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this)
|
||||||
.setTitle("Set labels")
|
.setTitle(R.string.article_set_labels)
|
||||||
.setMultiChoiceItems(items, checkedItems, new OnMultiChoiceClickListener() {
|
.setMultiChoiceItems(items, checkedItems, new OnMultiChoiceClickListener() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -1152,7 +1193,7 @@ public class MainActivity extends FragmentActivity implements OnlineServices {
|
|||||||
req.execute(map);
|
req.execute(map);
|
||||||
|
|
||||||
}
|
}
|
||||||
}).setPositiveButton("Close", new OnClickListener() {
|
}).setPositiveButton(R.string.dialog_close, new OnClickListener() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
@ -1359,6 +1400,8 @@ public class MainActivity extends FragmentActivity implements OnlineServices {
|
|||||||
|
|
||||||
m_menu.findItem(R.id.set_labels).setEnabled(m_apiLevel >= 1);
|
m_menu.findItem(R.id.set_labels).setEnabled(m_apiLevel >= 1);
|
||||||
|
|
||||||
|
m_menu.findItem(R.id.donate).setVisible(BillingHelper.isBillingSupported());
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
m_menu.setGroupVisible(R.id.menu_group_logged_in, false);
|
m_menu.setGroupVisible(R.id.menu_group_logged_in, false);
|
||||||
m_menu.setGroupVisible(R.id.menu_group_logged_out, true);
|
m_menu.setGroupVisible(R.id.menu_group_logged_out, true);
|
||||||
@ -1410,6 +1453,8 @@ public class MainActivity extends FragmentActivity implements OnlineServices {
|
|||||||
|
|
||||||
m_isOffline = false;
|
m_isOffline = false;
|
||||||
|
|
||||||
|
startService(new Intent(MainActivity.this, BillingService.class));
|
||||||
|
|
||||||
initMainMenu();
|
initMainMenu();
|
||||||
|
|
||||||
if (m_refreshTask != null) {
|
if (m_refreshTask != null) {
|
||||||
|
582
src/org/fox/ttrss/util/Base64.java
Normal file
582
src/org/fox/ttrss/util/Base64.java
Normal file
@ -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 rharder@usa.net
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
}
|
32
src/org/fox/ttrss/util/Base64DecoderException.java
Normal file
32
src/org/fox/ttrss/util/Base64DecoderException.java
Normal file
@ -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;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user