Remove billing

* Remove billing service (#53)
* BILLING permission prevented distribution to various countries
pull/120/head
Alex Baker 10 years ago
parent 1571dd8852
commit 9b6ea1f2de

@ -39,8 +39,6 @@
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!-- Used to initiate sync when device comes back online -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- required for in-app billing of donations -->
<uses-permission android:name="com.android.vending.BILLING" />
<!-- ============================================== Exported Permissions = -->

@ -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);
}

@ -30,8 +30,6 @@ import com.todoroo.astrid.actfm.TagViewFragment;
import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.api.Filter;
import com.todoroo.astrid.api.FilterListItem;
import com.todoroo.astrid.billing.BillingConstants;
import com.todoroo.astrid.billing.BillingService;
import com.todoroo.astrid.core.CoreFilterExposer;
import com.todoroo.astrid.core.CustomFilterActivity;
import com.todoroo.astrid.core.PluginServices;
@ -75,7 +73,6 @@ public class TaskListActivity extends AstridActivity implements OnPageChangeList
private int filterMode;
private FilterModeSpec filterModeSpec;
private BillingService billingService;
/**
* @see android.app.Activity#onCreate(Bundle)
@ -140,9 +137,6 @@ public class TaskListActivity extends AstridActivity implements OnPageChangeList
if (getIntent().hasExtra(TOKEN_SOURCE)) {
trackActivitySource();
}
billingService = new BillingService();
billingService.setActivity(this);
}
@Override
@ -319,7 +313,6 @@ public class TaskListActivity extends AstridActivity implements OnPageChangeList
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
menuDrawer.closeMenu();
menu.findItem(R.id.menu_donate).setVisible(billingService.showDonateOption());
return super.onPrepareOptionsMenu(menu);
}
@ -532,9 +525,6 @@ public class TaskListActivity extends AstridActivity implements OnPageChangeList
rename.putExtra(TagViewFragment.EXTRA_TAG_UUID, renameTag.uuid);
startActivityForResult(rename, FilterListFragment.REQUEST_CUSTOM_INTENT);
return true;
case R.id.menu_donate:
billingService.requestPurchase(BillingConstants.TASKS_DONATION_ITEM_ID, BillingConstants.ITEM_TYPE_INAPP, null);
return true;
default:
return super.onOptionsItemSelected(item);
}
@ -569,28 +559,4 @@ public class TaskListActivity extends AstridActivity implements OnPageChangeList
}
return super.onKeyDown(keyCode, event);
}
@Override
protected void onStart() {
super.onStart();
billingService.onStart();
}
@Override
protected void onStop() {
super.onStop();
billingService.onStop();
}
@Override
protected void onDestroy() {
super.onDestroy();
billingService.unbind();
}
@Override
protected void onResume() {
super.onResume();
billingService.checkBillingSupported();
}
}

@ -1,88 +0,0 @@
package com.todoroo.astrid.billing;
@SuppressWarnings("nls")
public class BillingConstants {
/** This is the action we use to bind to the MarketBillingService. */
public static final String MARKET_BILLING_SERVICE_ACTION = "com.android.vending.billing.MarketBillingService.BIND";
// Intent actions that we send from the BillingReceiver to the
// BillingService. Defined by this application.
public static final String ACTION_CONFIRM_NOTIFICATION = "com.timsu.astrid.subscriptions.CONFIRM_NOTIFICATION";
public static final String ACTION_GET_PURCHASE_INFORMATION = "com.timsu.astrid.subscriptions.GET_PURCHASE_INFORMATION";
// 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";
// 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";
// These are the names of the fields in the request bundle.
public static final String BILLING_REQUEST_METHOD = "BILLING_REQUEST";
public static final String BILLING_REQUEST_API_VERSION = "API_VERSION";
public static final String BILLING_REQUEST_PACKAGE_NAME = "PACKAGE_NAME";
public static final String BILLING_REQUEST_ITEM_ID = "ITEM_ID";
public static final String BILLING_REQUEST_ITEM_TYPE = "ITEM_TYPE";
public static final String BILLING_REQUEST_DEVELOPER_PAYLOAD = "DEVELOPER_PAYLOAD";
public static final String BILLING_REQUEST_NOTIFY_IDS = "NOTIFY_IDS";
public static final String BILLING_REQUEST_NONCE = "NONCE";
public static final String BILLING_RESPONSE_RESPONSE_CODE = "RESPONSE_CODE";
public static final String BILLING_RESPONSE_PURCHASE_INTENT = "PURCHASE_INTENT";
public static final String BILLING_RESPONSE_REQUEST_ID = "REQUEST_ID";
public static final long BILLING_RESPONSE_INVALID_REQUEST_ID = -1;
// These are the types supported in the IAB v2
public static final String ITEM_TYPE_INAPP = "inapp";
public static final String ITEM_TYPE_SUBSCRIPTION = "subs";
public static final String TASKS_DONATION_ITEM_ID = "tasks_donation_4_6";
public static final boolean DEBUG = true;
// 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. (NOT THE SAME AS CANCELING A SUBSCRIPTION)
REFUNDED, // User received a refund for the order.
EXPIRED; // Subscription expired due to non-payment or cancellation
// 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];
}
}
}

