diff --git a/astrid/res/values/strings-premium.xml b/astrid/res/values/strings-premium.xml
index 89f15f1c4..a4f030a74 100644
--- a/astrid/res/values/strings-premium.xml
+++ b/astrid/res/values/strings-premium.xml
@@ -46,9 +46,21 @@
Sorry, the system does not yet support this type of file
- Subscriptions not supported
- Sorry! The Market billing
+ Can\'t make purchases
+ The Market billing
+ service is not available at this time. You can continue to use this app but you
+ won\'t be able to make purchases.
+ Can\'t purchase subscriptions
+ The Market billing
service on this device does not support subscriptions at this time.
+ Can\'t connect to Market
+ This app cannot connect to Market.
+ Your version of Market may be out of date.
+ You can continue to use this app but you
+ won\'t be able to make purchases.
+
+ Restoring transactions
+
Learn more
http://market.android.com/support/bin/answer.py?answer=1050566&hl=%lang%&dl=%region%
diff --git a/astrid/src/com/todoroo/astrid/billing/BillingActivity.java b/astrid/src/com/todoroo/astrid/billing/BillingActivity.java
index 7b69811e3..c22b1bd7e 100644
--- a/astrid/src/com/todoroo/astrid/billing/BillingActivity.java
+++ b/astrid/src/com/todoroo/astrid/billing/BillingActivity.java
@@ -4,19 +4,40 @@ import java.util.Locale;
import android.app.Activity;
import android.app.AlertDialog;
+import android.app.Dialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
+import android.os.Handler;
+import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
+import android.widget.Toast;
import com.timsu.astrid.R;
+import com.todoroo.andlib.utility.Preferences;
+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 com.todoroo.astrid.utility.Constants;
public class BillingActivity extends Activity {
+ private static final int DIALOG_CANNOT_CONNECT_ID = 1;
+ private static final int DIALOG_BILLING_NOT_SUPPORTED_ID = 2;
+ private static final int DIALOG_SUBSCRIPTIONS_NOT_SUPPORTED_ID = 3;
+
+ private static final String TRANSACTIONS_INITIALIZED = "premium_transactions_initialized"; //$NON-NLS-1$
+
+ private Handler handler;
private BillingService billingService;
+ private AstridPurchaseObserver purchaseObserver;
+ private Button buyMonth;
+ private Button buyYear;
+
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -25,17 +46,21 @@ public class BillingActivity extends Activity {
setupButtons();
+ handler = new Handler();
billingService = new BillingService();
billingService.setContext(this);
+ purchaseObserver = new AstridPurchaseObserver(handler);
+
+ ResponseHandler.register(purchaseObserver);
if (!billingService.checkBillingSupported(BillingConstants.ITEM_TYPE_SUBSCRIPTION)) {
- showSubscriptionsNotSupported();
+ showDialog(DIALOG_SUBSCRIPTIONS_NOT_SUPPORTED_ID);
}
}
private void setupButtons() {
- Button buyMonth = (Button) findViewById(R.id.buy_month);
- Button buyYear = (Button) findViewById(R.id.buy_year);
+ buyMonth = (Button) findViewById(R.id.buy_month);
+ buyYear = (Button) findViewById(R.id.buy_year);
//TODO: Figure out if we need a payload for any reason
@@ -44,7 +69,7 @@ public class BillingActivity extends Activity {
public void onClick(View v) {
if (!billingService.requestPurchase(BillingConstants.PRODUCT_ID_MONTHLY,
BillingConstants.ITEM_TYPE_SUBSCRIPTION, null)) {
- showSubscriptionsNotSupported();
+ showDialog(DIALOG_SUBSCRIPTIONS_NOT_SUPPORTED_ID);
}
}
});
@@ -54,30 +79,12 @@ public class BillingActivity extends Activity {
public void onClick(View v) {
if (!billingService.requestPurchase(BillingConstants.PRODUCT_ID_YEARLY,
BillingConstants.ITEM_TYPE_SUBSCRIPTION, null)) {
- showSubscriptionsNotSupported();
+ showDialog(DIALOG_SUBSCRIPTIONS_NOT_SUPPORTED_ID);
}
}
});
}
- private void showSubscriptionsNotSupported() {
- String helpUrl = replaceLanguageAndRegion(getString(R.string.subscriptions_help_url));
- final Uri helpUri = Uri.parse(helpUrl);
-
- new AlertDialog.Builder(this)
- .setTitle(R.string.subscriptions_not_supported)
- .setMessage(R.string.subscriptions_not_supported_message)
- .setCancelable(false)
- .setPositiveButton(R.string.DLG_ok, null)
- .setNegativeButton(R.string.subscriptions_learn_more, new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- Intent intent = new Intent(Intent.ACTION_VIEW, helpUri);
- startActivity(intent);
- }
- }).create().show();
- }
-
/**
* Replaces the language and/or country of the device into the given string.
* The pattern "%lang%" will be replaced by the device's language code and
@@ -96,4 +103,148 @@ public class BillingActivity extends Activity {
}
return str;
}
+
+ @Override
+ protected Dialog onCreateDialog(int id) {
+ switch (id) {
+ case DIALOG_CANNOT_CONNECT_ID:
+ return createDialog(R.string.cannot_connect_title,
+ R.string.cannot_connect_message);
+ case DIALOG_BILLING_NOT_SUPPORTED_ID:
+ return createDialog(R.string.billing_not_supported_title,
+ R.string.billing_not_supported_message);
+ case DIALOG_SUBSCRIPTIONS_NOT_SUPPORTED_ID:
+ return createDialog(R.string.subscriptions_not_supported_title,
+ R.string.subscriptions_not_supported_message);
+ default:
+ return null;
+ }
+ }
+
+ private Dialog createDialog(int titleId, int messageId) {
+ String helpUrl = replaceLanguageAndRegion(getString(R.string.subscriptions_help_url));
+ if (Constants.DEBUG) {
+ Log.i("billing-activity-url", helpUrl); //$NON-NLS-1$
+ }
+ final Uri helpUri = Uri.parse(helpUrl);
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(titleId)
+ .setIcon(android.R.drawable.stat_sys_warning)
+ .setMessage(messageId)
+ .setCancelable(false)
+ .setPositiveButton(android.R.string.ok, null)
+ .setNegativeButton(R.string.subscriptions_learn_more, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ Intent intent = new Intent(Intent.ACTION_VIEW, helpUri);
+ startActivity(intent);
+ }
+ });
+ return builder.create();
+ }
+
+ private void restoreTransactions() {
+ boolean initialized = Preferences.getBoolean(TRANSACTIONS_INITIALIZED, false);
+ if (!initialized) {
+ billingService.restoreTransactions();
+ Toast.makeText(this, R.string.restoring_transactions, Toast.LENGTH_LONG).show();
+ }
+ }
+
+ /**
+ * A {@link PurchaseObserver} is used to get callbacks when Android Market sends
+ * messages to this application so that we can update the UI.
+ */
+ @SuppressWarnings("nls")
+ private class AstridPurchaseObserver extends PurchaseObserver {
+ public AstridPurchaseObserver(Handler handler) {
+ super(BillingActivity.this, handler);
+ }
+
+ @Override
+ public void onBillingSupported(boolean supported, String type) {
+ if (Constants.DEBUG) {
+ Log.i(TAG, "supported: " + supported);
+ }
+ if (type != null && type.equals(BillingConstants.ITEM_TYPE_SUBSCRIPTION)) {
+ if (supported) {
+ restoreTransactions();
+ buyMonth.setEnabled(true);
+ buyYear.setEnabled(true);
+ } else {
+ showDialog(DIALOG_BILLING_NOT_SUPPORTED_ID);
+ }
+ } else {
+ showDialog(DIALOG_SUBSCRIPTIONS_NOT_SUPPORTED_ID);
+ }
+ }
+
+ @Override
+ public void onPurchaseStateChange(PurchaseState purchaseState, String itemId,
+ int quantity, long purchaseTime, String developerPayload) {
+ if (Constants.DEBUG) {
+ Log.i(TAG, "onPurchaseStateChange() itemId: " + itemId + " " + purchaseState);
+ }
+
+ if (developerPayload == null) {
+ //
+ } else {
+ //
+ }
+
+ if (purchaseState == PurchaseState.PURCHASED) {
+// mOwnedItems.add(itemId);
+//
+// // If this is a subscription, then enable the "Edit
+// // Subscriptions" button.
+// for (CatalogEntry e : CATALOG) {
+// if (e.sku.equals(itemId) &&
+// e.managed.equals(Managed.SUBSCRIPTION)) {
+// mEditSubscriptionsButton.setVisibility(View.VISIBLE);
+// }
+// }
+ }
+// mCatalogAdapter.setOwnedItems(mOwnedItems);
+// mOwnedItemsCursor.requery();
+ }
+
+ @Override
+ public void onRequestPurchaseResponse(RequestPurchase request,
+ ResponseCode responseCode) {
+ if (Constants.DEBUG) {
+ Log.d(TAG, request.mProductId + ": " + responseCode);
+ }
+ if (responseCode == ResponseCode.RESULT_OK) {
+ if (Constants.DEBUG) {
+ Log.i(TAG, "purchase was successfully sent to server");
+ }
+ } else if (responseCode == ResponseCode.RESULT_USER_CANCELED) {
+ if (Constants.DEBUG) {
+ Log.i(TAG, "user canceled purchase");
+ }
+ } else {
+ if (Constants.DEBUG) {
+ Log.i(TAG, "purchase failed");
+ }
+ }
+ }
+
+ @Override
+ public void onRestoreTransactionsResponse(RestoreTransactions request,
+ ResponseCode responseCode) {
+ if (responseCode == ResponseCode.RESULT_OK) {
+ if (Constants.DEBUG) {
+ Log.d(TAG, "completed RestoreTransactions request");
+ }
+ // Update the shared preferences so that we don't perform
+ // a RestoreTransactions again.
+ Preferences.setBoolean(TRANSACTIONS_INITIALIZED, true);
+ } else {
+ if (Constants.DEBUG) {
+ Log.d(TAG, "RestoreTransactions error: " + responseCode);
+ }
+ }
+ }
+ }
}
diff --git a/astrid/src/com/todoroo/astrid/billing/BillingReceiver.java b/astrid/src/com/todoroo/astrid/billing/BillingReceiver.java
index 1708989da..fec87f70c 100644
--- a/astrid/src/com/todoroo/astrid/billing/BillingReceiver.java
+++ b/astrid/src/com/todoroo/astrid/billing/BillingReceiver.java
@@ -3,13 +3,92 @@ 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;
+import com.todoroo.astrid.utility.Constants;
public class BillingReceiver extends BroadcastReceiver {
+ private static final String TAG = "billing-receiver"; //$NON-NLS-1$
+ /**
+ * This is the entry point for all asynchronous messages sent from Android Market to
+ * the application. This method forwards the messages on to the
+ * {@link BillingService}, which handles the communication back to Android Market.
+ * The {@link BillingService} also reports state changes back to the application through
+ * the {@link ResponseHandler}.
+ */
@Override
public void onReceive(Context context, Intent intent) {
- // TODO Auto-generated method stub
+ String action = intent.getAction();
+ if (BillingConstants.ACTION_PURCHASE_STATE_CHANGED.equals(action)) {
+ String signedData = intent.getStringExtra(BillingConstants.INAPP_SIGNED_DATA);
+ String signature = intent.getStringExtra(BillingConstants.INAPP_SIGNATURE);
+ purchaseStateChanged(context, signedData, signature);
+ } else if (BillingConstants.ACTION_NOTIFY.equals(action)) {
+ String notifyId = intent.getStringExtra(BillingConstants.NOTIFICATION_ID);
+ if (Constants.DEBUG) {
+ Log.i(TAG, "notifyId: " + notifyId); //$NON-NLS-1$
+ }
+ notify(context, notifyId);
+ } else if (BillingConstants.ACTION_RESPONSE_CODE.equals(action)) {
+ 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);
+ } else {
+ Log.w(TAG, "unexpected action: " + action); //$NON-NLS-1$
+ }
+ }
+ /**
+ * This is called when Android Market sends information about a purchase state
+ * change. The signedData parameter is a plaintext JSON string that is
+ * signed by the server with the developer's private key. The signature
+ * for the signed data is passed in the signature parameter.
+ * @param context the context
+ * @param signedData the (unencrypted) JSON string
+ * @param signature the signature for the signedData
+ */
+ 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);
}
+ /**
+ * This is called when Android Market sends a "notify" message indicating that transaction
+ * information is available. The request includes a nonce (random number used once) that
+ * we generate and Android Market signs and sends back to us with the purchase state and
+ * other transaction details. This BroadcastReceiver cannot bind to the
+ * MarketBillingService directly so it starts the {@link BillingService}, which does the
+ * actual work of sending the message.
+ *
+ * @param context the context
+ * @param notifyId the notification ID
+ */
+ 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);
+ }
+
+ /**
+ * This is called when Android Market sends a server response code. The BillingService can
+ * then report the status of the response if desired.
+ *
+ * @param context the context
+ * @param requestId the request ID that corresponds to a previous request
+ * @param responseCodeIndex the ResponseCode ordinal value for the request
+ */
+ 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);
+ }
}
diff --git a/astrid/src/com/todoroo/astrid/billing/PurchaseObserver.java b/astrid/src/com/todoroo/astrid/billing/PurchaseObserver.java
index 2e11d1809..316aba62c 100644
--- a/astrid/src/com/todoroo/astrid/billing/PurchaseObserver.java
+++ b/astrid/src/com/todoroo/astrid/billing/PurchaseObserver.java
@@ -27,7 +27,7 @@ import com.todoroo.astrid.billing.BillingService.RestoreTransactions;
* are used to update the UI.
*/
public abstract class PurchaseObserver {
- private static final String TAG = "purchase-observer"; //$NON-NLS-1$
+ protected static final String TAG = "purchase-observer"; //$NON-NLS-1$
private final Activity mActivity;
private final Handler mHandler;
private Method mStartIntentSender;