diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 10a13213..a2aba21c 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1,15 +1,14 @@ + android:versionCode="87" + android:versionName="0.6.11" > - - - - + android:textSize="11sp" /> + + + \ No newline at end of file diff --git a/res/layout-port/headlines_row_selected.xml b/res/layout-port/headlines_row_selected.xml index 7f669957..f348cc8e 100644 --- a/res/layout-port/headlines_row_selected.xml +++ b/res/layout-port/headlines_row_selected.xml @@ -69,7 +69,7 @@ android:layout_weight="1" android:text="Jan 01, 00:00" android:textColor="?headlineSelectedExcerptTextColor" - android:textSize="11sp" /> + android:textSize="11sp" /> + + \ No newline at end of file diff --git a/res/layout-port/headlines_row_unread.xml b/res/layout-port/headlines_row_unread.xml index 98dc6248..7770bcc0 100644 --- a/res/layout-port/headlines_row_unread.xml +++ b/res/layout-port/headlines_row_unread.xml @@ -136,4 +136,13 @@ android:text="@string/attachment_copy" /> + + + \ No newline at end of file diff --git a/res/layout-xlarge/headlines_row.xml b/res/layout-xlarge/headlines_row.xml index 6b46d81d..bb673f9f 100644 --- a/res/layout-xlarge/headlines_row.xml +++ b/res/layout-xlarge/headlines_row.xml @@ -130,4 +130,12 @@ android:text="@string/attachment_copy" /> + + \ No newline at end of file diff --git a/res/layout-xlarge/headlines_row_selected.xml b/res/layout-xlarge/headlines_row_selected.xml index 66343703..5df99772 100644 --- a/res/layout-xlarge/headlines_row_selected.xml +++ b/res/layout-xlarge/headlines_row_selected.xml @@ -129,4 +129,13 @@ android:text="@string/attachment_copy" /> + + + \ No newline at end of file diff --git a/res/layout-xlarge/headlines_row_unread.xml b/res/layout-xlarge/headlines_row_unread.xml index 3e66e6ed..5ccac557 100644 --- a/res/layout-xlarge/headlines_row_unread.xml +++ b/res/layout-xlarge/headlines_row_unread.xml @@ -129,4 +129,13 @@ android:text="@string/attachment_copy" /> + + + \ No newline at end of file diff --git a/res/layout/headlines_row.xml b/res/layout/headlines_row.xml index b028eeb0..a339b18e 100644 --- a/res/layout/headlines_row.xml +++ b/res/layout/headlines_row.xml @@ -143,4 +143,13 @@ android:text="@string/attachment_copy" /> + + + \ No newline at end of file diff --git a/res/layout/headlines_row_selected.xml b/res/layout/headlines_row_selected.xml index e18f866e..edffdd2c 100644 --- a/res/layout/headlines_row_selected.xml +++ b/res/layout/headlines_row_selected.xml @@ -142,4 +142,12 @@ android:text="@string/attachment_copy" /> + + \ No newline at end of file diff --git a/res/layout/headlines_row_unread.xml b/res/layout/headlines_row_unread.xml index 33d50dfa..96fa7bd1 100644 --- a/res/layout/headlines_row_unread.xml +++ b/res/layout/headlines_row_unread.xml @@ -141,4 +141,13 @@ android:layout_weight="0" android:text="@string/attachment_copy" /> + + + \ No newline at end of file diff --git a/res/menu/main_menu.xml b/res/menu/main_menu.xml index 5b6f34a8..c6e49511 100644 --- a/res/menu/main_menu.xml +++ b/res/menu/main_menu.xml @@ -155,11 +155,6 @@ android:showAsAction="" android:title="@string/preferences"/> - - = 10 ? View.VISIBLE : View.GONE); + ib.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + getActivity().openContextMenu(v); + } + }); + } + return v; } } diff --git a/src/org/fox/ttrss/MainActivity.java b/src/org/fox/ttrss/MainActivity.java index c3a7ea21..5cb281ef 100644 --- a/src/org/fox/ttrss/MainActivity.java +++ b/src/org/fox/ttrss/MainActivity.java @@ -8,8 +8,6 @@ import java.util.List; import java.util.Timer; import java.util.TimerTask; -import org.fox.ttrss.billing.BillingHelper; -import org.fox.ttrss.billing.BillingService; import org.fox.ttrss.offline.OfflineActivity; import org.fox.ttrss.offline.OfflineDownloadService; import org.fox.ttrss.offline.OfflineUploadService; @@ -1037,45 +1035,6 @@ public class MainActivity extends CommonActivity implements OnlineServices { case R.id.close_article: closeArticle(); return true; - 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; @@ -1598,8 +1557,6 @@ public class MainActivity extends CommonActivity implements OnlineServices { m_menu.findItem(R.id.set_labels).setEnabled(m_apiLevel >= 1); m_menu.findItem(R.id.article_set_note).setEnabled(m_apiLevel >= 1); - - m_menu.findItem(R.id.donate).setVisible(BillingHelper.isBillingSupported()); } else { m_menu.setGroupVisible(R.id.menu_group_logged_in, false); @@ -1650,8 +1607,6 @@ public class MainActivity extends CommonActivity implements OnlineServices { setProgressBarIndeterminateVisibility(false); m_isOffline = false; - - startService(new Intent(MainActivity.this, BillingService.class)); initMainMenu(); diff --git a/src/org/fox/ttrss/billing/BillingConstants.java b/src/org/fox/ttrss/billing/BillingConstants.java deleted file mode 100644 index eb440219..00000000 --- a/src/org/fox/ttrss/billing/BillingConstants.java +++ /dev/null @@ -1,63 +0,0 @@ -package org.fox.ttrss.billing; - - -public class BillingConstants { - - // The response codes for a request, defined by Android Market. - public enum ResponseCode { - RESULT_OK, - RESULT_USER_CANCELED, - RESULT_SERVICE_UNAVAILABLE, - RESULT_BILLING_UNAVAILABLE, - RESULT_ITEM_UNAVAILABLE, - RESULT_DEVELOPER_ERROR, - RESULT_ERROR; - - // Converts from an ordinal value to the ResponseCode - public static ResponseCode valueOf(int index) { - ResponseCode[] values = ResponseCode.values(); - if (index < 0 || index >= values.length) { - return RESULT_ERROR; - } - return values[index]; - } - } - - // The possible states of an in-app purchase, as defined by Android Market. - public enum PurchaseState { - // Responses to requestPurchase or restoreTransactions. - PURCHASED, // User was charged for the order. - CANCELED, // The charge failed on the server. - REFUNDED; // User received a refund for the order. - - // Converts from an ordinal value to the PurchaseState - public static PurchaseState valueOf(int index) { - PurchaseState[] values = PurchaseState.values(); - if (index < 0 || index >= values.length) { - return CANCELED; - } - return values[index]; - } - } - - // These are the names of the extras that are passed in an intent from - // Market to this application and cannot be changed. - public static final String NOTIFICATION_ID = "notification_id"; - public static final String INAPP_SIGNED_DATA = "inapp_signed_data"; - public static final String INAPP_SIGNATURE = "inapp_signature"; - public static final String INAPP_REQUEST_ID = "request_id"; - public static final String INAPP_RESPONSE_CODE = "response_code"; - - // Intent actions that we send from the BillingReceiver to the - // BillingService. Defined by this application. - public static final String ACTION_CONFIRM_NOTIFICATION = "com.example.dungeons.CONFIRM_NOTIFICATION"; - public static final String ACTION_GET_PURCHASE_INFORMATION = "com.example.dungeons.GET_PURCHASE_INFORMATION"; - public static final String ACTION_RESTORE_TRANSACTIONS = "com.example.dungeons.RESTORE_TRANSACTIONS"; - - // Intent actions that we receive in the BillingReceiver from Market. - // These are defined by Market and cannot be changed. - public static final String ACTION_NOTIFY = "com.android.vending.billing.IN_APP_NOTIFY"; - public static final String ACTION_RESPONSE_CODE = "com.android.vending.billing.RESPONSE_CODE"; - public static final String ACTION_PURCHASE_STATE_CHANGED = "com.android.vending.billing.PURCHASE_STATE_CHANGED"; - -} diff --git a/src/org/fox/ttrss/billing/BillingHelper.java b/src/org/fox/ttrss/billing/BillingHelper.java deleted file mode 100644 index dcf29322..00000000 --- a/src/org/fox/ttrss/billing/BillingHelper.java +++ /dev/null @@ -1,268 +0,0 @@ -package org.fox.ttrss.billing; - -import java.util.ArrayList; - -import android.app.PendingIntent; -import android.app.PendingIntent.CanceledException; -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.os.Handler; -import android.os.RemoteException; -import android.util.Log; - -import com.android.vending.billing.IMarketBillingService; - -import org.fox.ttrss.billing.BillingConstants.ResponseCode; -import org.fox.ttrss.billing.BillingSecurity.VerifiedPurchase; - -public class BillingHelper { - - private static final String TAG = "BillingService"; - - private static IMarketBillingService mService; - private static Context mContext; - private static Handler mCompletedHandler; - - protected static VerifiedPurchase latestPurchase; - - protected static void instantiateHelper(Context context, IMarketBillingService service) { - mService = service; - mContext = context; - } - - protected static void setCompletedHandler(Handler handler){ - mCompletedHandler = handler; - } - - public static boolean isBillingSupported() { - if (amIDead()) { - return false; - } - Bundle request = makeRequestBundle("CHECK_BILLING_SUPPORTED"); - if (mService != null) { - try { - Bundle response = mService.sendBillingRequest(request); - ResponseCode code = ResponseCode.valueOf((Integer) response.get("RESPONSE_CODE")); - Log.i(TAG, "isBillingSupported response was: " + code.toString()); - if (ResponseCode.RESULT_OK.equals(code)) { - return true; - } else { - return false; - } - } catch (RemoteException e) { - Log.e(TAG, "isBillingSupported response was: RemoteException", e); - return false; - } - } else { - Log.i(TAG, "isBillingSupported response was: BillingService.mService = null"); - return false; - } - } - - /** - * A REQUEST_PURCHASE request also triggers two asynchronous responses (broadcast intents). - * First, the Android Market application sends a RESPONSE_CODE broadcast intent, which provides error information about the request. (which I ignore) - * Next, if the request was successful, the Android Market application sends an IN_APP_NOTIFY broadcast intent. - * This message contains a notification ID, which you can use to retrieve the transaction details for the REQUEST_PURCHASE - * @param activityContext - * @param itemId - */ - public static void requestPurchase(Context activityContext, String itemId){ - if (amIDead()) { - return; - } - Log.i(TAG, "requestPurchase()"); - Bundle request = makeRequestBundle("REQUEST_PURCHASE"); - request.putString("ITEM_ID", itemId); - try { - Bundle response = mService.sendBillingRequest(request); - - //The RESPONSE_CODE key provides you with the status of the request - Integer responseCodeIndex = (Integer) response.get("RESPONSE_CODE"); - //The PURCHASE_INTENT key provides you with a PendingIntent, which you can use to launch the checkout UI - PendingIntent pendingIntent = (PendingIntent) response.get("PURCHASE_INTENT"); - //The REQUEST_ID key provides you with a unique request identifier for the request - Long requestIndentifier = (Long) response.get("REQUEST_ID"); - Log.i(TAG, "current request is:" + requestIndentifier); - BillingConstants.ResponseCode responseCode = BillingConstants.ResponseCode.valueOf(responseCodeIndex); - Log.i(TAG, "REQUEST_PURCHASE Sync Response code: "+responseCode.toString()); - - startBuyPageActivity(pendingIntent, new Intent(), activityContext); - } catch (RemoteException e) { - Log.e(TAG, "Failed, internet error maybe", e); - Log.e(TAG, "Billing supported: "+isBillingSupported()); - } - } - - /** - * A GET_PURCHASE_INFORMATION request also triggers two asynchronous responses (broadcast intents). - * First, the Android Market application sends a RESPONSE_CODE broadcast intent, which provides status and error information about the request. (which I ignore) - * Next, if the request was successful, the Android Market application sends a PURCHASE_STATE_CHANGED broadcast intent. - * This message contains detailed transaction information. - * The transaction information is contained in a signed JSON string (unencrypted). - * The message includes the signature so you can verify the integrity of the signed string - * @param notifyIds - */ - protected static void getPurchaseInformation(String[] notifyIds){ - if (amIDead()) { - return; - } - Log.i(TAG, "getPurchaseInformation()"); - Bundle request = makeRequestBundle("GET_PURCHASE_INFORMATION"); - // The REQUEST_NONCE key contains a cryptographically secure nonce (number used once) that you must generate. - // The Android Market application returns this nonce with the PURCHASE_STATE_CHANGED broadcast intent so you can verify the integrity of the transaction information. - request.putLong("NONCE", BillingSecurity.generateNonce()); - // The NOTIFY_IDS key contains an array of notification IDs, which you received in the IN_APP_NOTIFY broadcast intent. - request.putStringArray("NOTIFY_IDS", notifyIds); - try { - Bundle response = mService.sendBillingRequest(request); - - //The REQUEST_ID key provides you with a unique request identifier for the request - Long requestIndentifier = (Long) response.get("REQUEST_ID"); - Log.i(TAG, "current request is:" + requestIndentifier); - //The RESPONSE_CODE key provides you with the status of the request - Integer responseCodeIndex = (Integer) response.get("RESPONSE_CODE"); - BillingConstants.ResponseCode responseCode = BillingConstants.ResponseCode.valueOf(responseCodeIndex); - Log.i(TAG, "GET_PURCHASE_INFORMATION Sync Response code: "+responseCode.toString()); - - } catch (RemoteException e) { - Log.e(TAG, "Failed, internet error maybe", e); - Log.e(TAG, "Billing supported: "+isBillingSupported()); - } - } - - /** - * To acknowledge that you received transaction information you send a - * CONFIRM_NOTIFICATIONS request. - * - * A CONFIRM_NOTIFICATIONS request triggers a single asynchronous response�a RESPONSE_CODE broadcast intent. - * This broadcast intent provides status and error information about the request. - * - * Note: As a best practice, you should not send a CONFIRM_NOTIFICATIONS request for a purchased item until you have delivered the item to the user. - * This way, if your application crashes or something else prevents your application from delivering the product, - * your application will still receive an IN_APP_NOTIFY broadcast intent from Android Market indicating that you need to deliver the product - * @param notifyIds - */ - protected static void confirmTransaction(String[] notifyIds) { - if (amIDead()) { - return; - } - Log.i(TAG, "confirmTransaction()"); - Bundle request = makeRequestBundle("CONFIRM_NOTIFICATIONS"); - request.putStringArray("NOTIFY_IDS", notifyIds); - try { - Bundle response = mService.sendBillingRequest(request); - - //The REQUEST_ID key provides you with a unique request identifier for the request - Long requestIndentifier = (Long) response.get("REQUEST_ID"); - Log.i(TAG, "current request is:" + requestIndentifier); - - //The RESPONSE_CODE key provides you with the status of the request - Integer responseCodeIndex = (Integer) response.get("RESPONSE_CODE"); - BillingConstants.ResponseCode responseCode = BillingConstants.ResponseCode.valueOf(responseCodeIndex); - - Log.i(TAG, "CONFIRM_NOTIFICATIONS Sync Response code: "+responseCode.toString()); - } catch (RemoteException e) { - Log.e(TAG, "Failed, internet error maybe", e); - Log.e(TAG, "Billing supported: " + isBillingSupported()); - } - } - - /** - * - * Can be used for when a user has reinstalled the app to give back prior purchases. - * if an item for sale's purchase type is "managed per user account" this means google will have a record ofthis transaction - * - * A RESTORE_TRANSACTIONS request also triggers two asynchronous responses (broadcast intents). - * First, the Android Market application sends a RESPONSE_CODE broadcast intent, which provides status and error information about the request. - * Next, if the request was successful, the Android Market application sends a PURCHASE_STATE_CHANGED broadcast intent. - * This message contains the detailed transaction information. The transaction information is contained in a signed JSON string (unencrypted). - * The message includes the signature so you can verify the integrity of the signed string - * @param nonce - */ - protected static void restoreTransactionInformation(Long nonce) { - if (amIDead()) { - return; - } - Log.i(TAG, "confirmTransaction()"); - Bundle request = makeRequestBundle("RESTORE_TRANSACTIONS"); - // The REQUEST_NONCE key contains a cryptographically secure nonce (number used once) that you must generate - request.putLong("NONCE", nonce); - try { - Bundle response = mService.sendBillingRequest(request); - - //The REQUEST_ID key provides you with a unique request identifier for the request - Long requestIndentifier = (Long) response.get("REQUEST_ID"); - Log.i(TAG, "current request is:" + requestIndentifier); - - //The RESPONSE_CODE key provides you with the status of the request - Integer responseCodeIndex = (Integer) response.get("RESPONSE_CODE"); - BillingConstants.ResponseCode responseCode = BillingConstants.ResponseCode.valueOf(responseCodeIndex); - Log.i(TAG, "RESTORE_TRANSACTIONS Sync Response code: "+responseCode.toString()); - } catch (RemoteException e) { - Log.e(TAG, "Failed, internet error maybe", e); - Log.e(TAG, "Billing supported: " + isBillingSupported()); - } - } - - private static boolean amIDead() { - if (mService == null || mContext == null) { - Log.e(TAG, "BillingHelper not fully instantiated"); - return true; - } else { - return false; - } - } - - private static Bundle makeRequestBundle(String method) { - Bundle request = new Bundle(); - request.putString("BILLING_REQUEST", method); - request.putInt("API_VERSION", 1); - request.putString("PACKAGE_NAME", mContext.getPackageName()); - return request; - } - - /** - * - * - * You must launch the pending intent from an activity context and not an application context - * You cannot use the singleTop launch mode to launch the pending intent - * @param pendingIntent - * @param intent - * @param context - */ - private static void startBuyPageActivity(PendingIntent pendingIntent, Intent intent, Context context){ - //TODO add above 2.0 implementation with reflection, for now just using 1.6 implem - - // This is on Android 1.6. The in-app checkout page activity will be on its - // own separate activity stack instead of on the activity stack of - // the application. - try { - pendingIntent.send(context, 0, intent); - } catch (CanceledException e){ - Log.e(TAG, "startBuyPageActivity CanceledException"); - } - } - - protected static void verifyPurchase(String signedData, String signature) { - ArrayList purchases = BillingSecurity.verifyPurchase(signedData, signature); - latestPurchase = purchases.get(0); - - confirmTransaction(new String[]{latestPurchase.notificationId}); - - if(mCompletedHandler != null){ - mCompletedHandler.sendEmptyMessage(0); - } else { - Log.e(TAG, "verifyPurchase error. Handler not instantiated. Have you called setCompletedHandler()?"); - } - } - - public static void stopService(){ - mContext.stopService(new Intent(mContext, BillingService.class)); - mService = null; - mContext = null; - mCompletedHandler = null; - Log.i(TAG, "Stopping Service"); - } -} diff --git a/src/org/fox/ttrss/billing/BillingReceiver.java b/src/org/fox/ttrss/billing/BillingReceiver.java deleted file mode 100644 index 9b772054..00000000 --- a/src/org/fox/ttrss/billing/BillingReceiver.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.fox.ttrss.billing; - -import static org.fox.ttrss.billing.BillingConstants.ACTION_NOTIFY; -import static org.fox.ttrss.billing.BillingConstants.ACTION_PURCHASE_STATE_CHANGED; -import static org.fox.ttrss.billing.BillingConstants.ACTION_RESPONSE_CODE; -import static org.fox.ttrss.billing.BillingConstants.INAPP_REQUEST_ID; -import static org.fox.ttrss.billing.BillingConstants.INAPP_RESPONSE_CODE; -import static org.fox.ttrss.billing.BillingConstants.INAPP_SIGNATURE; -import static org.fox.ttrss.billing.BillingConstants.INAPP_SIGNED_DATA; -import static org.fox.ttrss.billing.BillingConstants.NOTIFICATION_ID; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.util.Log; - -public class BillingReceiver extends BroadcastReceiver { - - private static final String TAG = "BillingService"; - - @Override - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - Log.i(TAG, "Received action: " + action); - if (ACTION_PURCHASE_STATE_CHANGED.equals(action)) { - String signedData = intent.getStringExtra(INAPP_SIGNED_DATA); - String signature = intent.getStringExtra(INAPP_SIGNATURE); - purchaseStateChanged(context, signedData, signature); - } else if (ACTION_NOTIFY.equals(action)) { - String notifyId = intent.getStringExtra(NOTIFICATION_ID); - notify(context, notifyId); - } else if (ACTION_RESPONSE_CODE.equals(action)) { - long requestId = intent.getLongExtra(INAPP_REQUEST_ID, -1); - int responseCodeIndex = intent.getIntExtra(INAPP_RESPONSE_CODE, BillingConstants.ResponseCode.RESULT_ERROR.ordinal()); - checkResponseCode(context, requestId, responseCodeIndex); - } else { - Log.e(TAG, "unexpected action: " + action); - } - } - - - private void purchaseStateChanged(Context context, String signedData, String signature) { - Log.i(TAG, "purchaseStateChanged got signedData: " + signedData); - Log.i(TAG, "purchaseStateChanged got signature: " + signature); - BillingHelper.verifyPurchase(signedData, signature); - } - - private void notify(Context context, String notifyId) { - Log.i(TAG, "notify got id: " + notifyId); - String[] notifyIds = {notifyId}; - BillingHelper.getPurchaseInformation(notifyIds); - } - - private void checkResponseCode(Context context, long requestId, int responseCodeIndex) { - Log.i(TAG, "checkResponseCode got requestId: " + requestId); - Log.i(TAG, "checkResponseCode got responseCode: " + BillingConstants.ResponseCode.valueOf(responseCodeIndex)); - } -} \ No newline at end of file diff --git a/src/org/fox/ttrss/billing/BillingSecurity.java b/src/org/fox/ttrss/billing/BillingSecurity.java deleted file mode 100644 index 513d6f34..00000000 --- a/src/org/fox/ttrss/billing/BillingSecurity.java +++ /dev/null @@ -1,258 +0,0 @@ -// Copyright 2010 Google Inc. All Rights Reserved. - -package org.fox.ttrss.billing; - -import java.security.InvalidKeyException; -import java.security.KeyFactory; -import java.security.NoSuchAlgorithmException; -import java.security.PublicKey; -import java.security.SecureRandom; -import java.security.Signature; -import java.security.SignatureException; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.X509EncodedKeySpec; -import java.util.ArrayList; -import java.util.HashSet; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import android.text.TextUtils; -import android.util.Log; - -import org.fox.ttrss.billing.BillingConstants.PurchaseState; -import org.fox.ttrss.util.Base64; -import org.fox.ttrss.util.Base64DecoderException; - -/** - * Security-related methods. For a secure implementation, all of this code - * should be implemented on a server that communicates with the application on - * the device. For the sake of simplicity and clarity of this example, this code - * is included here and is executed on the device. If you must verify the - * purchases on the phone, you should obfuscate this code to make it harder for - * an attacker to replace the code with stubs that treat all purchases as - * verified. - */ -public class BillingSecurity { - private static final String TAG = "BillingService"; - - private static final String KEY_FACTORY_ALGORITHM = "RSA"; - private static final String SIGNATURE_ALGORITHM = "SHA1withRSA"; - private static final SecureRandom RANDOM = new SecureRandom(); - - /** - * This keeps track of the nonces that we generated and sent to the server. - * We need to keep track of these until we get back the purchase state and - * send a confirmation message back to Android Market. If we are killed and - * lose this list of nonces, it is not fatal. Android Market will send us a - * new "notify" message and we will re-generate a new nonce. This has to be - * "static" so that the {@link BillingReceiver} can check if a nonce exists. - */ - private static HashSet sKnownNonces = new HashSet(); - - /** - * 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 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 purchases = new ArrayList(); - try { - for (int i = 0; i < numTransactions; i++) { - JSONObject jElement = jTransactionsArray.getJSONObject(i); - int response = jElement.getInt("purchaseState"); - PurchaseState purchaseState = PurchaseState.valueOf(response); - String productId = jElement.getString("productId"); - String packageName = jElement.getString("packageName"); - long purchaseTime = jElement.getLong("purchaseTime"); - String orderId = jElement.optString("orderId", ""); - String notifyId = null; - if (jElement.has("notificationId")) { - notifyId = jElement.getString("notificationId"); - } - String developerPayload = jElement.optString("developerPayload", null); - - // If the purchase state is PURCHASED, then we require a - // verified nonce. - if (purchaseState == PurchaseState.PURCHASED && !verified) { - continue; - } - purchases.add(new VerifiedPurchase(purchaseState, notifyId, productId, orderId, purchaseTime, developerPayload)); - } - } catch (JSONException e) { - Log.e(TAG, "JSON exception: ", e); - return null; - } - removeNonce(nonce); - return purchases; - } - - /** - * Generates a PublicKey instance from a string containing the - * Base64-encoded public key. - * - * @param encodedPublicKey - * Base64-encoded public key - * @throws IllegalArgumentException - * if encodedPublicKey is invalid - */ - public static PublicKey generatePublicKey(String encodedPublicKey) { - try { - byte[] decodedKey = Base64.decode(encodedPublicKey); - KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM); - return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey)); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } catch (InvalidKeySpecException e) { - Log.e(TAG, "Invalid key specification."); - throw new IllegalArgumentException(e); - } catch (Base64DecoderException e) { - Log.e(TAG, "Base64DecoderException.", e); - return null; - } - } - - /** - * Verifies that the signature from the server matches the computed - * signature on the data. Returns true if the data is correctly signed. - * - * @param publicKey - * public key associated with the developer account - * @param signedData - * signed data from server - * @param signature - * server signature - * @return true if the data and signature match - */ - public static boolean verify(PublicKey publicKey, String signedData, String signature) { - Log.i(TAG, "signature: " + signature); - Signature sig; - try { - sig = Signature.getInstance(SIGNATURE_ALGORITHM); - sig.initVerify(publicKey); - sig.update(signedData.getBytes()); - if (!sig.verify(Base64.decode(signature))) { - Log.e(TAG, "Signature verification failed."); - return false; - } - return true; - } catch (NoSuchAlgorithmException e) { - Log.e(TAG, "NoSuchAlgorithmException."); - } catch (InvalidKeyException e) { - Log.e(TAG, "Invalid key specification."); - } catch (SignatureException e) { - Log.e(TAG, "Signature exception."); - } catch (Base64DecoderException e) { - Log.e(TAG, "Base64DecoderException.", e); - } - return false; - } -} diff --git a/src/org/fox/ttrss/billing/BillingService.java b/src/org/fox/ttrss/billing/BillingService.java deleted file mode 100644 index 2ae53234..00000000 --- a/src/org/fox/ttrss/billing/BillingService.java +++ /dev/null @@ -1,60 +0,0 @@ -package org.fox.ttrss.billing; - -import android.app.Service; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.ServiceConnection; -import android.os.IBinder; -import android.util.Log; - -import com.android.vending.billing.IMarketBillingService; - -public class BillingService extends Service implements ServiceConnection{ - - private static final String TAG = "BillingService"; - - /** The service connection to the remote MarketBillingService. */ - private IMarketBillingService mService; - - @Override - public void onCreate() { - super.onCreate(); - Log.i(TAG, "Service starting with onCreate"); - - try { - boolean bindResult = bindService(new Intent("com.android.vending.billing.MarketBillingService.BIND"), this, Context.BIND_AUTO_CREATE); - if(bindResult){ - Log.i(TAG,"Market Billing Service Successfully Bound"); - } else { - Log.e(TAG,"Market Billing Service could not be bound."); - //TODO stop user continuing - } - } catch (SecurityException e){ - Log.e(TAG,"Market Billing Service could not be bound. SecurityException: "+e); - //TODO stop user continuing - } - } - - public void setContext(Context context) { - attachBaseContext(context); - } - - @Override - public IBinder onBind(Intent intent) { - return null; - } - - @Override - public void onServiceConnected(ComponentName name, IBinder service) { - Log.i(TAG, "Market Billing Service Connected."); - mService = IMarketBillingService.Stub.asInterface(service); - BillingHelper.instantiateHelper(getBaseContext(), mService); - } - - @Override - public void onServiceDisconnected(ComponentName name) { - - } - -} diff --git a/src/org/fox/ttrss/offline/OfflineHeadlinesFragment.java b/src/org/fox/ttrss/offline/OfflineHeadlinesFragment.java index 69e662ab..40d42eb1 100644 --- a/src/org/fox/ttrss/offline/OfflineHeadlinesFragment.java +++ b/src/org/fox/ttrss/offline/OfflineHeadlinesFragment.java @@ -34,6 +34,7 @@ import android.widget.AdapterView; import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.AdapterView.OnItemClickListener; import android.widget.CheckBox; +import android.widget.ImageButton; import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; @@ -453,6 +454,18 @@ public class OfflineHeadlinesFragment extends Fragment implements OnItemClickLis }); } + ImageButton ib = (ImageButton) v.findViewById(R.id.article_menu_button); + + if (ib != null) { + ib.setVisibility(android.os.Build.VERSION.SDK_INT >= 10 ? View.VISIBLE : View.GONE); + ib.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + getActivity().openContextMenu(v); + } + }); + } + return v; } }