@ -1,61 +0,0 @@
package com.todoroo.astrid.billing;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import com.todoroo.astrid.billing.BillingConstants.ResponseCode;
public class BillingReceiver extends BroadcastReceiver {
private static final String TAG = "BillingReceiver"; //$NON-NLS-1$
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
switch (action) {
case BillingConstants.ACTION_PURCHASE_STATE_CHANGED:
String signedData = intent.getStringExtra(BillingConstants.INAPP_SIGNED_DATA);
String signature = intent.getStringExtra(BillingConstants.INAPP_SIGNATURE);
purchaseStateChanged(context, signedData, signature);
break;
case BillingConstants.ACTION_NOTIFY:
String notifyId = intent.getStringExtra(BillingConstants.NOTIFICATION_ID);
Log.i(TAG, "notifyId: " + notifyId); //$NON-NLS-1$
notify(context, notifyId);
break;
case BillingConstants.ACTION_RESPONSE_CODE:
long requestId = intent.getLongExtra(BillingConstants.INAPP_REQUEST_ID, -1);
int responseCodeIndex = intent.getIntExtra(BillingConstants.INAPP_RESPONSE_CODE,
ResponseCode.RESULT_ERROR.ordinal());
checkResponseCode(context, requestId, responseCodeIndex);
break;
default:
Log.w(TAG, "unexpected action: " + action); //$NON-NLS-1$
break;
}
}
private void purchaseStateChanged(Context context, String signedData, String signature) {
Intent intent = new Intent(BillingConstants.ACTION_PURCHASE_STATE_CHANGED);
intent.setClass(context, BillingService.class);
intent.putExtra(BillingConstants.INAPP_SIGNED_DATA, signedData);
intent.putExtra(BillingConstants.INAPP_SIGNATURE, signature);
context.startService(intent);
}
private void notify(Context context, String notifyId) {
Intent intent = new Intent(BillingConstants.ACTION_GET_PURCHASE_INFORMATION);
intent.setClass(context, BillingService.class);
intent.putExtra(BillingConstants.NOTIFICATION_ID, notifyId);
context.startService(intent);
}
private void checkResponseCode(Context context, long requestId, int responseCodeIndex) {
Intent intent = new Intent(BillingConstants.ACTION_RESPONSE_CODE);
intent.setClass(context, BillingService.class);
intent.putExtra(BillingConstants.INAPP_REQUEST_ID, requestId);
intent.putExtra(BillingConstants.INAPP_RESPONSE_CODE, responseCodeIndex);
context.startService(intent);
}
}

