// Copyright 2010 Google Inc. All Rights Reserved.
package com.godsandtowers.billing.google;
import java.lang.reflect.Method;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.PendingIntent;
import android.app.PendingIntent.CanceledException;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
import android.os.Handler;
import android.text.Html;
import android.text.SpannableStringBuilder;
import android.util.Log;
import com.gundogstudios.modules.Modules;
import com.godsandtowers.R;
import com.godsandtowers.billing.google.BillingService.RequestPurchase;
import com.godsandtowers.billing.google.BillingService.RestoreTransactions;
import com.godsandtowers.billing.google.Consts.PurchaseState;
import com.godsandtowers.billing.google.Consts.ResponseCode;
/**
* An interface for observing changes related to purchases. The main application extends this class and registers an
* instance of that derived class with {@link ResponseHandler}. The main application implements the callbacks
* {@link #onBillingSupported(boolean)} and {@link #onPurchaseStateChange(PurchaseState, String, int, long)}. These
* methods are used to update the UI.
*/
public class PurchaseObserver {
private static final String TAG = "PurchaseObserver";
protected final Activity mActivity;
private final Handler mHandler;
private Method mStartIntentSender;
private Object[] mStartIntentSenderArgs = new Object[5];
public PurchaseObserver(Activity activity, Handler handler) {
mActivity = activity;
mHandler = handler;
initCompatibilityLayer();
}
/**
* This is the callback that is invoked when Android Market responds to the
* {@link BillingService#checkBillingSupported()} request.
*
* @param supported
* true if in-app billing is supported.
*/
public void onBillingSupported(boolean supported) {
if (Consts.DEBUG) {
Modules.LOG.info(TAG, "supported: " + supported);
}
if (!supported) {
createDialog(mActivity, R.string.purchase_billing_not_supported_title,
R.string.purchase_billing_not_supported_message);
}
}
/**
* This is the callback that is invoked when an item is purchased, refunded, or canceled. It is the callback invoked
* in response to calling {@link BillingService#requestPurchase(String)}. It may also be invoked asynchronously when
* a purchase is made on another device (if the purchase was for a Market-managed item), or if the purchase was
* refunded, or the charge was canceled. This handles the UI update. The database update is handled in
* {@link ResponseHandler#purchaseResponse(Context, PurchaseState, String, String, long)}.
*
* @param purchaseState
* the purchase state of the item
* @param itemId
* a string identifying the item (the "SKU")
* @param quantity
* the current quantity of this item after the purchase
* @param purchaseTime
* the time the product was purchased, in milliseconds since the epoch (Jan 1, 1970)
*/
public void onPurchaseStateChange(PurchaseState purchaseState, String itemId, int quantity, long purchaseTime,
String developerPayload) {
if (Consts.DEBUG) {
Modules.LOG.info(TAG, "onPurchaseStateChange() itemId: " + itemId + " " + purchaseState);
}
if (developerPayload == null) {
logProductActivity(itemId, purchaseState.toString());
} else {
logProductActivity(itemId, purchaseState + "\n\t" + developerPayload);
}
if (purchaseState == PurchaseState.PURCHASED) {
Modules.PURCHASER.purchased(itemId, developerPayload);
}
}
/**
* 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
* {@link #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 void onRequestPurchaseResponse(RequestPurchase request, ResponseCode responseCode) {
if (Consts.DEBUG) {
Log.d(TAG, request.mProductId + ": " + responseCode);
}
if (responseCode == ResponseCode.RESULT_OK) {
if (Consts.DEBUG) {
Modules.LOG.info(TAG, "purchase was successfully sent to server");
}
logProductActivity(request.mProductId, "sending purchase request");
} else if (responseCode == ResponseCode.RESULT_USER_CANCELED) {
if (Consts.DEBUG) {
Modules.LOG.info(TAG, "user canceled purchase");
}
logProductActivity(request.mProductId, "dismissed purchase dialog");
} else {
if (Consts.DEBUG) {
Modules.LOG.info(TAG, "purchase failed");
}
logProductActivity(request.mProductId, "request purchase returned " + 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 void onRestoreTransactionsResponse(RestoreTransactions request, ResponseCode responseCode) {
if (responseCode == ResponseCode.RESULT_OK) {
if (Consts.DEBUG) {
Log.d(TAG, "completed RestoreTransactions request");
}
} else {
if (Consts.DEBUG) {
Log.d(TAG, "RestoreTransactions error: " + responseCode);
}
}
}
private void initCompatibilityLayer() {
try {
mStartIntentSender = mActivity.getClass().getMethod("startIntentSender",
new Class[] { IntentSender.class, Intent.class, int.class, int.class, int.class });
} catch (SecurityException e) {
mStartIntentSender = null;
} catch (NoSuchMethodException e) {
mStartIntentSender = null;
}
}
void startBuyPageActivity(PendingIntent pendingIntent, Intent intent) {
if (mStartIntentSender != null) {
// This is on Android 2.0 and beyond. The in-app buy page activity
// must be on the activity stack of the application.
try {
// This implements the method call:
// mActivity.startIntentSender(pendingIntent.getIntentSender(),
// intent, 0, 0, 0);
mStartIntentSenderArgs[0] = pendingIntent.getIntentSender();
mStartIntentSenderArgs[1] = intent;
mStartIntentSenderArgs[2] = Integer.valueOf(0);
mStartIntentSenderArgs[3] = Integer.valueOf(0);
mStartIntentSenderArgs[4] = Integer.valueOf(0);
mStartIntentSender.invoke(mActivity, mStartIntentSenderArgs);
} catch (Exception e) {
Modules.LOG.error(TAG, "error buy page starting activity");
}
} else {
// This is on Android version 1.6. The in-app buy page activity must be on its
// own separate activity stack instead of on the activity stack of
// the application.
try {
pendingIntent.send(mActivity, 0 /* code */, intent);
} catch (CanceledException e) {
Modules.LOG.error(TAG, "error buy page starting activity");
}
}
}
/**
* Updates the UI after the database has been updated. This method runs in a background thread so it has to post a
* Runnable to run on the UI thread.
*
* @param purchaseState
* the purchase state of the item
* @param itemId
* a string identifying the item
* @param quantity
* the quantity of items in this purchase
*/
void postPurchaseStateChange(final PurchaseState purchaseState, final String itemId, final int quantity,
final long purchaseTime, final String developerPayload) {
mHandler.post(new Runnable() {
public void run() {
onPurchaseStateChange(purchaseState, itemId, quantity, purchaseTime, developerPayload);
}
});
}
private Dialog createDialog(Activity activity, int titleId, int messageId) {
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setTitle(titleId).setIcon(android.R.drawable.stat_sys_warning).setMessage(messageId)
.setCancelable(false).setPositiveButton(android.R.string.ok, null);
return builder.create();
}
private void logProductActivity(String product, String activity) {
SpannableStringBuilder contents = new SpannableStringBuilder();
contents.append(Html.fromHtml("<b>" + product + "</b>: "));
contents.append(activity);
contents.append('\n');
}
}