package com.defold.iap; import java.text.SimpleDateFormat; import java.util.Map; import java.util.Set; import java.util.HashMap; import java.util.HashSet; import java.util.Date; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import org.json.JSONException; import org.json.JSONObject; import android.app.Activity; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; import android.util.Log; import com.amazon.device.iap.PurchasingService; import com.amazon.device.iap.PurchasingListener; import com.amazon.device.iap.model.ProductDataResponse; import com.amazon.device.iap.model.PurchaseUpdatesResponse; import com.amazon.device.iap.model.PurchaseResponse; import com.amazon.device.iap.model.UserDataResponse; import com.amazon.device.iap.model.RequestId; import com.amazon.device.iap.model.Product; import com.amazon.device.iap.model.Receipt; import com.amazon.device.iap.model.UserData; import com.amazon.device.iap.model.FulfillmentResult; public class IapAmazon implements PurchasingListener { public static final String TAG = "iap"; private HashMap listProductsListeners; private HashMap listProductsCommandPtrs; private HashMap purchaseListeners; private Activity activity; private boolean autoFinishTransactions; public IapAmazon(Activity activity, boolean autoFinishTransactions) { this.activity = activity; this.autoFinishTransactions = autoFinishTransactions; this.listProductsListeners = new HashMap(); this.purchaseListeners = new HashMap(); PurchasingService.registerListener(activity, this); } private void init() { } public void stop() { } public void listItems(final String skus, final IListProductsListener listener, final long commandPtr) { final Set skuSet = new HashSet(); for (String x : skus.split(",")) { if (x.trim().length() > 0) { if (!skuSet.contains(x)) { skuSet.add(x); } } } // It might seem unconventional to hold the lock while doing the function call, // but it prevents a race condition, as the API does not allow supplying own // requestId which could be generated ahead of time. synchronized (listProductsListeners) { RequestId req = PurchasingService.getProductData(skuSet); if (req != null) { listProductsListeners.put(req, listener); listProductsCommandPtrs.put(req, commandPtr); } else { Log.e(TAG, "Did not expect a null requestId"); } } } public void buy(final String product, final IPurchaseListener listener) { synchronized (purchaseListeners) { RequestId req = PurchasingService.purchase(product); if (req != null) { purchaseListeners.put(req, listener); } else { Log.e(TAG, "Did not expect a null requestId"); } } } public void finishTransaction(final String receipt, final IPurchaseListener listener) { if(this.autoFinishTransactions) { return; } PurchasingService.notifyFulfillment(receipt, FulfillmentResult.FULFILLED); } private void doGetPurchaseUpdates(final IPurchaseListener listener, final boolean reset) { synchronized (purchaseListeners) { RequestId req = PurchasingService.getPurchaseUpdates(reset); if (req != null) { purchaseListeners.put(req, listener); } else { Log.e(TAG, "Did not expect a null requestId"); } } } public void processPendingConsumables(final IPurchaseListener listener) { // reset = false means getting any new receipts since the last call. doGetPurchaseUpdates(listener, false); } public void restore(final IPurchaseListener listener) { // reset = true means getting all transaction history, although consumables // are not included, only entitlements, after testing. doGetPurchaseUpdates(listener, true); } public static String toISO8601(final Date date) { String formatted = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").format(date); return formatted.substring(0, 22) + ":" + formatted.substring(22); } private JSONObject makeTransactionObject(final UserData user, final Receipt receipt, int state) throws JSONException { JSONObject transaction = new JSONObject(); transaction.put("ident", receipt.getSku()); transaction.put("state", state); transaction.put("date", toISO8601(receipt.getPurchaseDate())); transaction.put("trans_ident", receipt.getReceiptId()); transaction.put("receipt", receipt.getReceiptId()); // Only for amazon (this far), but required for using their server side receipt validation. transaction.put("is_sandbox_mode", PurchasingService.IS_SANDBOX_MODE); transaction.put("user_id", user.getUserId()); // According to documentation, cancellation support has to be enabled per item, and this is // not officially supported by any other IAP provider, and it is not expected to be used here either. // // But enforcing the use of only non-cancelable items is not possible either; so include these flags // for completeness. if (receipt.getCancelDate() != null) transaction.put("cancel_date", toISO8601(receipt.getCancelDate())); transaction.put("canceled", receipt.isCanceled()); return transaction; } // This callback method is invoked when an ProductDataResponse is available for a request initiated by PurchasingService.getProductData(java.util.Set). @Override public void onProductDataResponse(ProductDataResponse productDataResponse) { RequestId reqId = productDataResponse.getRequestId(); IListProductsListener listener; long commadPtr = 0; synchronized (this.listProductsListeners) { listener = this.listProductsListeners.get(reqId); commadPtr = this.listProductsCommandPtrs.get(reqId); this.listProductsListeners.remove(reqId); this.listProductsCommandPtrs.remove(reqId); if (listener == null) { Log.e(TAG, "No listener found for request " + reqId.toString()); return; } } if (productDataResponse.getRequestStatus() != ProductDataResponse.RequestStatus.SUCCESSFUL) { listener.onProductsResult(IapJNI.BILLING_RESPONSE_RESULT_ERROR, null, commadPtr); } else { Map products = productDataResponse.getProductData(); try { JSONObject data = new JSONObject(); for (Map.Entry entry : products.entrySet()) { String key = entry.getKey(); Product product = entry.getValue(); JSONObject item = new JSONObject(); item.put("ident", product.getSku()); item.put("title", product.getTitle()); item.put("description", product.getDescription()); if (product.getPrice() != null) { String priceString = product.getPrice(); item.put("price_string", priceString); // Based on return values from getPrice: https://developer.amazon.com/public/binaries/content/assets/javadoc/in-app-purchasing-api/com/amazon/inapp/purchasing/item.html item.put("price", priceString.replaceAll("[^0-9.,]", "")); } data.put(key, item); } listener.onProductsResult(IapJNI.BILLING_RESPONSE_RESULT_OK, data.toString(), commadPtr); } catch (JSONException e) { listener.onProductsResult(IapJNI.BILLING_RESPONSE_RESULT_ERROR, null, commadPtr); } } } // Convenience function for getting and removing a purchaseListener (used for more than one operation). private IPurchaseListener pickPurchaseListener(RequestId requestId) { synchronized (this.purchaseListeners) { IPurchaseListener listener = this.purchaseListeners.get(requestId); if (listener != null) { this.purchaseListeners.remove(requestId); return listener; } } return null; } // This callback method is invoked when a PurchaseResponse is available for a purchase initiated by PurchasingService.purchase(String). @Override public void onPurchaseResponse(PurchaseResponse purchaseResponse) { IPurchaseListener listener = pickPurchaseListener(purchaseResponse.getRequestId()); if (listener == null) { Log.e(TAG, "No listener found for request: " + purchaseResponse.getRequestId().toString()); return; } int code; String data = null; String fulfilReceiptId = null; switch (purchaseResponse.getRequestStatus()) { case SUCCESSFUL: { try { code = IapJNI.BILLING_RESPONSE_RESULT_OK; data = makeTransactionObject(purchaseResponse.getUserData(), purchaseResponse.getReceipt(), IapJNI.TRANS_STATE_PURCHASED).toString(); fulfilReceiptId = purchaseResponse.getReceipt().getReceiptId(); } catch (JSONException e) { Log.e(TAG, "JSON Exception occured: " + e.toString()); code = IapJNI.BILLING_RESPONSE_RESULT_DEVELOPER_ERROR; } } break; case ALREADY_PURCHASED: code = IapJNI.BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED; break; case INVALID_SKU: code = IapJNI.BILLING_RESPONSE_RESULT_ITEM_UNAVAILABLE; break; case FAILED: case NOT_SUPPORTED: default: code = IapJNI.BILLING_RESPONSE_RESULT_ERROR; break; } listener.onPurchaseResult(code, data); if (fulfilReceiptId != null && autoFinishTransactions) { PurchasingService.notifyFulfillment(fulfilReceiptId, FulfillmentResult.FULFILLED); } } // This callback method is invoked when a PurchaseUpdatesResponse is available for a request initiated by PurchasingService.getPurchaseUpdates(boolean). @Override public void onPurchaseUpdatesResponse(PurchaseUpdatesResponse purchaseUpdatesResponse) { // The documentation seems to be a little misguiding regarding how to handle this. // This call is in response to getPurchaseUpdates() which can be called in two modes // // 1) Get all receipts since last call (reset = true) // 2) Get the whole transaction history. // // The result can carry the flag hasMore() where it is required to call getPurchaseUpdates again. See docs: // https://developer.amazon.com/public/apis/earn/in-app-purchasing/docs-v2/implementing-iap-2.0 // // Examples indicate it should be called with the same value for 'reset' the secon time around // but actual testing ends up in an infinite loop where the same results are returned over and over. // // So here getPurchaseUpdates is called with result=false to fetch the next round of receipts. RequestId reqId = purchaseUpdatesResponse.getRequestId(); IPurchaseListener listener = pickPurchaseListener(reqId); if (listener == null) { Log.e(TAG, "No listener found for request " + reqId.toString()); return; } switch (purchaseUpdatesResponse.getRequestStatus()) { case SUCCESSFUL: { try { for (Receipt receipt : purchaseUpdatesResponse.getReceipts()) { JSONObject trans = makeTransactionObject(purchaseUpdatesResponse.getUserData(), receipt, IapJNI.TRANS_STATE_PURCHASED); listener.onPurchaseResult(IapJNI.BILLING_RESPONSE_RESULT_OK, trans.toString()); if(autoFinishTransactions) { PurchasingService.notifyFulfillment(receipt.getReceiptId(), FulfillmentResult.FULFILLED); } } if (purchaseUpdatesResponse.hasMore()) { doGetPurchaseUpdates(listener, false); } } catch (JSONException e) { Log.e(TAG, "JSON Exception occured: " + e.toString()); listener.onPurchaseResult(IapJNI.BILLING_RESPONSE_RESULT_DEVELOPER_ERROR, null); } } break; case FAILED: case NOT_SUPPORTED: default: listener.onPurchaseResult(IapJNI.BILLING_RESPONSE_RESULT_ERROR, null); break; } } // This callback method is invoked when a UserDataResponse is available for a request initiated by PurchasingService.getUserData(). @Override public void onUserDataResponse(UserDataResponse userDataResponse) { // Intentionally left un-implemented; not used. } }