@ -1,541 +0,0 @@
package com.todoroo.astrid.billing;
import android.app.Activity;
import android.app.PendingIntent;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import com.android.vending.billing.IMarketBillingService;
import com.todoroo.astrid.billing.BillingConstants.ResponseCode;
import org.tasks.billing.PurchaseHandler;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import static com.todoroo.astrid.billing.Security.Purchase;
@SuppressWarnings("nls")
public class BillingService extends Service implements ServiceConnection {
private static final String TAG = "BillingService";
private static IMarketBillingService marketBillingService;
private static LinkedList<BillingRequest> pendingRequests = new LinkedList<>();
private static HashMap<Long, BillingRequest> sentRequests = new HashMap<>();
private PurchaseObserver purchaseObserver;
private PurchaseHandler purchaseHandler;
public boolean showDonateOption() {
return purchaseHandler.isBillingSupported() && !purchaseHandler.userDonated();
}
abstract class BillingRequest {
private final int mStartId;
protected long mRequestId;
public BillingRequest(int startId) {
mStartId = startId;
}
public int getStartId() {
return mStartId;
}
public boolean runRequest() {
if (runIfConnected()) {
return true;
}
if (bindToMarketBillingService()) {
pendingRequests.add(this);
return true;
}
return false;
}
/**
* Try running the request directly if the service is already connected.
*
* @return true if the request ran successfully; false if the service
* is not connected or there was an error when trying to use it
*/
public boolean runIfConnected() {
if (BillingConstants.DEBUG) {
Log.d(TAG, getClass().getSimpleName());
}
if (marketBillingService != null) {
try {
mRequestId = run();
if (BillingConstants.DEBUG) {
Log.d(TAG, "request id: " + mRequestId);
}
if (mRequestId >= 0) {
sentRequests.put(mRequestId, this);
}
return true;
} catch (RemoteException e) {
onRemoteException(e);
}
}
return false;
}
/**
* Called when a remote exception occurs while trying to execute the
* {@link #run()} method. The derived class can override this to
* execute exception-handling code.
*
* @param e the exception
*/
protected void onRemoteException(RemoteException e) {
Log.w(TAG, "remote billing service crashed");
marketBillingService = null;
}
/**
* The derived class must implement this method.
*
* @throws android.os.RemoteException
*/
abstract protected long run() throws RemoteException;
/**
* This is called when Android Market sends a response code for this
* request.
*
* @param responseCode the response code
*/
protected void responseCodeReceived(ResponseCode responseCode) {
//
}
protected Bundle makeRequestBundle(String method) {
Bundle request = new Bundle();
request.putString(BillingConstants.BILLING_REQUEST_METHOD, method);
request.putInt(BillingConstants.BILLING_REQUEST_API_VERSION, 2);
request.putString(BillingConstants.BILLING_REQUEST_PACKAGE_NAME, getPackageName());
return request;
}
protected void logResponseCode(String method, Bundle response) {
ResponseCode responseCode = ResponseCode.valueOf(
response.getInt(BillingConstants.BILLING_RESPONSE_RESPONSE_CODE));
if (BillingConstants.DEBUG) {
Log.e(TAG, method + " received " + responseCode.toString());
}
}
}
class CheckBillingSupported extends BillingRequest {
public String mProductType = null;
/**
* Constructor
* <p/>
* Note: Support for subscriptions implies support for one-time purchases. However, the
* opposite is not true.
* <p/>
* Developers may want to perform two checks if both one-time and subscription products are
* available.
*
* @param itemType Either BillingConstants.ITEM_TYPE_INAPP or BillingConstants.ITEM_TYPE_SUBSCRIPTION, indicating
* the type of item support is being checked for.
*/
public CheckBillingSupported(String itemType) {
super(-1);
mProductType = itemType;
}
@Override
protected long run() throws RemoteException {
Bundle request = makeRequestBundle("CHECK_BILLING_SUPPORTED");
if (mProductType != null) {
request.putString(BillingConstants.BILLING_REQUEST_ITEM_TYPE, mProductType);
}
Bundle response = marketBillingService.sendBillingRequest(request);
int responseCode = response.getInt(BillingConstants.BILLING_RESPONSE_RESPONSE_CODE);
if (BillingConstants.DEBUG) {
Log.i(TAG, "CheckBillingSupported response code: " +
ResponseCode.valueOf(responseCode));
}
boolean billingSupported = (responseCode == ResponseCode.RESULT_OK.ordinal());
Log.d(TAG, "check billing support type: " + mProductType + ", response code: " + responseCode);
ResponseHandler.checkBillingSupportedResponse(billingSupported, mProductType);
return BillingConstants.BILLING_RESPONSE_INVALID_REQUEST_ID;
}
}
class RequestPurchase extends BillingRequest {
public final String mProductId;
public final String mDeveloperPayload;
public final String mProductType;
/**
* Constructor
*
* @param itemId The ID of the item to be purchased. Will be assumed to be a one-time
* purchase.
* @param itemType Either BillingConstants.ITEM_TYPE_INAPP or BillingConstants.ITEM_TYPE_SUBSCRIPTION,
* indicating the type of item type support is being checked for.
* @param developerPayload Optional data.
*/
public RequestPurchase(String itemId, String itemType, String developerPayload) {
// This object is never created as a side effect of starting this
// service so we pass -1 as the startId to indicate that we should
// not stop this service after executing this request.
super(-1);
mProductId = itemId;
mDeveloperPayload = developerPayload;
mProductType = itemType;
}
@Override
protected long run() throws RemoteException {
Bundle request = makeRequestBundle("REQUEST_PURCHASE");
request.putString(BillingConstants.BILLING_REQUEST_ITEM_ID, mProductId);
request.putString(BillingConstants.BILLING_REQUEST_ITEM_TYPE, mProductType);
// Note that the developer payload is optional.
if (mDeveloperPayload != null) {
request.putString(BillingConstants.BILLING_REQUEST_DEVELOPER_PAYLOAD, mDeveloperPayload);
}
Bundle response = marketBillingService.sendBillingRequest(request);
PendingIntent pendingIntent
= response.getParcelable(BillingConstants.BILLING_RESPONSE_PURCHASE_INTENT);
if (pendingIntent == null) {
Log.e(TAG, "Error with requestPurchase");
return BillingConstants.BILLING_RESPONSE_INVALID_REQUEST_ID;
}
Intent intent = new Intent();
ResponseHandler.buyPageIntentResponse(pendingIntent, intent);
return response.getLong(BillingConstants.BILLING_RESPONSE_REQUEST_ID,
BillingConstants.BILLING_RESPONSE_INVALID_REQUEST_ID);
}
@Override
protected void responseCodeReceived(ResponseCode responseCode) {
Log.d(TAG, "received response code " + responseCode + " for request " + this);
}
}
class ConfirmNotifications extends BillingRequest {
final String[] mNotifyIds;
public ConfirmNotifications(int startId, String[] notifyIds) {
super(startId);
mNotifyIds = notifyIds;
}
@Override
protected long run() throws RemoteException {
Bundle request = makeRequestBundle("CONFIRM_NOTIFICATIONS");
request.putStringArray(BillingConstants.BILLING_REQUEST_NOTIFY_IDS, mNotifyIds);
Bundle response = marketBillingService.sendBillingRequest(request);
logResponseCode("confirmNotifications", response);
return response.getLong(BillingConstants.BILLING_RESPONSE_REQUEST_ID,
BillingConstants.BILLING_RESPONSE_INVALID_REQUEST_ID);
}
}
class GetPurchaseInformation extends BillingRequest {
long mNonce;
final String[] mNotifyIds;
public GetPurchaseInformation(int startId, String[] notifyIds) {
super(startId);
mNotifyIds = notifyIds;
}
@Override
protected long run() throws RemoteException {
mNonce = Security.generateNonce();
Bundle request = makeRequestBundle("GET_PURCHASE_INFORMATION");
request.putLong(BillingConstants.BILLING_REQUEST_NONCE, mNonce);
request.putStringArray(BillingConstants.BILLING_REQUEST_NOTIFY_IDS, mNotifyIds);
Bundle response = marketBillingService.sendBillingRequest(request);
logResponseCode("getPurchaseInformation", response);
return response.getLong(BillingConstants.BILLING_RESPONSE_REQUEST_ID,
BillingConstants.BILLING_RESPONSE_INVALID_REQUEST_ID);
}
}
class RestoreTransactions extends BillingRequest {
long mNonce;
public RestoreTransactions() {
// This object is never created as a side effect of starting this
// service so we pass -1 as the startId to indicate that we should
// not stop this service after executing this request.
super(-1);
}
@Override
protected long run() throws RemoteException {
mNonce = Security.generateNonce();
Bundle request = makeRequestBundle("RESTORE_TRANSACTIONS");
request.putLong(BillingConstants.BILLING_REQUEST_NONCE, mNonce);
Bundle response = marketBillingService.sendBillingRequest(request);
logResponseCode("restoreTransactions", response);
return response.getLong(BillingConstants.BILLING_RESPONSE_REQUEST_ID,
BillingConstants.BILLING_RESPONSE_INVALID_REQUEST_ID);
}
@Override
protected void responseCodeReceived(ResponseCode responseCode) {
Log.d(TAG, "received response code " + responseCode + " for request " + this);
ResponseHandler.responseCodeReceived(responseCode);
}
}
public void setActivity(Activity activity) {
attachBaseContext(activity);
purchaseHandler = new PurchaseHandler(this);
purchaseObserver = new PurchaseObserver(activity, purchaseHandler);
}
@Override
public IBinder onBind(Intent intent) {
return null; // binding not supported for this service
}
@Override
public void onStart(Intent intent, int startId) {
handleCommand(intent, startId);
}
/**
* The {@link BillingReceiver} sends messages to this service using intents.
* Each intent has an action and some extra arguments specific to that action.
*
* @param intent the intent containing one of the supported actions
* @param startId an identifier for the invocation instance of this service
*/
private void handleCommand(Intent intent, int startId) {
if (intent == null) {
return;
}
String action = intent.getAction();
Log.d(TAG, "handleCommand(" + action + ")");
switch (action) {
case BillingConstants.ACTION_CONFIRM_NOTIFICATION:
String[] notifyIds = intent.getStringArrayExtra(BillingConstants.NOTIFICATION_ID);
confirmNotifications(startId, notifyIds);
break;
case BillingConstants.ACTION_GET_PURCHASE_INFORMATION:
String notifyId = intent.getStringExtra(BillingConstants.NOTIFICATION_ID);
getPurchaseInformation(startId, new String[]{notifyId});
break;
case BillingConstants.ACTION_PURCHASE_STATE_CHANGED:
String signedData = intent.getStringExtra(BillingConstants.INAPP_SIGNED_DATA);
purchaseStateChanged(startId, signedData);
break;
case BillingConstants.ACTION_RESPONSE_CODE:
long requestId = intent.getLongExtra(BillingConstants.INAPP_REQUEST_ID, -1);
int responseCodeIndex = intent.getIntExtra(BillingConstants.INAPP_RESPONSE_CODE,
ResponseCode.RESULT_ERROR.ordinal());
ResponseCode responseCode = ResponseCode.valueOf(responseCodeIndex);
checkResponseCode(requestId, responseCode);
break;
}
}
private boolean bindToMarketBillingService() {
Log.d(TAG, "bindToMarketBillingService()");
try {
boolean bindResult = bindService(
new Intent(BillingConstants.MARKET_BILLING_SERVICE_ACTION),
this,
Context.BIND_AUTO_CREATE);
if (bindResult) {
return true;
} else {
Log.e(TAG, "Could not bind to service.");
}
} catch (SecurityException e) {
Log.e(TAG, "Security exception: " + e);
}
return false;
}
public boolean checkBillingSupported() {
return purchaseHandler.userDonated() || new CheckBillingSupported(BillingConstants.ITEM_TYPE_INAPP).runRequest();
}
/**
* Requests that the given item be offered to the user for purchase. When
* the purchase succeeds (or is canceled) the {@link BillingReceiver}
* receives an intent with the action {@link BillingConstants#ACTION_NOTIFY}.
* Returns false if there was an error trying to connect to Android Market.
*
* @param productId an identifier for the item being offered for purchase
* @param itemType Either BillingConstants.ITEM_TYPE_INAPP or BillingConstants.ITEM_TYPE_SUBSCRIPTION, indicating
* the type of item type support is being checked for.
* @param developerPayload a payload that is associated with a given
* purchase, if null, no payload is sent
* @return false if there was an error connecting to Android Market
*/
public boolean requestPurchase(String productId, String itemType, String developerPayload) {
return new RequestPurchase(productId, itemType, developerPayload).runRequest();
}
/**
* Requests transaction information for all managed items. Call this only when the
* application is first installed or after a database wipe. Do NOT call this
* every time the application starts up.
*
* @return false if there was an error connecting to Android Market
*/
public boolean restoreTransactions() {
return new RestoreTransactions().runRequest();
}
/**
* Confirms receipt of a purchase state change. Each {@code notifyId} is
* an opaque identifier that came from the server. This method sends those
* identifiers back to the MarketBillingService, which ACKs them to the
* server. Returns false if there was an error trying to connect to the
* MarketBillingService.
*
* @param startId an identifier for the invocation instance of this service
* @param notifyIds a list of opaque identifiers associated with purchase
* state changes.
* @return false if there was an error connecting to Market
*/
private boolean confirmNotifications(int startId, String[] notifyIds) {
return new ConfirmNotifications(startId, notifyIds).runRequest();
}
/**
* Gets the purchase information. This message includes a list of
* notification IDs sent to us by Android Market, which we include in
* our request. The server responds with the purchase information,
* encoded as a JSON string, and sends that to the {@link BillingReceiver}
* in an intent with the action {@link BillingConstants#ACTION_PURCHASE_STATE_CHANGED}.
* Returns false if there was an error trying to connect to the MarketBillingService.
*
* @param startId an identifier for the invocation instance of this service
* @param notifyIds a list of opaque identifiers associated with purchase
* state changes
* @return false if there was an error connecting to Android Market
*/
private boolean getPurchaseInformation(int startId, String[] notifyIds) {
return new GetPurchaseInformation(startId, notifyIds).runRequest();
}
/**
* Verifies that the data was signed with the given signature, and calls
* ResponseHandler.purchaseResponse(android.content.Context, PurchaseState, String, String, long)
* for each verified purchase.
*
* @param startId an identifier for the invocation instance of this service
* @param signedData the signed JSON string (signed, not encrypted)
*/
private void purchaseStateChanged(int startId, String signedData) {
ArrayList<Purchase> purchases;
purchases = Security.parse(signedData);
ArrayList<String> notifyList = new ArrayList<>();
for (Purchase vp : purchases) {
if (vp.notificationId != null) {
notifyList.add(vp.notificationId);
}
Log.d(TAG, "purchase state changed productId: " + vp.productId + ", state: " + vp.purchaseState);
ResponseHandler.purchaseResponse(vp.purchaseState, vp.productId);
}
if (!notifyList.isEmpty()) {
String[] notifyIds = notifyList.toArray(new String[notifyList.size()]);
confirmNotifications(startId, notifyIds);
}
}
private void checkResponseCode(long requestId, ResponseCode responseCode) {
BillingRequest request = sentRequests.get(requestId);
if (request != null) {
Log.d(TAG, request.getClass().getSimpleName() + ": " + responseCode);
request.responseCodeReceived(responseCode);
}
sentRequests.remove(requestId);
}
/**
* Runs any pending requests that are waiting for a connection to the
* service to be established. This runs in the main UI thread.
*/
private void runPendingRequests() {
int maxStartId = -1;
BillingRequest request;
while ((request = pendingRequests.peek()) != null) {
if (request.runIfConnected()) {
// Remove the request
pendingRequests.remove();
// Remember the largest startId, which is the most recent
// request to start this service.
if (maxStartId < request.getStartId()) {
maxStartId = request.getStartId();
}
} else {
// The service crashed, so restart it. Note that this leaves
// the current request on the queue.
bindToMarketBillingService();
return;
}
}
// If we get here then all the requests ran successfully. If maxStartId
// is not -1, then one of the requests started the service, so we can
// stop it now.
if (maxStartId >= 0) {
if (BillingConstants.DEBUG) {
Log.i(TAG, "stopping service, startId: " + maxStartId);
}
stopSelf(maxStartId);
}
}
/**
* This is called when we are connected to the MarketBillingService.
* This runs in the main UI thread.
*/
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Log.d(TAG, "Billing service connected");
marketBillingService = IMarketBillingService.Stub.asInterface(service);
runPendingRequests();
}
@Override
public void onServiceDisconnected(ComponentName name) {
Log.w(TAG, "Billing service disconnected");
marketBillingService = null;
}
public void unbind() {
try {
unbindService(this);
} catch (IllegalArgumentException e) {
// This might happen if the service was disconnected
}
}
public void onStart() {
ResponseHandler.register(purchaseObserver);
}
public void onStop() {
ResponseHandler.unregister();
}
}

