mirror of https://github.com/tasks/tasks
Add donate menu item
parent
d39a6a1868
commit
86b318a6b8
@ -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);
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue