Add donate menu item

pull/73/head
Alex Baker 11 years ago
parent d39a6a1868
commit 86b318a6b8

@ -41,6 +41,7 @@
-keep public class * extends android.content.ContentProvider
-keep public class * extends android.app.backup.BackupAgentHelper
-keep public class * extends android.preference.Preference
-keep public class com.android.vending.licensing.ILicensingService
-keepclassmembers public class com.todoroo.astrid.data.* {
*;

@ -6,7 +6,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.tasks"
android:versionName="4.6.11"
android:versionCode="324">
android:versionCode="325">
<!-- widgets, alarms, and services will break if Astrid is installed on SD card -->
<!-- android:installLocation="internalOnly"> -->
@ -94,6 +94,18 @@
android:hardwareAccelerated="false"
android:manageSpaceActivity="com.todoroo.astrid.core.OldTaskPreferences">
<!-- ========================================================= Billing = -->
<service android:name="com.todoroo.astrid.billing.BillingService" />
<receiver android:name="com.todoroo.astrid.billing.BillingReceiver">
<intent-filter>
<action android:name="com.android.vending.billing.IN_APP_NOTIFY" />
<action android:name="com.android.vending.billing.RESPONSE_CODE" />
<action android:name="com.android.vending.billing.PURCHASE_STATE_CHANGED" />
</intent-filter>
</receiver>
<!-- ====================================================== Activities = -->
<!-- Activity that displays task list -->

@ -0,0 +1,24 @@
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.vending.billing;
import android.os.Bundle;
interface IMarketBillingService {
/** Given the arguments in bundle form, returns a bundle for results. */
Bundle sendBillingRequest(in Bundle bundle);
}

@ -30,6 +30,8 @@ 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;
@ -73,6 +75,7 @@ public class TaskListActivity extends AstridActivity implements OnPageChangeList
private int filterMode;
private FilterModeSpec filterModeSpec;
private BillingService billingService;
/**
* @see android.app.Activity#onCreate(Bundle)
@ -137,6 +140,9 @@ public class TaskListActivity extends AstridActivity implements OnPageChangeList
if (getIntent().hasExtra(TOKEN_SOURCE)) {
trackActivitySource();
}
billingService = new BillingService();
billingService.setActivity(this);
}
@Override
@ -313,6 +319,7 @@ 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);
}
@ -539,6 +546,9 @@ 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);
}
@ -573,4 +583,28 @@ 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();
}
}

@ -0,0 +1,72 @@
package com.todoroo.astrid.billing;
import android.app.Activity;
import android.os.Handler;
import android.util.Log;
import com.todoroo.andlib.utility.Preferences;
import com.todoroo.astrid.actfm.sync.ActFmPreferenceService;
import com.todoroo.astrid.billing.BillingConstants.PurchaseState;
import com.todoroo.astrid.billing.BillingConstants.ResponseCode;
import com.todoroo.astrid.billing.BillingService.RequestPurchase;
import com.todoroo.astrid.billing.BillingService.RestoreTransactions;
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 AstridPurchaseObserver extends PurchaseObserver {
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 BillingService billingService;
public AstridPurchaseObserver(Activity activity, BillingService billingService) {
super(activity, new Handler());
this.billingService = billingService;
}
public boolean isBillingSupported() {
return billingSupported;
}
public boolean userDonated() {
return BillingConstants.TASKS_DONATION_ITEM_ID.equals(getStringValue(PREF_PRODUCT_ID)) &&
getInt(PREF_PURCHASE_STATE, -1) == PurchaseState.PURCHASED.ordinal();
}
@Override
public void onBillingSupported(boolean supported, String type) {
Log.d(TAG, "onBillingSupported(" + supported + ", " + type + ")");
if (BillingConstants.ITEM_TYPE_INAPP.equals(type)) {
billingSupported = supported;
if (supported && !getBoolean(PREF_TRANSACTIONS_INITIALIZED, false)) {
billingService.restoreTransactions();
}
}
}
@Override
public void onPurchaseStateChange(PurchaseState purchaseState, final String itemId) {
Log.d(TAG, "onPurchaseStateChange(" + purchaseState + ", " + itemId + ")");
if (BillingConstants.TASKS_DONATION_ITEM_ID.equals(itemId)) {
Preferences.setString(PREF_PRODUCT_ID, itemId);
Preferences.setInt(PREF_PURCHASE_STATE, purchaseState.ordinal());
}
}
@Override
public void onRequestPurchaseResponse(RequestPurchase request, ResponseCode responseCode) {
Log.d(TAG, "onRequestPurchaseResponse(" + request + ", " + responseCode + ")");
}
@Override
public void onRestoreTransactionsResponse(RestoreTransactions request, ResponseCode responseCode) {
Log.d(TAG, "onRestoreTransactionsResponse(" + request + ", " + responseCode + ")");
if (responseCode == ResponseCode.RESULT_OK) {
Preferences.setBoolean(PREF_TRANSACTIONS_INITIALIZED, true);
}
}
}

@ -0,0 +1,87 @@
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 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];
}
}
}

@ -0,0 +1,61 @@
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);
}
}

@ -0,0 +1,534 @@
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 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 AstridPurchaseObserver purchaseObserver;
public boolean showDonateOption() {
return purchaseObserver.isBillingSupported() && !purchaseObserver.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());
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) {
ResponseHandler.responseCodeReceived(this, responseCode);
}
}
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) {
ResponseHandler.responseCodeReceived(this, responseCode);
}
}
public void setActivity(Activity activity) {
attachBaseContext(activity);
purchaseObserver = new AstridPurchaseObserver(activity, this);
}
@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 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);
}
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();
}
}

@ -0,0 +1,102 @@
// 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 com.todoroo.astrid.billing.BillingService.RequestPurchase;
import com.todoroo.astrid.billing.BillingService.RestoreTransactions;
import java.lang.reflect.Method;
public abstract class PurchaseObserver {
protected static final String TAG = "purchase-observer"; //$NON-NLS-1$
protected final Activity mActivity;
private final Handler mHandler;
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, Handler handler) {
mActivity = activity;
mHandler = handler;
initCompatibilityLayer();
}
public abstract void onBillingSupported(boolean supported, String type);
public abstract void onPurchaseStateChange(PurchaseState purchaseState, String itemId);
/**
* This is called when we receive a response code from Market for a
* RequestPurchase request that we made. This is NOT used for any
* purchase state changes. All purchase state changes are received in
* onPurchaseStateChange(PurchaseState, String, int, long).
* This is used for reporting various errors, or if the user backed out
* and didn't purchase the item. The possible response codes are:
* RESULT_OK means that the order was sent successfully to the server.
* The onPurchaseStateChange() will be invoked later (with a
* purchase state of PURCHASED or CANCELED) when the order is
* charged or canceled. This response code can also happen if an
* order for a Market-managed item was already sent to the server.
* RESULT_USER_CANCELED means that the user didn't buy the item.
* RESULT_SERVICE_UNAVAILABLE means that we couldn't connect to the
* Android Market server (for example if the data connection is down).
* RESULT_BILLING_UNAVAILABLE means that in-app billing is not
* supported yet.
* RESULT_ITEM_UNAVAILABLE means that the item this app offered for
* sale does not exist (or is not published) in the server-side
* catalog.
* RESULT_ERROR is used for any other errors (such as a server error).
*/
public abstract void onRequestPurchaseResponse(RequestPurchase request,
ResponseCode responseCode);
/**
* This is called when we receive a response code from Android Market for a
* RestoreTransactions request that we made. A response code of
* RESULT_OK means that the request was successfully sent to the server.
*/
public abstract void onRestoreTransactionsResponse(RestoreTransactions request,
ResponseCode 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() {
onPurchaseStateChange(purchaseState, itemId);
}
});
}
}

@ -0,0 +1,61 @@
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;
import com.todoroo.astrid.billing.BillingService.RequestPurchase;
import com.todoroo.astrid.billing.BillingService.RestoreTransactions;
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(RequestPurchase request, ResponseCode responseCode) {
if (sPurchaseObserver != null) {
sPurchaseObserver.onRequestPurchaseResponse(request, responseCode);
}
}
public static void responseCodeReceived(RestoreTransactions request, ResponseCode responseCode) {
if (sPurchaseObserver != null) {
sPurchaseObserver.onRestoreTransactionsResponse(request, responseCode);
}
}
}

@ -0,0 +1,45 @@
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;
}
}

@ -39,6 +39,11 @@
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,4 +3,5 @@
<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>
Loading…
Cancel
Save