@ -1,74 +0,0 @@
// Copyright 2010 Google Inc. All Rights Reserved.
package com.todoroo.astrid.billing;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.Intent;
import android.content.IntentSender;
import android.os.Handler;
import android.util.Log;
import com.todoroo.astrid.billing.BillingConstants.PurchaseState;
import com.todoroo.astrid.billing.BillingConstants.ResponseCode;
import org.tasks.billing.PurchaseHandler;
import java.lang.reflect.Method;
public class PurchaseObserver {
protected static final String TAG = "purchase-observer"; //$NON-NLS-1$
protected final Activity mActivity;
private PurchaseHandler purchaseHandler;
private final Handler mHandler = new Handler();
private Method mStartIntentSender;
private final Object[] mStartIntentSenderArgs = new Object[5];
private static final Class<?>[] START_INTENT_SENDER_SIG = new Class[] {
IntentSender.class, Intent.class, int.class, int.class, int.class
};
public PurchaseObserver(Activity activity, PurchaseHandler purchaseHandler) {
mActivity = activity;
this.purchaseHandler = purchaseHandler;
initCompatibilityLayer();
}
public void onBillingSupported(boolean supported, String type) {
purchaseHandler.onBillingSupported(supported, type);
}
public void onRestoreTransactionsResponse(ResponseCode responseCode) {
purchaseHandler.onRestoreTransactionsResponse(responseCode);
}
private void initCompatibilityLayer() {
try {
mStartIntentSender = mActivity.getClass().getMethod("startIntentSender", //$NON-NLS-1$
START_INTENT_SENDER_SIG);
} catch (SecurityException | NoSuchMethodException e) {
mStartIntentSender = null;
}
}
void startBuyPageActivity(PendingIntent pendingIntent, Intent intent) {
try {
mStartIntentSenderArgs[0] = pendingIntent.getIntentSender();
mStartIntentSenderArgs[1] = intent;
mStartIntentSenderArgs[2] = 0;
mStartIntentSenderArgs[3] = 0;
mStartIntentSenderArgs[4] = 0;
mStartIntentSender.invoke(mActivity, mStartIntentSenderArgs);
} catch (Exception e) {
Log.e(TAG, "error starting activity", e); //$NON-NLS-1$
}
}
void postPurchaseStateChange(final PurchaseState purchaseState, final String itemId) {
mHandler.post(new Runnable() {
@Override
public void run() {
purchaseHandler.onPurchaseStateChange(purchaseState, itemId);
}
});
}
}

