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;