com.badlogic.gdx.pay.android.googleplay.billing.V3GoogleInAppBillingService Maven / Gradle / Ivy
package com.badlogic.gdx.pay.android.googleplay.billing;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import com.android.vending.billing.IInAppBillingService;
import com.badlogic.gdx.backends.android.AndroidApplication;
import com.badlogic.gdx.backends.android.AndroidEventListener;
import com.badlogic.gdx.pay.Information;
import com.badlogic.gdx.pay.Transaction;
import com.badlogic.gdx.pay.android.googleplay.GdxPayException;
import com.badlogic.gdx.pay.android.googleplay.ResponseCode;
import com.badlogic.gdx.pay.android.googleplay.billing.converter.PurchaseResponseActivityResultConverter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
import static com.badlogic.gdx.pay.android.googleplay.GoogleBillingConstants.BUY_INTENT;
import static com.badlogic.gdx.pay.android.googleplay.GoogleBillingConstants.RESPONSE_CODE;
import static com.badlogic.gdx.pay.android.googleplay.billing.converter.GetPurchasesResponseConverter.convertPurchasesResponseToTransactions;
import static com.badlogic.gdx.pay.android.googleplay.billing.converter.GetSkuDetailsRequestConverter.convertProductIdsToItemIdList;
import static com.badlogic.gdx.pay.android.googleplay.billing.converter.GetSkusDetailsResponseBundleConverter.convertSkuDetailsResponse;
public class V3GoogleInAppBillingService implements GoogleInAppBillingService {
public static final int BILLING_API_VERSION = 3;
public static final String PURCHASE_TYPE_IN_APP = "inapp";
public static final String ERROR_NOT_CONNECTED_TO_GOOGLE_IAB = "Not connected to Google In-app Billing service";
public static final String ERROR_ON_SERVICE_DISCONNECTED_RECEIVED = "onServiceDisconnected() received.";
public static final String DEFAULT_DEVELOPER_PAYLOAD = "JustRandomStringTooHardToRememberTralala";
static final String LOG_TAG = "GdxPay/V3GoogleIABS";
public static final long RETRY_PURCHASE_DELAY_IN_MS = 3000L;
private ServiceConnection billingServiceConnection;
@Nullable
private IInAppBillingService billingService;
private final AndroidApplication androidApplication;
private int activityRequestCode;
private PurchaseResponseActivityResultConverter purchaseResponseActivityResultConverter;
private AsyncExecutor asyncExecutor;
private final String installerPackageName;
private final V3GoogleInAppBillingServiceAndroidEventListener androidEventListener = new V3GoogleInAppBillingServiceAndroidEventListener();
private GdxPayAsyncOperationResultListener asyncOperationResultListener;
private ConnectionListener connectionListener;
public V3GoogleInAppBillingService(AndroidApplication application, int activityRequestCode, PurchaseResponseActivityResultConverter purchaseResponseActivityResultConverter, AsyncExecutor asyncExecutor) {
this.androidApplication = application;
this.activityRequestCode = activityRequestCode;
this.purchaseResponseActivityResultConverter = purchaseResponseActivityResultConverter;
this.asyncExecutor = asyncExecutor;
installerPackageName = application.getPackageName();
}
@Override
public void requestConnect(ConnectionListener connectionListener) {
if (this.connectionListener != null) {
throw new IllegalStateException("Already listening for connections.");
}
this.connectionListener = connectionListener;
billingServiceConnection = new BillingServiceInitializingServiceConnection();
bindBillingServiceConnectionToActivity();
}
protected void bindBillingServiceConnectionToActivity() {
try {
if (!androidApplication.bindService(createBindBillingServiceIntent(), billingServiceConnection, Context.BIND_AUTO_CREATE)) {
this.connectionListener.disconnected(new GdxPayException("bindService() returns false."));
}
} catch(GdxPayException e) {
throw e;
}
catch (RuntimeException e) {
this.connectionListener.disconnected(new GdxPayException("requestConnect() failed.", e));
}
}
private Intent createBindBillingServiceIntent() {
Intent serviceIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND");
serviceIntent.setPackage("com.android.vending");
return serviceIntent;
}
@Override
public Map getProductsDetails(List productIds) {
long startTimeInMs = System.currentTimeMillis();
try {
return fetchSkuDetails(productIds);
} catch (RuntimeException e) {
throw new GdxPayException("getProductsDetails(" + productIds + " failed) after " + deltaInSeconds(startTimeInMs) + " seconds", e);
}
}
@Override
public void startPurchaseRequest(String productId, PurchaseRequestCallback listener) {
internalStartPurchaseRequest(productId, listener, true);
}
private void internalStartPurchaseRequest(String productId, PurchaseRequestCallback listener, boolean retryOnError) {
PendingIntent pendingIntent;
try {
pendingIntent = getBuyIntent(productId);
} catch (RemoteException |RuntimeException e) {
if (retryOnError) {
reconnectToHandleDeadObjectExceptions();
schedulePurchaseRetry(productId, listener);
return;
}
listener.purchaseError(new GdxPayException("startPurchaseRequest failed at getBuyIntent() for product: " + productId, e));
return;
}
startPurchaseIntentSenderForResult(productId, pendingIntent, listener);
}
private void schedulePurchaseRetry(final String productId, final PurchaseRequestCallback listener) {
Runnable runnable = new Runnable() {
@Override
public void run() {
internalStartPurchaseRequest(productId, listener, false);
}
};
asyncExecutor.executeAsync(runnable, RETRY_PURCHASE_DELAY_IN_MS);
}
private void reconnectToHandleDeadObjectExceptions() {
unbindBillingServiceAndRemoveAndroidEvenetListener();
bindBillingServiceConnectionToActivity();
}
private void startPurchaseIntentSenderForResult(String productId, PendingIntent pendingIntent, final PurchaseRequestCallback listener) {
try {
androidApplication.startIntentSenderForResult(pendingIntent.getIntentSender(),
activityRequestCode, new Intent(), 0, 0, 0);
listenForAppBillingActivityEventOnce(new GdxPayAsyncOperationResultListener() {
@Override
public void onEvent(int resultCode, Intent data) {
if (resultCode == Activity.RESULT_OK) {
handleResultOk(data);
return;
}
if (resultCode == Activity.RESULT_CANCELED) {
listener.purchaseCanceled();
return;
}
listener.purchaseError(new GdxPayException("Unexpected resultCode:" + resultCode + "with data:" + data));
}
protected void handleResultOk(Intent data) {
final Transaction transaction;
try {
transaction = convertPurchaseResponseDataToTransaction(data);
} catch (GdxPayException e) {
listener.purchaseError(new GdxPayException("Error converting purchase success response: " + data, e));
return;
}
listener.purchaseSuccess(transaction);
}
});
} catch (IntentSender.SendIntentException e) {
listener.purchaseError(new GdxPayException("startIntentSenderForResult failed for product: " + productId, e));
}
}
private Transaction convertPurchaseResponseDataToTransaction(Intent responseIntentData) {
return purchaseResponseActivityResultConverter.convertToTransaction(responseIntentData);
}
private void listenForAppBillingActivityEventOnce(GdxPayAsyncOperationResultListener gdxPayAsyncListener) {
asyncOperationResultListener = gdxPayAsyncListener;
}
private PendingIntent getBuyIntent(String productId) throws RemoteException {
Bundle intent = billingService().getBuyIntent(BILLING_API_VERSION, installerPackageName, productId, PURCHASE_TYPE_IN_APP, DEFAULT_DEVELOPER_PAYLOAD);
return fetchPendingIntentFromGetBuyIntentResponse(intent);
}
private PendingIntent fetchPendingIntentFromGetBuyIntentResponse(Bundle responseData) {
int code = responseData.getInt(RESPONSE_CODE);
ResponseCode responseCode = ResponseCode.findByCode(code);
if (responseCode != ResponseCode.BILLING_RESPONSE_RESULT_OK) {
// TODO: unit test this.
throw new GdxPayException("Unexpected getBuyIntent() responseCode: " + responseCode + " with response data: " + responseData);
}
PendingIntent pendingIntent = responseData.getParcelable(BUY_INTENT);
if (pendingIntent == null) {
throw new GdxPayException("Missing value for key: " + BUY_INTENT + "in getBuyIntent() response: " + responseData);
}
return pendingIntent;
}
private Map fetchSkuDetails(List productIds) {
Bundle skusRequest = convertProductIdsToItemIdList(productIds);
Bundle skuDetailsResponse = executeGetSkuDetails(skusRequest);
Map informationMap = new HashMap<>();
informationMap.putAll(convertSkuDetailsResponse(skuDetailsResponse));
return informationMap;
}
@Override
public void disconnect() {
billingService = null;
unbindBillingServiceAndRemoveAndroidEvenetListener();
connectionListener = null;
}
boolean isConnected() {
return billingService != null;
}
@Override
public boolean isListeningForConnections() {
return connectionListener != null;
}
@Override
public List getPurchases() {
try {
Bundle purchases = billingService().getPurchases(BILLING_API_VERSION, installerPackageName, V3GoogleInAppBillingService.PURCHASE_TYPE_IN_APP, null);
return convertPurchasesResponseToTransactions(purchases);
} catch (RemoteException | RuntimeException e) { // TODO: unit test RuntimeException scenario, e.g. : java.lang.IllegalArgumentException: Unexpected response code: ResponseCode{code=3, message='Billing API version is not supported for the type requested'}, response: Bundle[{RESPONSE_CODE=3}]
throw new GdxPayException("Unexpected exception in getPurchases()", e);
}
}
private void unbindBillingServiceAndRemoveAndroidEvenetListener() {
if (billingServiceConnection != null) {
try {
androidApplication.unbindService(billingServiceConnection);
} catch(Exception e) {
// Gdx-Pay uses statics. Android reuses JVM instances sometimes.
// When com.badlogic.gdx.pay.PurchaseSystem.onAppRestarted() unbinds, with
// an old activity instance from a previous launch, it will run into this Exception.
Log.e(LOG_TAG, "Unexpected exception in unbindService()", e);
}
}
androidApplication.removeAndroidEventListener(androidEventListener);
}
private Bundle executeGetSkuDetails(Bundle skusRequest) {
try {
return billingService().getSkuDetails(BILLING_API_VERSION, installerPackageName,
PURCHASE_TYPE_IN_APP, skusRequest);
} catch (RemoteException e) {
// TODO: unit test this.
throw new GdxPayException("getProductsDetails failed for bundle:" + skusRequest, e);
}
}
private IInAppBillingService billingService() {
if (!isConnected()) {
throw new GdxPayException(ERROR_NOT_CONNECTED_TO_GOOGLE_IAB);
}
return billingService;
}
protected IInAppBillingService lookupByStubAsInterface(IBinder service) {
return IInAppBillingService.Stub.asInterface(service);
}
private class BillingServiceInitializingServiceConnection implements ServiceConnection {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Log.d(LOG_TAG, "start onServiceConnected(), isConnected() is: " + isConnected());
if (isConnected()) {
return;
}
billingService = lookupByStubAsInterface(service);
connectionListener.connected();
androidApplication.addAndroidEventListener(androidEventListener);
}
@Override
public void onServiceDisconnected(ComponentName name) {
unbindBillingServiceAndRemoveAndroidEvenetListener();
billingService = null;
connectionListener.disconnected(new GdxPayException(ERROR_ON_SERVICE_DISCONNECTED_RECEIVED));
}
}
private void onGdxPayActivityEvent(int resultCode, Intent data) {
if (this.asyncOperationResultListener != null) {
asyncOperationResultListener.onEvent(resultCode, data);
asyncOperationResultListener = null;
}
}
private final class V3GoogleInAppBillingServiceAndroidEventListener implements AndroidEventListener {
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (activityRequestCode == requestCode) {
onGdxPayActivityEvent(resultCode, data);
}
}
}
private interface GdxPayAsyncOperationResultListener {
void onEvent(int resultCode, Intent data);
}
int deltaInSeconds(long startTimeInMs) {
return deltaInSeconds(System.currentTimeMillis(), startTimeInMs);
}
int deltaInSeconds(long endTimeMillis, long startTimeMillis) {
return (int) ((endTimeMillis - startTimeMillis) / 1000l);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy