package com.unity3d.player; import android.app.Activity; import android.os.Handler; import android.os.Looper; import android.util.Log; import com.flexionmobile.ddpx.listener.ConnectionStateListener; import com.flexionmobile.ddpx.listener.PurchasesUpdateListener; import com.flexionmobile.ddpx.model.BillingResult; import com.flexionmobile.ddpx.model.BillingResults; import com.flexionmobile.ddpx.model.Purchase; import com.flexionmobile.ddpx.model.ProductDetails; import com.flexionmobile.ddpx.model.params.BillingFlowParams; import com.flexionmobile.ddpx.model.params.ConsumeParams; import com.flexionmobile.ddpx.model.params.QueryPurchasesParams; import com.flexionmobile.ddpx.model.params.ProductDetailsParams; import com.flexionmobile.ddpx.service.BillingService; import com.flexionmobile.fdk.FLX; import com.tuyoo.gamesdk.api.SDKAPI; import com.tuyoo.gamesdk.model.InitParam; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class FlexionSDKAdapter implements SDKManager.IChannelSDKAdapter, SDKManager.IPaymentAdapter, SDKManager.IProductQueryAdapter, SDKManager.IPendingPurchaseAdapter, SDKManager.IBillingLifecycleAdapter { private static final String TAG = "FlexionSDKAdapter"; private static BillingService billingService; private static boolean billingConnected = false; private static boolean billingConnecting = false; private static Activity billingActivity; private static final Handler mainHandler = new Handler(Looper.getMainLooper()); private static final List pendingPurchases = new ArrayList<>(); private static final List pendingBillingOperations = new ArrayList<>(); private static final ExecutorService billingExecutor = Executors.newSingleThreadExecutor(r -> new Thread(r, "FlexionBillingThread")); private interface BillingReadyOperation { void run(BillingService service); } private static class PendingBillingOperation { final String operation; final BillingReadyOperation onReady; final Runnable onFail; PendingBillingOperation(String operation, BillingReadyOperation onReady, Runnable onFail) { this.operation = operation; this.onReady = onReady; this.onFail = onFail; } } private static final PurchasesUpdateListener purchasesListener = (billingResult, purchase) -> { Log.w(TAG, "[PAY-FLOW] PurchasesUpdateListener: code=" + billingResult.getResponseCode() + " hasPurchase=" + (purchase != null)); if (billingResult.getResponseCode() == BillingResults.ResultCode.PURCHASE_SUCCESS_CODE) { if (purchase != null) handlePurchase(purchase); } else if (billingResult.getResponseCode() == BillingResults.ResultCode.PURCHASE_USER_CANCELLED_CODE) { Log.i(TAG, "[PAY-FLOW] user cancelled"); sendPayCallback(1, "user_cancelled", null, null, null, null); } else { Log.e(TAG, "[PAY-FLOW] purchase failed: code=" + billingResult.getResponseCode()); sendPayCallback(1, "purchase_failed_" + billingResult.getResponseCode(), null, null, null, null); } }; @Override public void init(Activity activity) { activity.runOnUiThread(() -> { SDKAPI.getIns().lightModeEnable(); boolean sdkInitOk = false; try { SDKAPI.getIns().init(new InitParam.Builder() .withActivity(activity) .withAppId(ConfigManager.SDK_APPID) .withClientId(ConfigManager.SDK_CLIENTID) .withServerUrl(ConfigManager.SDK_LOGIN_SERVER_URL) .build()); SDKAPI.getIns().onActivityStart(activity); SDKAPI.getIns().onActivityResume(activity); sdkInitOk = true; } catch (Exception e) { Log.e(TAG, "TuYoo SDK init failed, skip FLX init: " + e); TYUnityActivity.setSdkInited(); SDKManager.notifyInitResult(false, "tuyoo_init_failed: " + e.getMessage()); } if (sdkInitOk) { FLX.init(activity, (initCode, initMsg) -> { Log.i(TAG, "FLX.init result=" + initCode); if (initCode == 0) { FLX.showFlexionScreens(activity, (screenCode, screenMsg) -> { Log.i(TAG, "FLX.showFlexionScreens result=" + screenCode); initBillingService(activity); TYUnityActivity.setSdkInited(); SDKManager.notifyInitResult(true, "ok"); }); } else { Log.e(TAG, "FLX.init failed: " + initCode); TYUnityActivity.setSdkInited(); SDKManager.notifyInitResult(false, "flx_init_failed: " + initCode); } }); } }); } @Override public void pay(Activity activity, String productId, String productPrice, String productName, String productCount, String prodorderId, String appInfo) { Log.i(TAG, "[PAY-FLOW] pay called: productId=" + productId + " billingConnected=" + billingConnected); String developerPayload = ""; try { byte[] decoded = Base64.getDecoder().decode(appInfo); JSONObject json = new JSONObject(new String(decoded, "UTF-8")); JSONObject payload = new JSONObject(); String customData = json.optString("customData", ""); payload.put("orderId", prodorderId); payload.put("userId", json.optString("userId", "")); payload.put("pfid", json.optString("pfid", "")); if (!customData.isEmpty()) { payload.put("customDataB64", Base64.getEncoder().encodeToString(customData.getBytes("UTF-8"))); } payload.put("prodPrice", productPrice); payload.put("prodCount", productCount); developerPayload = payload.toString(); Log.i(TAG, "[PAY-CUSTOMDATA] customData length=" + customData.length() + " developerPayload length=" + developerPayload.length()); } catch (Exception e) { Log.w(TAG, "Failed to extract payload fields from appInfo: " + e.getMessage()); } String finalPayload = developerPayload; ensureBillingReady(activity, "pay", bs -> billingExecutor.execute(() -> { try { if (bs == null || !bs.isReady()) { Log.e(TAG, "[PAY-FLOW] billingService became null or not ready"); sendPayCallback(1, "billing_not_ready", null, null, null, null); return; } Log.i(TAG, "[PAY-FLOW] launching billing flow for: " + productId + " payload=" + finalPayload); BillingResult result = bs.launchBillingFlow(activity, new BillingFlowParams(productId, "inapp", finalPayload)); Log.i(TAG, "[PAY-FLOW] launchBillingFlow result: code=" + result.getResponseCode() + " debugMsg=" + result.getDebugMessage()); if (result.getResponseCode() != 2000) { Log.e(TAG, "[PAY-FLOW] launchBillingFlow error: code=" + result.getResponseCode() + " debugMsg=" + result.getDebugMessage()); sendPayCallback(1, "billing_flow_error_" + result.getResponseCode() + "_" + result.getDebugMessage(), null, null, null, null); } } catch (Exception e) { Log.e(TAG, "[PAY-FLOW] launchBillingFlow exception: " + e.getClass().getName() + " msg=" + e.getMessage()); sendPayCallback(1, "launch_billing_failed: " + e.getMessage(), null, null, null, null); } }), () -> sendPayCallback(1, "billing_not_ready", null, null, null, null)); } @Override public void queryProducts() { // Flexion 渠道不通过 thirdExtend 获取商品列表,由 queryProducts(String) 替代 } @Override public void queryProducts(String productIds) { Log.i(TAG, "queryProducts(productIds) called, billingConnected=" + billingConnected); List ids = Arrays.asList(productIds.split(",")); Log.i(TAG, "queryProductDetails with " + ids.size() + " items from config"); ensureBillingReady(null, "queryProducts", bs -> queryProductDetails(bs, ids), () -> sendExtensionError("billing_not_ready")); } private static void initBillingService(Activity activity) { Log.i(TAG, "[PAY-FLOW] initBillingService"); ensureBillingReady(activity, "initBillingService", bs -> queryPendingPurchases(), () -> Log.w(TAG, "[PAY-FLOW] initBillingService failed: billing not ready")); } private static void ensureBillingReady(Activity activity, String operation, BillingReadyOperation onReady, Runnable onFail) { ensureBillingReady(activity, operation, onReady, onFail, false); } private static void ensureBillingReady(Activity activity, String operation, BillingReadyOperation onReady, Runnable onFail, boolean forceReconnect) { if (activity != null) { billingActivity = activity; } Activity targetActivity = activity != null ? activity : billingActivity; if (targetActivity == null) { Log.e(TAG, "[PAY-FLOW] ensureBillingReady failed: activity is null operation=" + operation); if (onFail != null) onFail.run(); return; } targetActivity.runOnUiThread(() -> { try { if (billingService == null) { Log.i(TAG, "[PAY-FLOW] create BillingService operation=" + operation); billingService = FLX.createBillingService(targetActivity, purchasesListener); } boolean ready = billingService != null && billingService.isReady(); Log.i(TAG, "[PAY-FLOW] ensureBillingReady operation=" + operation + " connected=" + billingConnected + " ready=" + ready + " connecting=" + billingConnecting + " forceReconnect=" + forceReconnect); if (ready && !forceReconnect) { billingConnected = true; if (onReady != null) onReady.run(billingService); return; } pendingBillingOperations.add(new PendingBillingOperation(operation, onReady, onFail)); if (billingConnecting) { Log.i(TAG, "[PAY-FLOW] billing connection already in progress, queued operation=" + operation); return; } billingConnecting = true; Log.i(TAG, "[PAY-FLOW] billing reconnect start operation=" + operation); billingService.startConnection(targetActivity, new ConnectionStateListener() { @Override public void onBillingSetupFinished(BillingResult result) { int code = result.getResponseCode(); Log.i(TAG, "[PAY-FLOW] onBillingSetupFinished: code=" + code); billingConnecting = false; billingConnected = code == BillingResults.ResultCode.CONNECTION_SUCCESS_CODE; drainBillingOperations(billingConnected); if (billingConnected) { queryPendingPurchases(); } } @Override public void onBillingServiceDisconnected() { Log.w(TAG, "[PAY-FLOW] billing service disconnected"); billingConnected = false; } }); mainHandler.postDelayed(() -> { if (!billingConnecting) return; boolean timeoutReady = billingService != null && billingService.isReady(); Log.w(TAG, "[PAY-FLOW] billing reconnect timeout operation=" + operation + " ready=" + timeoutReady); billingConnecting = false; billingConnected = timeoutReady; drainBillingOperations(timeoutReady); }, 10000); } catch (Exception e) { Log.e(TAG, "[PAY-FLOW] ensureBillingReady exception operation=" + operation + " msg=" + e.getMessage()); billingConnecting = false; billingConnected = false; drainBillingOperations(false); } }); } private static void drainBillingOperations(boolean success) { List operations = new ArrayList<>(pendingBillingOperations); pendingBillingOperations.clear(); Log.i(TAG, "[PAY-FLOW] drainBillingOperations success=" + success + " count=" + operations.size()); for (PendingBillingOperation operation : operations) { if (success) { if (operation.onReady != null) operation.onReady.run(billingService); } else { Log.w(TAG, "[PAY-FLOW] billing operation failed operation=" + operation.operation); if (operation.onFail != null) operation.onFail.run(); } } } private static void handlePurchase(Purchase purchase) { Log.i(TAG, "[PAY-FLOW] handlePurchase: state=" + purchase.getPurchaseState() + " orderId=" + purchase.getOrderId() + " token=" + purchase.getToken()); if (purchase.getPurchaseState() == 0) { Log.i(TAG, "[PAY-FLOW] purchase success, sending to Unity for verification"); sendPayCallback(0, "success", purchase.getOrderId(), purchase.getPurchaseJson(), purchase.getSignature(), purchase.getToken()); } else if (purchase.getPurchaseState() == 2) { Log.i(TAG, "[PAY-FLOW] purchase state=2 (pending), skip"); } } private static void queryPendingPurchases() { Log.i(TAG, "[PAY-FLOW] queryPendingPurchases start: billingService=" + billingService + " billingConnected=" + billingConnected); ensureBillingReady(null, "queryPendingPurchases", bs -> bs.queryPurchasesAsync( new QueryPurchasesParams("inapp"), (result, purchases) -> { Log.i(TAG, "[PAY-FLOW] queryPendingPurchases result: code=" + result.getResponseCode() + " count=" + (purchases == null ? 0 : purchases.size())); if (purchases != null) for (Purchase p : purchases) cachePendingPurchase(p); }), () -> Log.w(TAG, "[PAY-FLOW] queryPendingPurchases skipped: billing not connected")); } private static void cachePendingPurchase(Purchase purchase) { if (purchase == null) { Log.w(TAG, "[PAY-FLOW] cachePendingPurchase skipped: purchase is null"); return; } Log.i(TAG, "[PAY-FLOW] cachePendingPurchase state=" + purchase.getPurchaseState() + " orderId=" + purchase.getOrderId() + " token=" + purchase.getToken()); if (purchase.getPurchaseState() != 0) return; try { JSONObject result = new JSONObject(); if (purchase.getOrderId() != null) result.put("orderId", purchase.getOrderId()); if (purchase.getPurchaseJson() != null) result.put("purchaseJson", purchase.getPurchaseJson()); if (purchase.getSignature() != null) result.put("signature", purchase.getSignature()); if (purchase.getToken() != null) result.put("purchaseToken", purchase.getToken()); synchronized (pendingPurchases) { String token = purchase.getToken(); for (String item : pendingPurchases) { JSONObject cached = new JSONObject(item); if (token != null && token.equals(cached.optString("purchaseToken", null))) { Log.i(TAG, "[PAY-FLOW] pending purchase already cached token=" + token); return; } } pendingPurchases.add(result.toString()); } Log.i(TAG, "[PAY-FLOW] cached pending purchase: orderId=" + purchase.getOrderId() + " cachedCount=" + pendingPurchases.size()); } catch (JSONException e) { Log.e(TAG, "[PAY-FLOW] cachePendingPurchase json error: " + e.getMessage()); } } @Override public String getPendingPurchases() { synchronized (pendingPurchases) { Log.i(TAG, "[PAY-FLOW] getPendingPurchases start cachedCount=" + pendingPurchases.size()); JSONArray result = new JSONArray(); for (String item : pendingPurchases) { try { result.put(new JSONObject(item)); } catch (JSONException e) { Log.e(TAG, "[PAY-FLOW] getPendingPurchases json error: " + e.getMessage()); } } pendingPurchases.clear(); Log.i(TAG, "[PAY-FLOW] getPendingPurchases count=" + result.length()); return result.toString(); } } @Override public void refreshBilling() { Log.i(TAG, "[PAY-FLOW] refreshBilling requested"); billingConnected = false; ensureBillingReady(null, "refreshBilling", bs -> Log.i(TAG, "[PAY-FLOW] refreshBilling ready"), () -> Log.w(TAG, "[PAY-FLOW] refreshBilling failed"), true); } private static void queryProductDetails(BillingService service, List productIds) { final boolean[] completed = {false}; mainHandler.postDelayed(() -> { if (completed[0]) return; completed[0] = true; Log.w(TAG, "[PAY-FLOW] queryProductDetails timeout count=" + productIds.size()); sendExtensionError("flexion_query_timeout"); }, 30000); service.queryProductDetailsAsync( new ProductDetailsParams("inapp", productIds), (billingResult, productDetails) -> { if (completed[0]) { Log.w(TAG, "[PAY-FLOW] queryProductDetails callback ignored after timeout"); return; } completed[0] = true; try { JSONObject result = new JSONObject(); if (billingResult.getResponseCode() == BillingResults.ResultCode.QUERY_PRODUCT_DETAILS_SUCCESS_CODE && productDetails != null) { JSONArray arr = new JSONArray(); for (ProductDetails detail : productDetails) { JSONObject item = new JSONObject(); item.put("productId", detail.getId()); item.put("ourProductId", detail.getId()); item.put("title", detail.getTitle()); item.put("description", detail.getDescription()); item.put("price", detail.getPrice()); item.put("price_amount_micros", detail.getPriceAmountMicros()); item.put("price_currency_code", detail.getPriceCurrencyCode()); item.put("type", detail.getType()); arr.put(item); } result.put("code", 0); result.put("respObj", arr.toString()); } else { result.put("code", 1); result.put("thirdExtend_errStr", "flexion_query_failed: " + billingResult.getDebugMessage()); } UnityPlayer.UnitySendMessage(ConfigManager.UNITY_FACADE_NAME, ConfigManager.PAY_TYPE_CALLBACK_FUNCTION_NAME, result.toString()); } catch (JSONException e) { UnityPlayer.UnitySendMessage(ConfigManager.UNITY_FACADE_NAME, ConfigManager.PAY_TYPE_CALLBACK_FUNCTION_NAME, "{\"code\":1}"); } }); } private static void sendPayCallback(int code, String errStr, String storeOrderId, String purchaseJson, String signature, String purchaseToken) { Log.i(TAG, "[PAY-FLOW] sendPayCallback: code=" + code + " errStr=" + errStr + " storeOrderId=" + storeOrderId + " hasPurchaseJson=" + (purchaseJson != null) + " hasSignature=" + (signature != null) + " hasToken=" + (purchaseToken != null)); try { JSONObject result = new JSONObject(); result.put("code", code); result.put("errStr", errStr != null ? errStr : ""); if (storeOrderId != null) result.put("orderId", storeOrderId); if (purchaseJson != null) result.put("purchaseJson", purchaseJson); if (signature != null) result.put("signature", signature); if (purchaseToken != null) result.put("purchaseToken", purchaseToken); Log.i(TAG, "[PAY-FLOW] UnitySendMessage callback: " + result.toString()); UnityPlayer.UnitySendMessage(ConfigManager.UNITY_FACADE_NAME, ConfigManager.PAY_CALLBACK_FUNCTION_NAME, result.toString()); } catch (JSONException e) { UnityPlayer.UnitySendMessage(ConfigManager.UNITY_FACADE_NAME, ConfigManager.PAY_CALLBACK_FUNCTION_NAME, "{\"code\":1,\"errStr\":\"json_error\"}"); } } @Override public void consume(String token) { consumePurchase(token); } public static void consumePurchase(String token) { if (token == null || token.isEmpty()) { Log.w(TAG, "[PAY-FLOW] consumePurchase: token is null"); return; } Log.i(TAG, "[PAY-FLOW] consumePurchase: token=" + token); ensureBillingReady(null, "consumePurchase", bs -> bs.consumeAsync( new ConsumeParams(token), r -> Log.i(TAG, "[PAY-FLOW] consume result: " + (r.getResponseCode() == BillingResults.ResultCode.CONSUME_SUCCESS_CODE ? "ok" : "fail"))), () -> Log.w(TAG, "[PAY-FLOW] consumePurchase skipped: billing not connected")); } public static void cancelPurchase() { Log.i(TAG, "[PAY-FLOW] cancelPurchase: TODO - dismiss billing dialog"); } private static void sendExtensionError(String errStr) { try { JSONObject result = new JSONObject(); result.put("code", 1); result.put("thirdExtend_errStr", errStr); UnityPlayer.UnitySendMessage(ConfigManager.UNITY_FACADE_NAME, ConfigManager.PAY_TYPE_CALLBACK_FUNCTION_NAME, result.toString()); } catch (JSONException e) { UnityPlayer.UnitySendMessage(ConfigManager.UNITY_FACADE_NAME, ConfigManager.PAY_TYPE_CALLBACK_FUNCTION_NAME, "{\"code\":1}"); } } }