package com.defold.iap; import java.util.ArrayList; import java.util.List; import org.json.JSONException; import org.json.JSONObject; import android.app.Activity; import android.app.PendingIntent; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentSender.SendIntentException; import android.content.ServiceConnection; import android.content.pm.ResolveInfo; import android.os.Bundle; import android.os.IBinder; import android.os.Message; import android.os.Messenger; import android.os.RemoteException; import android.util.Log; import android.view.View; import android.view.ViewGroup.LayoutParams; import com.android.vending.billing.IInAppBillingService; import com.defold.iap.IapGooglePlay.Action; public class IapGooglePlayActivity extends Activity { private boolean hasPendingPurchases = false; private boolean autoFinishTransactions = true; private boolean isDone = false; private Messenger messenger; ServiceConnection serviceConn; IInAppBillingService service; // NOTE: Code from "trivialdrivesample" int getResponseCodeFromBundle(Bundle b) { Object o = b.get(IapGooglePlay.RESPONSE_CODE); if (o == null) { Log.d(IapGooglePlay.TAG, "Bundle with null response code, assuming OK (known issue)"); return IapJNI.BILLING_RESPONSE_RESULT_OK; } else if (o instanceof Integer) return ((Integer) o).intValue(); else if (o instanceof Long) return (int) ((Long) o).longValue(); else { Log.e(IapGooglePlay.TAG, "Unexpected type for bundle response code."); Log.e(IapGooglePlay.TAG, o.getClass().getName()); throw new RuntimeException("Unexpected type for bundle response code: " + o.getClass().getName()); } } private void sendBuyError(int error) { Bundle bundle = new Bundle(); bundle.putString("action", Action.BUY.toString()); bundle.putInt(IapGooglePlay.RESPONSE_CODE, error); Message msg = new Message(); msg.setData(bundle); try { messenger.send(msg); } catch (RemoteException e) { Log.wtf(IapGooglePlay.TAG, "Unable to send message", e); } this.finish(); } private void buy(String product, String productType) { // Flush any pending items, in order to be able to buy the same (new) product again processPendingConsumables(); try { Bundle buyIntentBundle = service.getBuyIntent(3, getPackageName(), product, productType, ""); int response = getResponseCodeFromBundle(buyIntentBundle); if (response == IapJNI.BILLING_RESPONSE_RESULT_OK) { hasPendingPurchases = true; PendingIntent pendingIntent = buyIntentBundle.getParcelable("BUY_INTENT"); startIntentSenderForResult(pendingIntent.getIntentSender(), 1001, new Intent(), Integer.valueOf(0), Integer.valueOf(0), Integer.valueOf(0)); } else if (response == IapJNI.BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED) { sendBuyError(IapJNI.BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED); } else { sendBuyError(response); } } catch (RemoteException e) { Log.e(IapGooglePlay.TAG, String.format("Failed to buy", e)); sendBuyError(IapJNI.BILLING_RESPONSE_RESULT_ERROR); } catch (SendIntentException e) { Log.e(IapGooglePlay.TAG, String.format("Failed to buy", e)); sendBuyError(IapJNI.BILLING_RESPONSE_RESULT_ERROR); } } private boolean consume(String purchaseData) { try { if (purchaseData == null) { Log.e(IapGooglePlay.TAG, String.format("Failed to consume purchase, purchaseData was null!")); return false; } JSONObject pd = new JSONObject(purchaseData); if (!pd.isNull("autoRenewing")) { Log.i(IapGooglePlay.TAG, "Will not consume purchase since it is a subscription."); return true; } String token = pd.getString("purchaseToken"); int consumeResponse = service.consumePurchase(3, getPackageName(), token); if (consumeResponse == IapJNI.BILLING_RESPONSE_RESULT_OK) { return true; } else { Log.e(IapGooglePlay.TAG, String.format("Failed to consume purchase (%d)", consumeResponse)); sendBuyError(consumeResponse); } } catch (RemoteException e) { Log.e(IapGooglePlay.TAG, "Failed to consume purchase", e); sendBuyError(IapJNI.BILLING_RESPONSE_RESULT_ERROR); } catch (JSONException e) { Log.e(IapGooglePlay.TAG, "Failed to consume purchase", e); sendBuyError(IapJNI.BILLING_RESPONSE_RESULT_ERROR); } return false; } private boolean processPurchase(String purchaseData, String signature) { if (this.autoFinishTransactions && !consume(purchaseData)) { Log.e(IapGooglePlay.TAG, "Failed to consume and send message"); return false; } Bundle bundle = new Bundle(); bundle.putString("action", Action.BUY.toString()); bundle.putInt(IapGooglePlay.RESPONSE_CODE, IapJNI.BILLING_RESPONSE_RESULT_OK); bundle.putString(IapGooglePlay.RESPONSE_INAPP_PURCHASE_DATA, purchaseData); bundle.putString(IapGooglePlay.RESPONSE_INAPP_SIGNATURE, signature); Message msg = new Message(); msg.setData(bundle); try { messenger.send(msg); return true; } catch (RemoteException e) { Log.wtf(IapGooglePlay.TAG, "Unable to send message", e); return false; } } // Make buy response codes for all consumables not yet processed. private void processPendingConsumables() { try { // Note: subscriptions cannot be consumed // https://developer.android.com/google/play/billing/api.html#subs Bundle items = service.getPurchases(3, getPackageName(), "inapp", null); int response = getResponseCodeFromBundle(items); if (response == IapJNI.BILLING_RESPONSE_RESULT_OK) { ArrayList purchaseDataList = items.getStringArrayList(IapGooglePlay.RESPONSE_INAPP_PURCHASE_DATA_LIST); ArrayList signatureList = items.getStringArrayList(IapGooglePlay.RESPONSE_INAPP_SIGNATURE_LIST); for (int i = 0; i < purchaseDataList.size(); ++i) { String purchaseData = purchaseDataList.get(i); String signature = signatureList.get(i); if (!processPurchase(purchaseData, signature)) { // abort and retry some other time break; } } } } catch (RemoteException e) { Log.e(IapGooglePlay.TAG, "Failed to process purchase", e); } } private void restore() { int response = IapJNI.BILLING_RESPONSE_RESULT_ERROR; Bundle bundle = new Bundle(); bundle.putString("action", Action.RESTORE.toString()); Bundle items = new Bundle(); try { ArrayList purchaseItemList = new ArrayList(); ArrayList purchaseDataList = new ArrayList(); ArrayList signatureList = new ArrayList(); Bundle inapp = service.getPurchases(3, getPackageName(), "inapp", null); if (getResponseCodeFromBundle(inapp) == IapJNI.BILLING_RESPONSE_RESULT_OK) { purchaseItemList.addAll(inapp.getStringArrayList(IapGooglePlay.RESPONSE_INAPP_ITEM_LIST)); purchaseDataList.addAll(inapp.getStringArrayList(IapGooglePlay.RESPONSE_INAPP_PURCHASE_DATA_LIST)); signatureList.addAll(inapp.getStringArrayList(IapGooglePlay.RESPONSE_INAPP_SIGNATURE_LIST)); } Bundle subs = service.getPurchases(3, getPackageName(), "subs", null); if (getResponseCodeFromBundle(subs) == IapJNI.BILLING_RESPONSE_RESULT_OK) { purchaseItemList.addAll(subs.getStringArrayList(IapGooglePlay.RESPONSE_INAPP_ITEM_LIST)); purchaseDataList.addAll(subs.getStringArrayList(IapGooglePlay.RESPONSE_INAPP_PURCHASE_DATA_LIST)); signatureList.addAll(subs.getStringArrayList(IapGooglePlay.RESPONSE_INAPP_SIGNATURE_LIST)); } items.putStringArrayList(IapGooglePlay.RESPONSE_INAPP_ITEM_LIST, purchaseItemList); items.putStringArrayList(IapGooglePlay.RESPONSE_INAPP_PURCHASE_DATA_LIST, purchaseDataList); items.putStringArrayList(IapGooglePlay.RESPONSE_INAPP_SIGNATURE_LIST, signatureList); } catch (RemoteException e) { Log.e(IapGooglePlay.TAG, "Failed to restore purchases", e); } bundle.putBundle("items", items); bundle.putInt(IapGooglePlay.RESPONSE_CODE, response); Message msg = new Message(); msg.setData(bundle); try { messenger.send(msg); } catch (RemoteException e) { Log.wtf(IapGooglePlay.TAG, "Unable to send message", e); } this.finish(); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); View view = new View(this); view.setBackgroundColor(0x10ffffff); setContentView(view, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); Intent intent = getIntent(); final Bundle extras = intent.getExtras(); this.messenger = (Messenger) extras.getParcelable(IapGooglePlay.PARAM_MESSENGER); final Action action = Action.valueOf(intent.getAction()); this.autoFinishTransactions = extras.getBoolean(IapGooglePlay.PARAM_AUTOFINISH_TRANSACTIONS); Intent serviceIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND"); serviceIntent.setPackage("com.android.vending"); List intentServices = getPackageManager().queryIntentServices(serviceIntent, 0); if (intentServices != null && !intentServices.isEmpty()) { // service available to handle that Intent serviceConn = new ServiceConnection() { @Override public void onServiceDisconnected(ComponentName name) { service = null; } @Override public void onServiceConnected(ComponentName name, IBinder serviceBinder) { service = IInAppBillingService.Stub.asInterface(serviceBinder); if (action == Action.BUY) { buy(extras.getString(IapGooglePlay.PARAM_PRODUCT), extras.getString(IapGooglePlay.PARAM_PRODUCT_TYPE)); } else if (action == Action.RESTORE) { restore(); } else if (action == Action.PROCESS_PENDING_CONSUMABLES) { processPendingConsumables(); finish(); } else if (action == Action.FINISH_TRANSACTION) { consume(extras.getString(IapGooglePlay.PARAM_PURCHASE_DATA)); finish(); } } }; bindService(serviceIntent, serviceConn, Context.BIND_AUTO_CREATE); } else { // Service will never be connected; just send unavailability message Bundle bundle = new Bundle(); bundle.putString("action", intent.getAction()); bundle.putInt(IapGooglePlay.RESPONSE_CODE, IapJNI.BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE); Message msg = new Message(); msg.setData(bundle); try { messenger.send(msg); } catch (RemoteException e) { Log.wtf(IapGooglePlay.TAG, "Unable to send message", e); } this.finish(); } } @Override public void finish() { super.finish(); this.isDone = true; } @Override protected void onDestroy() { if (hasPendingPurchases) { // Not sure connection is up so need to check here. if (service != null) { if(autoFinishTransactions) { processPendingConsumables(); } } hasPendingPurchases = false; } if( !isDone ) { Intent intent = getIntent(); if( intent != null && intent.getComponent().getClassName().equals( getClass().getName() ) ) { Log.v(IapGooglePlay.TAG, "There's still an intent left: " + intent.getAction() ); sendBuyError(IapJNI.BILLING_RESPONSE_RESULT_ERROR); } } if (serviceConn != null) { try { unbindService(serviceConn); } catch (IllegalArgumentException e) { Log.wtf(IapGooglePlay.TAG, "Unable to unbind service", e); } } super.onDestroy(); } // NOTE: Code from "trivialdrivesample" int getResponseCodeFromIntent(Intent i) { Object o = i.getExtras().get(IapGooglePlay.RESPONSE_CODE); if (o == null) { Log.e(IapGooglePlay.TAG, "Intent with no response code, assuming OK (known issue)"); return IapJNI.BILLING_RESPONSE_RESULT_OK; } else if (o instanceof Integer) { return ((Integer) o).intValue(); } else if (o instanceof Long) { return (int) ((Long) o).longValue(); } else { Log.e(IapGooglePlay.TAG, "Unexpected type for intent response code."); Log.e(IapGooglePlay.TAG, o.getClass().getName()); throw new RuntimeException("Unexpected type for intent response code: " + o.getClass().getName()); } } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); Bundle bundle = null; if (data != null) { int responseCode = getResponseCodeFromIntent(data); String purchaseData = data.getStringExtra(IapGooglePlay.RESPONSE_INAPP_PURCHASE_DATA); String dataSignature = data.getStringExtra(IapGooglePlay.RESPONSE_INAPP_SIGNATURE); if (responseCode == IapJNI.BILLING_RESPONSE_RESULT_OK) { processPurchase(purchaseData, dataSignature); } else { bundle = new Bundle(); bundle.putString("action", Action.BUY.toString()); bundle.putInt(IapGooglePlay.RESPONSE_CODE, responseCode); bundle.putString(IapGooglePlay.RESPONSE_INAPP_PURCHASE_DATA, purchaseData); bundle.putString(IapGooglePlay.RESPONSE_INAPP_SIGNATURE, dataSignature); } } else { bundle = new Bundle(); bundle.putString("action", Action.BUY.toString()); bundle.putInt(IapGooglePlay.RESPONSE_CODE, IapJNI.BILLING_RESPONSE_RESULT_ERROR); } // Send message if generated above if (bundle != null) { Message msg = new Message(); msg.setData(bundle); try { messenger.send(msg); } catch (RemoteException e) { Log.wtf(IapGooglePlay.TAG, "Unable to send message", e); } } this.finish(); } }