/*
* 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.example.dungeons;
import com.android.vending.billing.IMarketBillingService;
import com.example.dungeons.Consts.PurchaseState;
import com.example.dungeons.Consts.ResponseCode;
import com.example.dungeons.Security.VerifiedPurchase;
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 java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
/**
* This class sends messages to Android Market on behalf of the application by
* connecting (binding) to the MarketBillingService. The application
* creates an instance of this class and invokes billing requests through this service.
*
* The {@link BillingReceiver} class starts this service to process commands
* that it receives from Android Market.
*
* You should modify and obfuscate this code before using it.
*/
public class BillingService extends Service implements ServiceConnection {
private static final String TAG = "BillingService";
/** The service connection to the remote MarketBillingService. */
private static IMarketBillingService mService;
/**
* The list of requests that are pending while we are waiting for the
* connection to the MarketBillingService to be established.
*/
private static LinkedList<BillingRequest> mPendingRequests = new LinkedList<BillingRequest>();
/**
* The list of requests that we have sent to Android Market but for which we have
* not yet received a response code. The HashMap is indexed by the
* request Id that each request receives when it executes.
*/
private static HashMap<Long, BillingRequest> mSentRequests =
new HashMap<Long, BillingRequest>();
/**
* The base class for all requests that use the MarketBillingService.
* Each derived class overrides the run() method to call the appropriate
* service interface. If we are already connected to the MarketBillingService,
* then we call the run() method directly. Otherwise, we bind
* to the service and save the request on a queue to be run later when
* the service is connected.
*/
abstract class BillingRequest {
private final int mStartId;
protected long mRequestId;
public BillingRequest(int startId) {
mStartId = startId;
}
public int getStartId() {
return mStartId;
}
/**
* Run the request, starting the connection if necessary.
* @return true if the request was executed or queued; false if there
* was an error starting the connection
*/
public boolean runRequest() {
if (runIfConnected()) {
return true;
}
if (bindToMarketBillingService()) {
// Add a pending request to run when the service is connected.
mPendingRequests.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 (Consts.DEBUG) {
Log.d(TAG, getClass().getSimpleName());
}
if (mService != null) {
try {
mRequestId = run();
if (Consts.DEBUG) {
Log.d(TAG, "request id: " + mRequestId);
}
if (mRequestId >= 0) {
mSentRequests.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");
mService = null;
}
/**
* The derived class must implement this method.
* @throws 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(Consts.BILLING_REQUEST_METHOD, method);
request.putInt(Consts.BILLING_REQUEST_API_VERSION, 2);
request.putString(Consts.BILLING_REQUEST_PACKAGE_NAME, getPackageName());
return request;
}
protected void logResponseCode(String method, Bundle response) {
ResponseCode responseCode = ResponseCode.valueOf(
response.getInt(Consts.BILLING_RESPONSE_RESPONSE_CODE));
if (Consts.DEBUG) {
Log.e(TAG, method + " received " + responseCode.toString());
}
}
}
/**
* Wrapper class that checks if in-app billing is supported.
*
* Note: Support for subscriptions implies support for one-time purchases. However, the opposite
* is not true.
*
* Developers may want to perform two checks if both one-time and subscription products are
* available.
*/
class CheckBillingSupported extends BillingRequest {
public String mProductType = null;
/** Legacy contrustor
*
* This constructor is provided for legacy purposes. Assumes the calling application will
* not be using any features not present in API v1, such as subscriptions.
*/
@Deprecated
public CheckBillingSupported() {
// 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);
}
/** Constructor
*
* Note: Support for subscriptions implies support for one-time purchases. However, the
* opposite is not true.
*
* Developers may want to perform two checks if both one-time and subscription products are
* available.
*
* @pram itemType Either Consts.ITEM_TYPE_INAPP or Consts.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(Consts.BILLING_REQUEST_ITEM_TYPE, mProductType);
}
Bundle response = mService.sendBillingRequest(request);
int responseCode = response.getInt(Consts.BILLING_RESPONSE_RESPONSE_CODE);
if (Consts.DEBUG) {
Log.i(TAG, "CheckBillingSupported response code: " +
ResponseCode.valueOf(responseCode));
}
boolean billingSupported = (responseCode == ResponseCode.RESULT_OK.ordinal());
ResponseHandler.checkBillingSupportedResponse(billingSupported, mProductType);
return Consts.BILLING_RESPONSE_INVALID_REQUEST_ID;
}
}
/**
* Wrapper class that requests a purchase.
*/
class RequestPurchase extends BillingRequest {
public final String mProductId;
public final String mDeveloperPayload;
public final String mProductType;
/** Legacy constructor
*
* @param itemId The ID of the item to be purchased. Will be assumed to be a one-time
* purchase.
*/
@Deprecated
public RequestPurchase(String itemId) {
this(itemId, null, null);
}
/** Legacy constructor
*
* @param itemId The ID of the item to be purchased. Will be assumed to be a one-time
* purchase.
* @param developerPayload Optional data.
*/
@Deprecated
public RequestPurchase(String itemId, String developerPayload) {
this(itemId, null, developerPayload);
}
/** Constructor
*
* @param itemId The ID of the item to be purchased. Will be assumed to be a one-time
* purchase.
* @param itemType Either Consts.ITEM_TYPE_INAPP or Consts.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(Consts.BILLING_REQUEST_ITEM_ID, mProductId);
request.putString(Consts.BILLING_REQUEST_ITEM_TYPE, mProductType);
// Note that the developer payload is optional.
if (mDeveloperPayload != null) {
request.putString(Consts.BILLING_REQUEST_DEVELOPER_PAYLOAD, mDeveloperPayload);
}
Bundle response = mService.sendBillingRequest(request);
PendingIntent pendingIntent
= response.getParcelable(Consts.BILLING_RESPONSE_PURCHASE_INTENT);
if (pendingIntent == null) {
Log.e(TAG, "Error with requestPurchase");
return Consts.BILLING_RESPONSE_INVALID_REQUEST_ID;
}
Intent intent = new Intent();
ResponseHandler.buyPageIntentResponse(pendingIntent, intent);
return response.getLong(Consts.BILLING_RESPONSE_REQUEST_ID,
Consts.BILLING_RESPONSE_INVALID_REQUEST_ID);
}
@Override
protected void responseCodeReceived(ResponseCode responseCode) {
ResponseHandler.responseCodeReceived(BillingService.this, this, responseCode);
}
}
/**
* Wrapper class that confirms a list of notifications to the server.
*/
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(Consts.BILLING_REQUEST_NOTIFY_IDS, mNotifyIds);
Bundle response = mService.sendBillingRequest(request);
logResponseCode("confirmNotifications", response);
return response.getLong(Consts.BILLING_RESPONSE_REQUEST_ID,
Consts.BILLING_RESPONSE_INVALID_REQUEST_ID);
}
}
/**
* Wrapper class that sends a GET_PURCHASE_INFORMATION message to the server.
*/
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(Consts.BILLING_REQUEST_NONCE, mNonce);
request.putStringArray(Consts.BILLING_REQUEST_NOTIFY_IDS, mNotifyIds);
Bundle response = mService.sendBillingRequest(request);
logResponseCode("getPurchaseInformation", response);
return response.getLong(Consts.BILLING_RESPONSE_REQUEST_ID,
Consts.BILLING_RESPONSE_INVALID_REQUEST_ID);
}
@Override
protected void onRemoteException(RemoteException e) {
super.onRemoteException(e);
Security.removeNonce(mNonce);
}
}
/**
* Wrapper class that sends a RESTORE_TRANSACTIONS message to the server.
*/
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(Consts.BILLING_REQUEST_NONCE, mNonce);
Bundle response = mService.sendBillingRequest(request);
logResponseCode("restoreTransactions", response);
return response.getLong(Consts.BILLING_RESPONSE_REQUEST_ID,
Consts.BILLING_RESPONSE_INVALID_REQUEST_ID);
}
@Override
protected void onRemoteException(RemoteException e) {
super.onRemoteException(e);
Security.removeNonce(mNonce);
}
@Override
protected void responseCodeReceived(ResponseCode responseCode) {
ResponseHandler.responseCodeReceived(BillingService.this, this, responseCode);
}
}
public BillingService() {
super();
}
public void setContext(Context context) {
attachBaseContext(context);
}
/**
* We don't support binding to this service, only starting the service.
*/
@Override
public IBinder onBind(Intent intent) {
return null;
}
@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
*/
public void handleCommand(Intent intent, int startId) {
String action = intent.getAction();
if (Consts.DEBUG) {
Log.i(TAG, "handleCommand() action: " + action);
}
if (Consts.ACTION_CONFIRM_NOTIFICATION.equals(action)) {
String[] notifyIds = intent.getStringArrayExtra(Consts.NOTIFICATION_ID);
confirmNotifications(startId, notifyIds);
} else if (Consts.ACTION_GET_PURCHASE_INFORMATION.equals(action)) {
String notifyId = intent.getStringExtra(Consts.NOTIFICATION_ID);
getPurchaseInformation(startId, new String[] { notifyId });
} else if (Consts.ACTION_PURCHASE_STATE_CHANGED.equals(action)) {
String signedData = intent.getStringExtra(Consts.INAPP_SIGNED_DATA);
String signature = intent.getStringExtra(Consts.INAPP_SIGNATURE);
purchaseStateChanged(startId, signedData, signature);
} else if (Consts.ACTION_RESPONSE_CODE.equals(action)) {
long requestId = intent.getLongExtra(Consts.INAPP_REQUEST_ID, -1);
int responseCodeIndex = intent.getIntExtra(Consts.INAPP_RESPONSE_CODE,
ResponseCode.RESULT_ERROR.ordinal());
ResponseCode responseCode = ResponseCode.valueOf(responseCodeIndex);
checkResponseCode(requestId, responseCode);
}
}
/**
* Binds to the MarketBillingService and returns true if the bind
* succeeded.
* @return true if the bind succeeded; false otherwise
*/
private boolean bindToMarketBillingService() {
try {
if (Consts.DEBUG) {
Log.i(TAG, "binding to Market billing service");
}
boolean bindResult = bindService(
new Intent(Consts.MARKET_BILLING_SERVICE_ACTION),
this, // ServiceConnection.
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;
}
/**
* Checks if in-app billing is supported. Assumes this is a one-time purchase.
*
* @return true if supported; false otherwise
*/
@Deprecated
public boolean checkBillingSupported() {
return new CheckBillingSupported().runRequest();
}
/**
* Checks if in-app billing is supported.
* @pram itemType Either Consts.ITEM_TYPE_INAPP or Consts.ITEM_TYPE_SUBSCRIPTION, indicating the
* type of item support is being checked for.
* @return true if supported; false otherwise
*/
public boolean checkBillingSupported(String itemType) {
return new CheckBillingSupported(itemType).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 Consts#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 Consts.ITEM_TYPE_INAPP or Consts.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 Consts#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
* {@link ResponseHandler#purchaseResponse(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)
* @param signature the signature for the data, signed with the private key
*/
private void purchaseStateChanged(int startId, String signedData, String signature) {
ArrayList<Security.VerifiedPurchase> purchases;
purchases = Security.verifyPurchase(signedData, signature);
if (purchases == null) {
return;
}
ArrayList<String> notifyList = new ArrayList<String>();
for (VerifiedPurchase vp : purchases) {
if (vp.notificationId != null) {
notifyList.add(vp.notificationId);
}
ResponseHandler.purchaseResponse(this, vp.purchaseState, vp.productId,
vp.orderId, vp.purchaseTime, vp.developerPayload);
}
if (!notifyList.isEmpty()) {
String[] notifyIds = notifyList.toArray(new String[notifyList.size()]);
confirmNotifications(startId, notifyIds);
}
}
/**
* This is called when we receive a response code from Android Market for a request
* that we made. This is used for reporting various errors and for
* acknowledging that an order was sent to the server. This is NOT used
* for any purchase state changes. All purchase state changes are received
* in the {@link BillingReceiver} and passed to this service, where they are
* handled in {@link #purchaseStateChanged(int, String, String)}.
* @param requestId a number that identifies a request, assigned at the
* time the request was made to Android Market
* @param responseCode a response code from Android Market to indicate the state
* of the request
*/
private void checkResponseCode(long requestId, ResponseCode responseCode) {
BillingRequest request = mSentRequests.get(requestId);
if (request != null) {
if (Consts.DEBUG) {
Log.d(TAG, request.getClass().getSimpleName() + ": " + responseCode);
}
request.responseCodeReceived(responseCode);
}
mSentRequests.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 = mPendingRequests.peek()) != null) {
if (request.runIfConnected()) {
// Remove the request
mPendingRequests.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 (Consts.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) {
if (Consts.DEBUG) {
Log.d(TAG, "Billing service connected");
}
mService = IMarketBillingService.Stub.asInterface(service);
runPendingRequests();
}
/**
* This is called when we are disconnected from the MarketBillingService.
*/
@Override
public void onServiceDisconnected(ComponentName name) {
Log.w(TAG, "Billing service disconnected");
mService = null;
}
/**
* Unbinds from the MarketBillingService. Call this when the application
* terminates to avoid leaking a ServiceConnection.
*/
public void unbind() {
try {
unbindService(this);
} catch (IllegalArgumentException e) {
// This might happen if the service was disconnected
}
}
}