package com.unity3d.player; import android.app.Activity; 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 FlexionSDKService implements SDKManager.IChannelSDKAdapter, SDKManager.IPaymentAdapter, SDKManager.IProductQueryAdapter, SDKManager.IPendingPurchaseAdapter { private static final String TAG = "FlexionSDKService"; private static BillingService billingService; private static boolean billingConnected = false; private static final List pendingPurchases = new ArrayList<>(); private static final ExecutorService billingExecutor = Executors.newSingleThreadExecutor(r -> new Thread(r, "FlexionBillingThread")); 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 + " billingReady=" + billingConnected); if (billingService == null || !billingConnected || !billingService.isReady()) { Log.e(TAG, "[PAY-FLOW] billing not ready"); sendPayCallback(1, "billing_not_ready", null, null, null, null); return; } 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; billingExecutor.execute(() -> { try { BillingService bs = billingService; 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); } }); } @Override public void queryProducts() { // Flexion 渠道不通过 thirdExtend 获取商品列表,由 queryProducts(String) 替代 } @Override public void queryProducts(String productIds) { Log.i(TAG, "queryProducts(productIds) called, billingConnected=" + billingConnected); if (billingService == null || !billingConnected) { sendExtensionError("billing_not_ready"); return; } List ids = Arrays.asList(productIds.split(",")); Log.i(TAG, "queryProductDetails with " + ids.size() + " items from config"); queryProductDetails(ids); } private static void initBillingService(Activity activity) { Log.i(TAG, "[PAY-FLOW] initBillingService"); billingService = FLX.createBillingService(activity, purchasesListener); billingService.startConnection(activity, new ConnectionStateListener() { @Override public void onBillingSetupFinished(BillingResult result) { Log.i(TAG, "[PAY-FLOW] onBillingSetupFinished: code=" + result.getResponseCode()); if (result.getResponseCode() == BillingResults.ResultCode.CONNECTION_SUCCESS_CODE) { billingConnected = true; queryPendingPurchases(); } } @Override public void onBillingServiceDisconnected() { Log.w(TAG, "[PAY-FLOW] billing service disconnected"); billingConnected = false; } }); } 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); if (billingService == null || !billingConnected) { Log.w(TAG, "[PAY-FLOW] queryPendingPurchases skipped: billing not connected"); return; } billingService.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); }); } 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(); } } private static void queryProductDetails(List productIds) { billingService.queryProductDetailsAsync( new ProductDetailsParams("inapp", productIds), (billingResult, productDetails) -> { 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 (billingService == null || token == null || token.isEmpty()) { Log.w(TAG, "[PAY-FLOW] consumePurchase: billingService or token is null"); return; } Log.i(TAG, "[PAY-FLOW] consumePurchase: token=" + token); billingService.consumeAsync( new ConsumeParams(token), r -> Log.i(TAG, "[PAY-FLOW] consume result: " + (r.getResponseCode() == BillingResults.ResultCode.CONSUME_SUCCESS_CODE ? "ok" : "fail"))); } 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}"); } } }