Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Non-backwards compatible changes. Closes #52, closes #90. #92

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import android.app.PendingIntent.CanceledException;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.text.TextUtils;
import android.util.Log;

Expand All @@ -60,9 +61,12 @@ public interface IConfiguration {

/**
* Returns the public key used to verify the signature of responses of
* the Market Billing service.
* the Google Play Billing service. If you are using a custom signature
* validator with server-side validation this method might not be needed
* and can return null.
*
* @return Base64 encoded public key.
* @see BillingController#setSignatureValidator(ISignatureValidator)
*/
public String getPublicKey();
}
Expand Down Expand Up @@ -296,15 +300,15 @@ public static boolean isPurchased(Context context, String itemId) {
* @param state
* new purchase state of the item.
*/
private static void notifyPurchaseStateChange(String itemId, Transaction.PurchaseState state) {
private static void notifyPurchaseStateChange(String itemId, Transaction.PurchaseState state, String orderId) {
for (IBillingObserver o : observers) {
o.onPurchaseStateChanged(itemId, state);
o.onPurchaseStateChanged(itemId, state, orderId);
}
}

/**
* Obfuscates the specified purchase. Only the order id, product id and
* developer payload are obfuscated.
* developer payload, signed data and signature are obfuscated.
*
* @param context
* @param purchase
Expand All @@ -319,6 +323,8 @@ static void obfuscate(Context context, Transaction purchase) {
purchase.orderId = Security.obfuscate(context, salt, purchase.orderId);
purchase.productId = Security.obfuscate(context, salt, purchase.productId);
purchase.developerPayload = Security.obfuscate(context, salt, purchase.developerPayload);
purchase.signedData = Security.obfuscate(context, salt, purchase.signedData);
purchase.signature = Security.obfuscate(context, salt, purchase.signature);
}

/**
Expand Down Expand Up @@ -372,16 +378,16 @@ protected static void onPurchaseIntent(String itemId, PendingIntent purchaseInte
/**
* Called after the response to a
* {@link net.robotmedia.billing.request.GetPurchaseInformation} request is
* received. Registers all transactions in local memory and confirms those
* who can be confirmed automatically.
* received. Validates the signature asynchronously and calls
* {@link #onSignatureValidated(Context, String)} if successful.
*
* @param context
* @param signedData
* signed JSON data received from the Market Billing service.
* @param signature
* data signature.
*/
protected static void onPurchaseStateChanged(Context context, String signedData, String signature) {
protected static void onPurchaseStateChanged(final Context context, final String signedData, final String signature) {
debug("Purchase state changed");

if (TextUtils.isEmpty(signedData)) {
Expand All @@ -391,19 +397,51 @@ protected static void onPurchaseStateChanged(Context context, String signedData,
debug(signedData);
}

if (!debug) {
if (TextUtils.isEmpty(signature)) {
Log.w(LOG_TAG, "Empty signature requires debug mode");
return;
if (debug) {
onSignatureValidated(context, signedData, signature);
return;
}

if (TextUtils.isEmpty(signature)) {
Log.w(LOG_TAG, "Empty signature requires debug mode");
return;
}
final ISignatureValidator validator = BillingController.validator != null ? BillingController.validator
: new DefaultSignatureValidator(BillingController.configuration);

// Use AsyncTask mostly in case the signature is validated remotely
new AsyncTask<Void, Void, Boolean>() {

@Override
protected Boolean doInBackground(Void... params) {
return validator.validate(signedData, signature);
}
final ISignatureValidator validator = BillingController.validator != null ? BillingController.validator
: new DefaultSignatureValidator(BillingController.configuration);
if (!validator.validate(signedData, signature)) {
Log.w(LOG_TAG, "Signature does not match data.");
return;

@Override
protected void onPostExecute(Boolean result) {
if (result) {
onSignatureValidated(context, signedData, signature);
} else {
Log.w(LOG_TAG, "Validation failed");
}
}
}

}.execute();
}

/**
* Called after the signature of a response to a
* {@link net.robotmedia.billing.request.GetPurchaseInformation} request has
* been validated. Registers all transactions in local memory and confirms
* those who can be confirmed automatically.
*
* @param context
* @param signedData
* signed JSON data received from the Market Billing service.
* @param signature
* data signature.
*/
private static void onSignatureValidated(Context context, String signedData, String signature) {
List<Transaction> purchases;
try {
JSONObject jObject = new JSONObject(signedData);
Expand All @@ -426,13 +464,18 @@ protected static void onPurchaseStateChanged(Context context, String signedData,
// refunds.
addManualConfirmation(p.productId, p.notificationId);
}

// Add signedData and signature as receipt to transaction
p.signedData = signedData;
p.signature = signature;

storeTransaction(context, p);
notifyPurchaseStateChange(p.productId, p.purchaseState);
notifyPurchaseStateChange(p.productId, p.purchaseState, p.orderId);
}
if (!confirmations.isEmpty()) {
final String[] notifyIds = confirmations.toArray(new String[confirmations.size()]);
confirmNotifications(context, notifyIds);
}
}
}

/**
Expand Down Expand Up @@ -537,11 +580,10 @@ public static boolean registerObserver(IBillingObserver observer) {
}

/**
* Requests the purchase of the specified item. The transaction will not be
* confirmed automatically.
* Requests the purchase of the specified item. The transaction will be
* confirmed automatically. If manual confirmation or a developer payload are required use {@link #requestPurchase(Context, String, boolean, String)} instead.
* <p>
* For subscriptions, use {@link #requestSubscription(Context, String)}
* instead.
* For subscriptions, use {@link #requestSubscription(Context, String)}.
* </p>
*
* @param context
Expand All @@ -550,7 +592,7 @@ public static boolean registerObserver(IBillingObserver observer) {
* @see #requestPurchase(Context, String, boolean)
*/
public static void requestPurchase(Context context, String itemId) {
requestPurchase(context, itemId, false, null);
requestPurchase(context, itemId, true /* confirm */, null);
}

/**
Expand Down Expand Up @@ -583,16 +625,16 @@ public static void requestPurchase(Context context, String itemId, boolean confi
}

/**
* Requests the purchase of the specified subscription item. The transaction
* will not be confirmed automatically.
* Requests the purchase of the specified subscription item. The transaction will be
* confirmed automatically. If manual confirmation or a developer payload are required use {@link #requestSubscription(Context, String, boolean, String)} instead.
*
* @param context
* @param itemId
* id of the item to be purchased.
* @see #requestSubscription(Context, String, boolean, String)
*/
public static void requestSubscription(Context context, String itemId) {
requestSubscription(context, itemId, false, null);
requestSubscription(context, itemId, true /* confirm */, null);
}

/**
Expand Down Expand Up @@ -713,6 +755,8 @@ static void unobfuscate(Context context, Transaction purchase) {
purchase.orderId = Security.unobfuscate(context, salt, purchase.orderId);
purchase.productId = Security.unobfuscate(context, salt, purchase.productId);
purchase.developerPayload = Security.unobfuscate(context, salt, purchase.developerPayload);
purchase.signedData = Security.unobfuscate(context, salt, purchase.signedData);
purchase.signature = Security.unobfuscate(context, salt, purchase.signature);
}

/**
Expand Down
133 changes: 75 additions & 58 deletions AndroidBillingLibrary/src/net/robotmedia/billing/BillingService.java
Original file line number Diff line number Diff line change
Expand Up @@ -153,34 +153,6 @@ private void getPurchaseInformation(Intent intent, int startId) {
runRequestOrQueue(request);
}

@Override
public IBinder onBind(Intent intent) {
return null;
}

public void onServiceConnected(ComponentName name, IBinder service) {
mService = IMarketBillingService.Stub.asInterface(service);
runPendingRequests();
}

public void onServiceDisconnected(ComponentName name) {
mService = null;
}

// This is the old onStart method that will be called on the pre-2.0
// platform. On 2.0 or later we override onStartCommand() so this
// method will not be called.
@Override
public void onStart(Intent intent, int startId) {
handleCommand(intent, startId);
}

// @Override // Avoid compile errors on pre-2.0
public int onStartCommand(Intent intent, int flags, int startId) {
handleCommand(intent, startId);
return Compatibility.START_NOT_STICKY;
}

private void handleCommand(Intent intent, int startId) {
final Action action = getActionFromIntent(intent);
if (action == null) {
Expand Down Expand Up @@ -210,36 +182,104 @@ private void handleCommand(Intent intent, int startId) {
}
}

@Override
public IBinder onBind(Intent intent) {
return null;
}

@Override
public void onDestroy() {
super.onDestroy();
// Ensure we're not leaking Android Market billing service
if (mService != null) {
try {
unbindService(this);
} catch (IllegalArgumentException e) {
// This might happen if the service was disconnected
}
}
}

/**
* Called when a remote exception occurs while trying to execute the
* {@link BillingRequest#run(IMarketBillingService)} method.
* @param e the exception
*/
protected void onRemoteException(RemoteException e) {
Log.w(this.getClass().getSimpleName(), "Remote billing service crashed");
mService = null;
}

public void onServiceConnected(ComponentName name, IBinder service) {
mService = IMarketBillingService.Stub.asInterface(service);
runPendingRequests();
}

public void onServiceDisconnected(ComponentName name) {
mService = null;
}

// This is the old onStart method that will be called on the pre-2.0
// platform. On 2.0 or later we override onStartCommand() so this
// method will not be called.
@Override
public void onStart(Intent intent, int startId) {
handleCommand(intent, startId);
}

// @Override // Avoid compile errors on pre-2.0
public int onStartCommand(Intent intent, int flags, int startId) {
handleCommand(intent, startId);
return Compatibility.START_NOT_STICKY;
}

private void requestPurchase(Intent intent, int startId) {
final String packageName = getPackageName();
final String itemId = intent.getStringExtra(EXTRA_ITEM_ID);
final String developerPayload = intent.getStringExtra(EXTRA_DEVELOPER_PAYLOAD);
final RequestPurchase request = new RequestPurchase(packageName, startId, itemId, developerPayload);
runRequestOrQueue(request);
}

private void requestSubscription(Intent intent, int startId) {
final String packageName = getPackageName();
final String itemId = intent.getStringExtra(EXTRA_ITEM_ID);
final String developerPayload = intent.getStringExtra(EXTRA_DEVELOPER_PAYLOAD);
final RequestPurchase request = new RequestSubscription(packageName, startId, itemId, developerPayload);
runRequestOrQueue(request);
}

private void restoreTransactions(Intent intent, int startId) {
private void restoreTransactions(Intent intent, int startId) {
final String packageName = getPackageName();
final long nonce = intent.getLongExtra(EXTRA_NONCE, 0);
final RestoreTransactions request = new RestoreTransactions(packageName, startId);
request.setNonce(nonce);
runRequestOrQueue(request);
}


/**
* Runs the given billing request if the service is already connected.
* @param request the billing request
* @return true if the request ran successfully; false if the service
* is not connected or there was an error when trying to use it
*/
private boolean runIfConnected(BillingRequest request) {
if (mService == null) return false;
try {
final long requestId = request.run(mService);
BillingController.onRequestSent(requestId, request);
return true;
} catch (RemoteException e) {
onRemoteException(e);
}
return false;
}

private void runPendingRequests() {
BillingRequest request;
int maxStartId = -1;
while ((request = mPendingRequests.peek()) != null) {
if (mService != null) {
runRequest(request);
if (runIfConnected(request)) {
mPendingRequests.remove();
if (maxStartId < request.getStartId()) {
maxStartId = request.getStartId();
Expand All @@ -253,17 +293,7 @@ private void runPendingRequests() {
stopSelf(maxStartId);
}
}

private void runRequest(BillingRequest request) {
try {
final long requestId = request.run(mService);
BillingController.onRequestSent(requestId, request);
} catch (RemoteException e) {
Log.w(this.getClass().getSimpleName(), "Remote billing service crashed");
// TODO: Retry?
}
}


private void runRequestOrQueue(BillingRequest request) {
mPendingRequests.add(request);
if (mService == null) {
Expand All @@ -272,18 +302,5 @@ private void runRequestOrQueue(BillingRequest request) {
runPendingRequests();
}
}

@Override
public void onDestroy() {
super.onDestroy();
// Ensure we're not leaking Android Market billing service
if (mService != null) {
try {
unbindService(this);
} catch (IllegalArgumentException e) {
// This might happen if the service was disconnected
}
}
}

}
Loading