@ -1,53 +0,0 @@
package com.todoroo.astrid.billing;
import android.app.PendingIntent;
import android.content.Intent;
import com.todoroo.astrid.billing.BillingConstants.PurchaseState;
import com.todoroo.astrid.billing.BillingConstants.ResponseCode;
public class ResponseHandler {
private static PurchaseObserver sPurchaseObserver;
public static synchronized void register(PurchaseObserver observer) {
sPurchaseObserver = observer;
}
public static synchronized void unregister() {
sPurchaseObserver = null;
}
public static void checkBillingSupportedResponse(boolean supported, String type) {
if (sPurchaseObserver != null) {
sPurchaseObserver.onBillingSupported(supported, type);
}
}
public static void buyPageIntentResponse(PendingIntent pendingIntent, Intent intent) {
if (sPurchaseObserver != null) {
sPurchaseObserver.startBuyPageActivity(pendingIntent, intent);
}
}
public static void purchaseResponse(final PurchaseState purchaseState, final String productId) {
new Thread(new Runnable() {
@Override
public void run() {
// This needs to be synchronized because the UI thread can change the
// value of sPurchaseObserver.
synchronized (ResponseHandler.class) {
if (sPurchaseObserver != null) {
sPurchaseObserver.postPurchaseStateChange(purchaseState, productId);
}
}
}
}).start();
}
public static void responseCodeReceived(ResponseCode responseCode) {
if (sPurchaseObserver != null) {
sPurchaseObserver.onRestoreTransactionsResponse(responseCode);
}
}
}

