remove in-app billing
add spinner-like triangles for headlines fragment context menu bump version
This commit is contained in:
parent
e68c90cbce
commit
77352728e7
@ -1,15 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="org.fox.ttrss"
|
||||
android:versionCode="86"
|
||||
android:versionName="0.6.10" >
|
||||
android:versionCode="87"
|
||||
android:versionName="0.6.11" >
|
||||
|
||||
<uses-sdk android:minSdkVersion="8" android:targetSdkVersion="11" />
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="com.android.vending.BILLING" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
@ -48,8 +47,6 @@
|
||||
<service
|
||||
android:enabled="true"
|
||||
android:name=".util.ImageCacheService" />
|
||||
|
||||
<service android:name=".billing.BillingService" />
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.android.backup.api_key"
|
||||
|
BIN
res/drawable-hdpi/ic_mailbox_collapsed_holo_light.png
Normal file
BIN
res/drawable-hdpi/ic_mailbox_collapsed_holo_light.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 350 B |
@ -69,8 +69,8 @@
|
||||
android:layout_weight="1"
|
||||
android:text="Jan 01, 00:00"
|
||||
android:textColor="?headlineExcerptTextColor"
|
||||
android:textSize="11sp" />
|
||||
|
||||
android:textSize="11sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/feed_title"
|
||||
android:layout_width="wrap_content"
|
||||
@ -136,4 +136,12 @@
|
||||
android:text="@string/attachment_copy" />
|
||||
</LinearLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/article_menu_button"
|
||||
android:layout_gravity="right"
|
||||
android:background="@android:color/transparent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_mailbox_collapsed_holo_light" />
|
||||
|
||||
</LinearLayout>
|
@ -69,7 +69,7 @@
|
||||
android:layout_weight="1"
|
||||
android:text="Jan 01, 00:00"
|
||||
android:textColor="?headlineSelectedExcerptTextColor"
|
||||
android:textSize="11sp" />
|
||||
android:textSize="11sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/feed_title"
|
||||
@ -137,4 +137,12 @@
|
||||
android:text="@string/attachment_copy" />
|
||||
</LinearLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/article_menu_button"
|
||||
android:layout_gravity="right"
|
||||
android:background="@android:color/transparent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_mailbox_collapsed_holo_light" />
|
||||
|
||||
</LinearLayout>
|
@ -136,4 +136,13 @@
|
||||
android:text="@string/attachment_copy" />
|
||||
</LinearLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/article_menu_button"
|
||||
android:layout_gravity="right"
|
||||
android:background="@android:color/transparent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_mailbox_collapsed_holo_light" />
|
||||
|
||||
|
||||
</LinearLayout>
|
@ -130,4 +130,12 @@
|
||||
android:text="@string/attachment_copy" />
|
||||
</LinearLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/article_menu_button"
|
||||
android:layout_gravity="right"
|
||||
android:background="@android:color/transparent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_mailbox_collapsed_holo_light" />
|
||||
|
||||
</LinearLayout>
|
@ -129,4 +129,13 @@
|
||||
android:text="@string/attachment_copy" />
|
||||
</LinearLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/article_menu_button"
|
||||
android:layout_gravity="right"
|
||||
android:background="@android:color/transparent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_mailbox_collapsed_holo_light" />
|
||||
|
||||
|
||||
</LinearLayout>
|
@ -129,4 +129,13 @@
|
||||
android:text="@string/attachment_copy" />
|
||||
</LinearLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/article_menu_button"
|
||||
android:layout_gravity="right"
|
||||
android:background="@android:color/transparent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_mailbox_collapsed_holo_light" />
|
||||
|
||||
|
||||
</LinearLayout>
|
@ -143,4 +143,13 @@
|
||||
android:text="@string/attachment_copy" />
|
||||
</LinearLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/article_menu_button"
|
||||
android:layout_gravity="right"
|
||||
android:background="@android:color/transparent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_mailbox_collapsed_holo_light" />
|
||||
|
||||
|
||||
</LinearLayout>
|
@ -142,4 +142,12 @@
|
||||
android:text="@string/attachment_copy" />
|
||||
</LinearLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/article_menu_button"
|
||||
android:layout_gravity="right"
|
||||
android:background="@android:color/transparent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_mailbox_collapsed_holo_light" />
|
||||
|
||||
</LinearLayout>
|
@ -141,4 +141,13 @@
|
||||
android:layout_weight="0"
|
||||
android:text="@string/attachment_copy" />
|
||||
</LinearLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/article_menu_button"
|
||||
android:layout_gravity="right"
|
||||
android:background="@android:color/transparent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_mailbox_collapsed_holo_light" />
|
||||
|
||||
</LinearLayout>
|
@ -155,11 +155,6 @@
|
||||
android:showAsAction=""
|
||||
android:title="@string/preferences"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/donate"
|
||||
android:showAsAction=""
|
||||
android:title="@string/donate"/>
|
||||
|
||||
<group android:id="@+id/menu_group_logged_out" >
|
||||
|
||||
<item
|
||||
|
@ -1,24 +0,0 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
@ -45,6 +45,7 @@ import android.widget.AdapterView.OnItemClickListener;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Button;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ListView;
|
||||
import android.widget.Spinner;
|
||||
@ -596,6 +597,18 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener,
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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";
|
||||
|
||||
}
|
@ -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<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");
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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<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;
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user