package ru.zzzzzzerg; import android.app.Activity; import android.app.PendingIntent; import android.content.ServiceConnection; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.os.IBinder; import android.os.Bundle; import android.os.RemoteException; import android.util.Log; import android.util.Base64; import android.text.TextUtils; import android.content.IntentSender.SendIntentException; import com.android.vending.billing.IInAppBillingService; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.KeyFactory; import java.security.PublicKey; import java.security.Signature; import java.security.SignatureException; import java.security.spec.InvalidKeySpecException; import java.security.spec.X509EncodedKeySpec; import java.util.ArrayList; import org.haxe.nme.HaxeObject; import org.haxe.nme.GameActivity; public class IAP { // Billing response codes public static final int BILLING_RESPONSE_RESULT_OK = 0; public static final int BILLING_RESPONSE_RESULT_USER_CANCELED = 1; public static final int BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE = 3; public static final int BILLING_RESPONSE_RESULT_ITEM_UNAVAILABLE = 4; public static final int BILLING_RESPONSE_RESULT_DEVELOPER_ERROR = 5; public static final int BILLING_RESPONSE_RESULT_ERROR = 6; public static final int BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED = 7; public static final int BILLING_RESPONSE_RESULT_ITEM_NOT_OWNED = 8; // IAB Helper error codes public static final int IABHELPER_ERROR_BASE = -1000; public static final int IABHELPER_REMOTE_EXCEPTION = -1001; public static final int IABHELPER_BAD_RESPONSE = -1002; public static final int IABHELPER_VERIFICATION_FAILED = -1003; public static final int IABHELPER_SEND_INTENT_FAILED = -1004; public static final int IABHELPER_USER_CANCELLED = -1005; public static final int IABHELPER_UNKNOWN_PURCHASE_RESPONSE = -1006; public static final int IABHELPER_MISSING_TOKEN = -1007; public static final int IABHELPER_UNKNOWN_ERROR = -1008; public static final int IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE = -1009; public static final int IABHELPER_INVALID_CONSUMPTION = -1010; // Keys for the responses from InAppBillingService public static final String RESPONSE_CODE = "RESPONSE_CODE"; public static final String RESPONSE_GET_SKU_DETAILS_LIST = "DETAILS_LIST"; public static final String RESPONSE_BUY_INTENT = "BUY_INTENT"; public static final String RESPONSE_INAPP_PURCHASE_DATA = "INAPP_PURCHASE_DATA"; public static final String RESPONSE_INAPP_SIGNATURE = "INAPP_DATA_SIGNATURE"; public static final String RESPONSE_INAPP_ITEM_LIST = "INAPP_PURCHASE_ITEM_LIST"; public static final String RESPONSE_INAPP_PURCHASE_DATA_LIST = "INAPP_PURCHASE_DATA_LIST"; public static final String RESPONSE_INAPP_SIGNATURE_LIST = "INAPP_DATA_SIGNATURE_LIST"; public static final String INAPP_CONTINUATION_TOKEN = "INAPP_CONTINUATION_TOKEN"; // Item types public static final String ITEM_TYPE_INAPP = "inapp"; public static final String ITEM_TYPE_SUBS = "subs"; // some fields on the getSkuDetails response bundle public static final String GET_SKU_DETAILS_ITEM_LIST = "ITEM_ID_LIST"; public static final String GET_SKU_DETAILS_ITEM_TYPE_LIST = "ITEM_TYPE_LIST"; private static final String KEY_FACTORY_ALGORITHM = "RSA"; private static final String SIGNATURE_ALGORITHM = "SHA1withRSA"; private static String tag = "IAP:ru/zzzzzzerg"; private static String license = ""; public static IInAppBillingService iapService; public static ServiceConnection iapServiceConnection; public static boolean setupDone; public static HaxeObject buyCallback; public static int requestCode; public static String packageName; public static void createService(Context ctx, String licensePublicKey) { iapService = null; iapServiceConnection = null; packageName = ctx.getPackageName(); license = licensePublicKey; setupDone = false; buyCallback = null; iapServiceConnection = new ServiceConnection() { @Override public void onServiceDisconnected(ComponentName name) { iapService = null; Log.i(tag, "Billing service disconnected"); } @Override public void onServiceConnected(ComponentName name, IBinder service) { iapService = IInAppBillingService.Stub.asInterface(service); Log.i(tag, "Billing service connected"); setupDone = true; try { int response = iapService.isBillingSupported(3, packageName, ITEM_TYPE_INAPP); if(response != BILLING_RESPONSE_RESULT_OK) { Log.i(tag, "Billing is not supported: " + response); iapService = null; } else { Log.i(tag, "Billing supported"); } } catch(RemoteException e) { e.printStackTrace(); iapService = null; } } }; Log.d(tag, "Create service intent"); Intent serviceIntent = new Intent( "com.android.vending.billing.InAppBillingService.BIND"); if(!ctx.getPackageManager().queryIntentServices(serviceIntent, 0).isEmpty()) { Log.d(tag, "Bind service"); ctx.bindService(serviceIntent, iapServiceConnection, Context.BIND_AUTO_CREATE); } else { Log.i(tag, "No service for intent"); setupDone = true; } } public static void destroyService(Context ctx) { if(iapServiceConnection != null) { Log.d(tag, "Unbind service"); ctx.unbindService(iapServiceConnection); iapServiceConnection = null; } } public static void getItems(HaxeObject callback) { try { if(!setupDone || iapService == null) { callback.call("onWarning", new Object[] {"Service is not ready", "getItems"}); return; } Log.d(tag, "binded service"); ArrayList skuList = new ArrayList(); skuList.add("android.test.purchased"); skuList.add("android.test.canceled"); skuList.add("android.test.refunded"); skuList.add("android.test.item_unavailable"); Log.d(tag, "create query bundle"); Bundle query = new Bundle(); query.putStringArrayList("ITEM_ID_LIST", skuList); Log.d(tag, "get sku details: " + packageName); Bundle details = iapService.getSkuDetails(3, packageName, ITEM_TYPE_INAPP, query); if(!details.containsKey(RESPONSE_GET_SKU_DETAILS_LIST)) { int response = getResponseCode(details); if(response != BILLING_RESPONSE_RESULT_OK) { callback.call("onError", new Object[] {response, "getItems"}); } else { callback.call("onWarning", new Object[] {"No detail list", "getItems"}); } return; } Log.d(tag, "got sku details"); ArrayList<String> res = details.getStringArrayList(RESPONSE_GET_SKU_DETAILS_LIST); for(String r : res) { callback.call("addProduct", new Object[] {r}); } } catch(RemoteException e) { callback.call("onException", new Object[] {e.toString(), "getItems"}); } finally { callback.call("finish", new Object[] {}); } } public static void getPurchases(HaxeObject callback) { if(!setupDone || iapService == null) { callback.call("onWarning", new Object[] {"Service is not ready", "getPurchases"}); return; } try { String continueToken = null; do { Bundle items = iapService.getPurchases(3, packageName, ITEM_TYPE_INAPP, continueToken); int response = getResponseCode(items); if(response != BILLING_RESPONSE_RESULT_OK) { callback.call("onError", new Object[] {response, "getPurchases"}); return; } if(!items.containsKey(RESPONSE_INAPP_ITEM_LIST) || !items.containsKey(RESPONSE_INAPP_PURCHASE_DATA_LIST) || !items.containsKey(RESPONSE_INAPP_SIGNATURE_LIST)) { callback.call("onWarning", new Object[] {"Bundle doesn't contains required fields", "getPurchases"}); return; } ArrayList<String> skus = items.getStringArrayList(RESPONSE_INAPP_ITEM_LIST); ArrayList<String> purchases = items.getStringArrayList(RESPONSE_INAPP_PURCHASE_DATA_LIST); ArrayList<String> signatures = items.getStringArrayList(RESPONSE_INAPP_SIGNATURE_LIST); for(int i = 0, cnt = purchases.size(); i < cnt; ++i) { String data = purchases.get(i); String sku = skus.get(i); String signature = signatures.get(i); if(verify(license, data, signature, callback)) { callback.call("addPurchase", new Object[] {data}); } else { callback.call("onWarning", new Object[] {"Purchase signature verification failed", "getPurchases"}); } } continueToken = items.getString(INAPP_CONTINUATION_TOKEN); } while(!TextUtils.isEmpty(continueToken)); } catch(Exception e) { callback.call("onException", new Object[] {e.toString(), "getPurchases"}); } finally { callback.call("finish", new Object[]{}); } } public static void consumeItem(String sku, String token, HaxeObject callback) { try { if(!setupDone || iapService == null) { callback.call("onWarning", new Object[] {"Service is not ready", "consumeItem"}); return; } int response = iapService.consumePurchase(3, packageName, token); if(response == BILLING_RESPONSE_RESULT_OK) { callback.call("consumed", new Object[] {sku}); } else { callback.call("onError", new Object[] {response, "consumeItem"}); } } catch(RemoteException e) { callback.call("onException", new Object[] {e.toString(), "consumeItem"}); } finally { callback.call("finish", new Object[]{}); } } public static void purchaseItem(String sku, int rc, HaxeObject callback) { try { if(!setupDone || iapService == null) { callback.call("onWarning", new Object[] {"Service is not ready", "purchaseItem"}); return; } if(buyCallback != null) { callback.call("onWarning", new Object[] {"Purchase item is not end", "purchaseItem"}); return; } Log.d(tag, "getBuyIntent for " + sku); Bundle bundle = iapService.getBuyIntent(3, packageName, sku, ITEM_TYPE_INAPP, ""); int response = getResponseCode(bundle); if(response != BILLING_RESPONSE_RESULT_OK) { callback.call("onError", new Object[] {response, "purchaseItem"}); return; } buyCallback = callback; requestCode = rc; PendingIntent intent = bundle.getParcelable(RESPONSE_BUY_INTENT); Log.d(tag, "Start intent for " + sku + " with request code " + requestCode); Activity activity = GameActivity.getInstance(); activity.startIntentSenderForResult(intent.getIntentSender(), requestCode, new Intent(), Integer.valueOf(0), Integer.valueOf(0), Integer.valueOf(0)); Log.d(tag, "Intent for " + sku + " with request code " + requestCode + "started"); } catch(Exception e) { callback.call("onException", new Object[] {e.toString(), "purchaseItem"}); if(buyCallback != null) { buyCallback.call("finish", new Object[]{}); } buyCallback = null; } } public static boolean handleActivityResult(int rc, int resultCode, Intent data) { Log.d(tag, "handleActivityResult"); if(rc != requestCode) { return false; } if(buyCallback == null) { return false; } try { if(data == null) { String msg = "Null data for purchase activity result"; buyCallback.call("onWarning", new Object[] {msg, "handleActivityResult"}); return true; } int response = getResponseCode(data.getExtras()); if(resultCode == Activity.RESULT_OK && response == BILLING_RESPONSE_RESULT_OK) { String purchaseData = data.getStringExtra(RESPONSE_INAPP_PURCHASE_DATA); String signature = data.getStringExtra(RESPONSE_INAPP_SIGNATURE); if(purchaseData == null || signature == null) { String d = data.getExtras().toString(); buyCallback.call("onWarning", new Object[] {"Data or Signature is null: " + d, "handleActivityResult"}); } else if(verify(license, purchaseData, signature, buyCallback)) { buyCallback.call("purchased", new Object[] {purchaseData}); } } else if(resultCode == Activity.RESULT_OK) { buyCallback.call("onError", new Object[] {response, "handleActivityResult"}); } else if(resultCode == Activity.RESULT_CANCELED) { buyCallback.call("canceled", new Object[] {response}); } else { buyCallback.call("onError", new Object[] {resultCode, "handleActivityResult"}); } return true; } finally { if(buyCallback != null) { buyCallback.call("finish", new Object[]{}); } buyCallback = null; } } static boolean verify(String publicKey, String data, String signature, HaxeObject callback) { if(!TextUtils.isEmpty(signature)) { try { byte[] decodedKey = Base64.decode(publicKey, Base64.DEFAULT); KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM); PublicKey key = keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey)); Signature sig = Signature.getInstance(SIGNATURE_ALGORITHM); sig.initVerify(key); sig.update(data.getBytes()); if(!sig.verify(Base64.decode(signature, Base64.DEFAULT))) { callback.call("onWarning", new Object[] {"Signature verification failed", "verify"}); return false; } } catch(Exception e) { callback.call("onException", new Object[] {e.toString(), "verify"}); return false; } } return true; } // Workaround to bug where sometimes response codes come as Long instead of Integer static int getResponseCode(Bundle b) { Object o = b.get(RESPONSE_CODE); if (o == null) { Log.d(tag, "Bundle with null response code, assuming OK (known issue)"); return 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.d(tag, "Unexpected type for bundle response code."); Log.d(tag, o.getClass().getName()); return BILLING_RESPONSE_RESULT_ERROR; } } }