@ -1,45 +0,0 @@
package com.todoroo.astrid.billing;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.todoroo.astrid.billing.BillingConstants.PurchaseState;
import java.security.SecureRandom;
import java.util.ArrayList;
public class Security {
private static final SecureRandom RANDOM = new SecureRandom();
public static class Purchase {
public PurchaseState purchaseState;
public String notificationId;
public String productId;
public Purchase(PurchaseState purchaseState, String notificationId, String productId) {
this.purchaseState = purchaseState;
this.notificationId = notificationId;
this.productId = productId;
}
}
public static long generateNonce() {
return RANDOM.nextLong();
}
public static ArrayList<Purchase> parse(String signedData) {
ArrayList<Purchase> purchases = new ArrayList<>();
JsonElement jsonElement = new JsonParser().parse(signedData);
JsonObject jsonObject = jsonElement.getAsJsonObject();
JsonArray orders = jsonObject.getAsJsonArray("orders");
for (JsonElement orderElement : orders) {
JsonObject orderObject = orderElement.getAsJsonObject();
purchases.add(new Purchase(
PurchaseState.valueOf(orderObject.get("purchaseState").getAsInt()),
orderObject.has("notificationId") ? orderObject.get("notificationId").getAsString() : null,
orderObject.get("productId").getAsString()));
}
return purchases;
}
}

@ -1,68 +0,0 @@
package org.tasks.billing;
import com.todoroo.andlib.utility.Preferences;
import com.todoroo.astrid.actfm.sync.ActFmPreferenceService;
import com.todoroo.astrid.billing.BillingConstants;
import com.todoroo.astrid.billing.BillingConstants.PurchaseState;
import com.todoroo.astrid.billing.BillingConstants.ResponseCode;
import com.todoroo.astrid.billing.BillingService;
import static com.todoroo.andlib.utility.Preferences.getBoolean;
import static com.todoroo.andlib.utility.Preferences.getInt;
import static com.todoroo.andlib.utility.Preferences.getStringValue;
public class PurchaseHandler {
private static final String PREF_PRODUCT_ID = ActFmPreferenceService.IDENTIFIER + "_inapp_product_id";
private static final String PREF_PURCHASE_STATE = ActFmPreferenceService.IDENTIFIER + "_inapp_purchase_state";
private static final String PREF_TRANSACTIONS_INITIALIZED = "premium_transactions_initialized"; //$NON-NLS-1$
private boolean billingSupported;
private boolean userDonated;
private BillingService billingService;
public PurchaseHandler(BillingService billingService) {
this.billingService = billingService;
updateDonationStatus();
}
public boolean isBillingSupported() {
return billingSupported;
}
public boolean userDonated() {
return userDonated;
}
private void updateDonationStatus() {
userDonated = BillingConstants.TASKS_DONATION_ITEM_ID.equals(getStringValue(PREF_PRODUCT_ID)) &&
getInt(PREF_PURCHASE_STATE, -1) == PurchaseState.PURCHASED.ordinal();
}
public void onBillingSupported(boolean supported, String type) {
if (BillingConstants.ITEM_TYPE_INAPP.equals(type)) {
billingSupported = supported;
if (supported && !restoredTransactions()) {
billingService.restoreTransactions();
}
}
}
public void onPurchaseStateChange(PurchaseState purchaseState, final String itemId) {
if (BillingConstants.TASKS_DONATION_ITEM_ID.equals(itemId)) {
Preferences.setString(PREF_PRODUCT_ID, itemId);
Preferences.setInt(PREF_PURCHASE_STATE, purchaseState.ordinal());
updateDonationStatus();
}
}
public void onRestoreTransactionsResponse(ResponseCode responseCode) {
if (responseCode == ResponseCode.RESULT_OK) {
Preferences.setBoolean(PREF_TRANSACTIONS_INITIALIZED, true);
}
}
boolean restoredTransactions() {
return getBoolean(PREF_TRANSACTIONS_INITIALIZED, false);
}
}

@ -39,11 +39,6 @@
android:id="@+id/menu_new_filter"
android:title="@string/FLA_new_filter"
tasks:showAsAction="never" />
<item
android:id="@+id/menu_donate"
android:title="@string/TLA_menu_donate"
android:visible="false"
tasks:showAsAction="never" />
<item
android:id="@+id/menu_settings"
android:title="@string/TLA_menu_settings"

@ -3,5 +3,4 @@
<string name="EPr_use_dark_theme">Dark theme</string>
<string name="EPr_use_dark_theme_widget">Dark widget theme</string>
<string name="delete_task">Delete task</string>
<string name="TLA_menu_donate">Donate</string>
</resources>

@ -1,119 +0,0 @@
package org.tasks.billing;
import com.todoroo.astrid.billing.BillingConstants;
import com.todoroo.astrid.billing.BillingService;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.tasks.TestUtilities.resetPreferences;
@Ignore("Throws mockito exception on Travis for some reason")
@RunWith(RobolectricTestRunner.class)
public class PurchaseHandlerTest {
BillingService billingService;
PurchaseHandler purchaseHandler;
@Before
public void before() {
resetPreferences();
billingService = mock(BillingService.class);
purchaseHandler = new PurchaseHandler(billingService);
}
@After
public void after() {
verifyNoMoreInteractions(billingService);
}
@Test
public void userHasNotDonatedByDefault() {
assertFalse(purchaseHandler.userDonated());
}
@Test
public void billingNotSupportedByDefault() {
assertFalse(purchaseHandler.isBillingSupported());
}
@Test
public void haveNotRestoredTransactionsByDefault() {
assertFalse(purchaseHandler.restoredTransactions());
}
@Test
public void restoreTransactions() {
purchaseHandler.onBillingSupported(true, BillingConstants.ITEM_TYPE_INAPP);
verify(billingService).restoreTransactions();
}
@Test
public void dontRestoreWhenBillingNotSupported() {
purchaseHandler.onBillingSupported(false, BillingConstants.ITEM_TYPE_INAPP);
}
@Test
public void dontRestoreWhenAlreadyRestored() {
purchaseHandler.onRestoreTransactionsResponse(BillingConstants.ResponseCode.RESULT_OK);
purchaseHandler.onBillingSupported(true, BillingConstants.ITEM_TYPE_INAPP);
}
@Test
public void ignoreSubscriptions() {
purchaseHandler.onBillingSupported(true, BillingConstants.ITEM_TYPE_SUBSCRIPTION);
}
@Test
public void userDonated() {
purchaseHandler.onPurchaseStateChange(BillingConstants.PurchaseState.PURCHASED, BillingConstants.TASKS_DONATION_ITEM_ID);
assertTrue(purchaseHandler.userDonated());
}
@Test
public void ignoreFailedTransaction() {
purchaseHandler.onPurchaseStateChange(BillingConstants.PurchaseState.CANCELED, BillingConstants.TASKS_DONATION_ITEM_ID);
assertFalse(purchaseHandler.userDonated());
}
@Test
public void ignoreOldItems() {
purchaseHandler.onPurchaseStateChange(BillingConstants.PurchaseState.PURCHASED, "some old purchase");
assertFalse(purchaseHandler.userDonated());
}
@Test
public void oldItemsDontReplaceLatest() {
purchaseHandler.onPurchaseStateChange(BillingConstants.PurchaseState.PURCHASED, BillingConstants.TASKS_DONATION_ITEM_ID);
purchaseHandler.onPurchaseStateChange(BillingConstants.PurchaseState.PURCHASED, "some old purchase");
assertTrue(purchaseHandler.userDonated());
}
@Test
public void restoredTransactions() {
purchaseHandler.onRestoreTransactionsResponse(BillingConstants.ResponseCode.RESULT_OK);
assertTrue(purchaseHandler.restoredTransactions());
}
@Test
public void restoreTransactionsFailed() {
purchaseHandler.onRestoreTransactionsResponse(BillingConstants.ResponseCode.RESULT_DEVELOPER_ERROR);
assertFalse(purchaseHandler.restoredTransactions());
}
}
Loading…
Cancel
Save