package com.unity3d.player; import android.app.Activity; import android.content.Intent; import android.net.Uri; import android.os.Handler; import android.os.Looper; import android.text.TextUtils; 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.nio.charset.StandardCharsets; 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; import java.util.concurrent.atomic.AtomicBoolean; public class FlexionAdapter extends ChannelAdapterBase implements SDKManager.IPaymentAdapter, SDKManager.IProductQueryAdapter, SDKManager.IPendingPurchaseAdapter, SDKManager.IBillingLifecycleAdapter, SDKManager.IChannelInfoAdapter, SDKManager.IReviewAdapter { private static final String TAG = "FlexionAdapter"; private static final int FLX_INIT_MAX_ATTEMPTS = 3; private static final int QUERY_PURCHASES_SUCCESS_CODE = 5000; private static BillingService billingService; private static boolean billingConnected = false; private static boolean billingConnecting = false; private static boolean tuyooSdkInitialized = false; private static boolean flexionInitialized = false; private static boolean flexionScreensStarted = false; private static boolean flexionScreensShown = 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 Object pendingQueryLock = new Object(); private static boolean pendingPurchaseQueryInProgress = false; private static boolean pendingPurchaseCacheReady = false; private static boolean pendingPurchaseUnityCallbackWaiting = false; private static final ExecutorService billingExecutor = Executors.newSingleThreadExecutor(r -> new Thread(r, "FlexionBillingThread")); private interface BillingReadyOperation { void run(BillingService service); } private static class Config { public String SDK_CLIENTID = "Android_5.00_tyGuest,facebook.googleplay.0-hall20587.flexion.FishingMaster"; } 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() { // Log.i(TAG, "[FLX-INIT] adapter init flexionInitialized=" + flexionInitialized // + " flexionScreensStarted=" + flexionScreensStarted // + " flexionScreensShown=" + flexionScreensShown); activity.runOnUiThread(() -> { SDKAPI.getIns().lightModeEnable(); if (ensureTuYooSdkInitialized(activity)) { if (flexionInitialized) { notifyFlexionInitResult(true, "ok"); showFlexionScreensThenInitBilling(activity); } else { initFlexionWithRetry(activity, 1); } } }); } private static boolean ensureTuYooSdkInitialized(Activity activity) { if (tuyooSdkInitialized) { // Log.i(TAG, "[FLX-INIT] TuYoo SDK already initialized, skip SDKAPI.init"); return true; } try { // Log.i(TAG, "[FLX-INIT] TuYoo SDK init start"); 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); tuyooSdkInitialized = true; // Log.i(TAG, "[FLX-INIT] TuYoo SDK init success"); return 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()); return false; } } private static void initFlexionWithRetry(Activity activity, int attempt) { if (!isActivityAlive(activity)) { Log.w(TAG, "FLX.init skipped: activity not alive"); SDKManager.notifyInitResult(false, "flx_init_activity_not_alive"); return; } // Log.i(TAG, "[FLX-INIT] FLX.init start attempt=" + attempt + "/" + FLX_INIT_MAX_ATTEMPTS); try { FLX.init(activity, (initCode, initMsg) -> { // Log.i(TAG, "[FLX-INIT] FLX.init result=" + initCode // + " msg=" + initMsg // + " attempt=" + attempt + "/" + FLX_INIT_MAX_ATTEMPTS); if (initCode == 0) { flexionInitialized = true; notifyFlexionInitResult(true, "ok"); showFlexionScreensThenInitBilling(activity); return; } onFlexionInitFailed(activity, attempt, "code=" + initCode + " msg=" + initMsg); }); } catch (Exception e) { Log.e(TAG, "FLX.init exception attempt=" + attempt + "/" + FLX_INIT_MAX_ATTEMPTS + " error=" + e.getMessage()); onFlexionInitFailed(activity, attempt, "exception=" + e.getMessage()); } } private static void onFlexionInitFailed(Activity activity, int attempt, String detail) { if (attempt < FLX_INIT_MAX_ATTEMPTS) { long delayMs = attempt * 1000L; Log.w(TAG, "FLX.init failed, retry after " + delayMs + "ms" + " attempt=" + attempt + "/" + FLX_INIT_MAX_ATTEMPTS + " detail=" + detail); mainHandler.postDelayed(() -> initFlexionWithRetry(activity, attempt + 1), delayMs); return; } Log.e(TAG, "FLX.init failed after retry: " + detail); TYUnityActivity.setSdkInited(); SDKManager.notifyInitResult(false, "flx_init_failed_after_retry: " + detail); } private static void showFlexionScreensThenInitBilling(Activity activity) { if (flexionScreensShown) { // Log.i(TAG, "[FLX-INIT] showFlexionScreens skipped, already shown"); initBillingServiceAfterScreens(activity); return; } if (flexionScreensStarted) { Log.w(TAG, "[FLX-INIT] showFlexionScreens already started without result, skip duplicate show"); return; } // Log.i(TAG, "[FLX-INIT] showFlexionScreens start"); flexionScreensStarted = true; FLX.showFlexionScreens(activity, (screenCode, screenMsg) -> { // Log.i(TAG, "[FLX-INIT] showFlexionScreens result=" + screenCode // + " msg=" + screenMsg); flexionScreensShown = true; initBillingServiceAfterScreens(activity); }); } private static void initBillingServiceAfterScreens(Activity activity) { // Log.i(TAG, "[FLX-INIT] initBillingServiceAfterScreens start"); initBillingService(activity, () -> { // Log.i(TAG, "[FLX-INIT] initBillingServiceAfterScreens success"); }, () -> { Log.w(TAG, "[FLX-INIT] initBillingServiceAfterScreens failed"); }); } private static void notifyFlexionInitResult(boolean success, String msg) { TYUnityActivity.setSdkInited(); SDKManager.notifyInitResult(success, msg); } private static boolean isActivityAlive(Activity activity) { return activity != null && !activity.isFinishing() && !activity.isDestroyed(); } @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, StandardCharsets.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("customData", customData); } payload.put("prodPrice", productPrice); payload.put("prodCount", productCount); developerPayload = Base64.getEncoder().encodeToString(payload.toString().getBytes(StandardCharsets.UTF_8)); // 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) { List ids = Arrays.asList(productIds.split(",")); Log.i(TAG, "[FLEXION-SKU-PRICE] queryProducts productIds=" + productIds + " count=" + ids.size() + " billingConnected=" + billingConnected + " billingReady=" + (billingService != null && billingService.isReady())); ensureBillingReady(null, "queryProducts", bs -> queryProductDetails(bs, ids), () -> sendExtensionError("billing_not_ready")); } private static void initBillingService(Activity activity) { initBillingService(activity, null, null); } private static void initBillingService(Activity activity, Runnable onReady, Runnable onFail) { // Log.i(TAG, "[PAY-FLOW] initBillingService"); ensureBillingReady(activity, "initBillingService", bs -> { queryPendingPurchases(false); if (onReady != null) onReady.run(); }, () -> { Log.w(TAG, "[PAY-FLOW] initBillingService failed: billing not ready"); if (onFail != null) onFail.run(); }); } 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(false); } } @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(boolean sendUnityCallback) { if (sendUnityCallback) { JSONArray cached = drainCachedPendingPurchases(); boolean cacheReady = isPendingPurchaseCacheReady(); if (cached.length() > 0 || cacheReady) { // Log.i(TAG, "[PAY-FLOW] queryPendingPurchases use cached result count=" + cached.length() // + " cacheReady=" + cacheReady); markPendingPurchaseCacheConsumed(cached); sendPendingPurchasesCallback(0, null, cached); return; } } synchronized (pendingQueryLock) { if (sendUnityCallback) { pendingPurchaseUnityCallbackWaiting = true; } if (pendingPurchaseQueryInProgress) { // Log.i(TAG, "[PAY-FLOW] queryPendingPurchases wait running query" // + " sendUnityCallback=" + sendUnityCallback // + " cacheReady=" + pendingPurchaseCacheReady); return; } pendingPurchaseQueryInProgress = true; pendingPurchaseCacheReady = false; } // Log.i(TAG, "[PAY-FLOW] queryPendingPurchases start: billingService=" + billingService // + " billingConnected=" + billingConnected // + " sendUnityCallback=" + sendUnityCallback); ensureBillingReady(null, "queryPendingPurchases", bs -> { billingExecutor.execute(() -> { try { bs.queryPurchasesAsync( new QueryPurchasesParams("inapp"), (result, purchases) -> { int responseCode = result == null ? -1 : result.getResponseCode(); String debugMessage = result == null ? "billing_result_null" : result.getDebugMessage(); // Log.i(TAG, "[PAY-FLOW] queryPendingPurchases result: code=" + responseCode // + " debugMsg=" + debugMessage // + " count=" + (purchases == null ? 0 : purchases.size())); if (responseCode != QUERY_PURCHASES_SUCCESS_CODE) { boolean shouldCallback = finishPendingQueryFailed(); if (shouldCallback) { sendPendingPurchasesCallback(1, "query_pending_failed_" + responseCode + ": " + debugMessage, new JSONArray()); } return; } JSONArray response = new JSONArray(); if (purchases != null) { for (Purchase p : purchases) { JSONObject item = pendingPurchaseToJson(p); if (item != null) response.put(item); } } boolean shouldCallback = finishPendingQuerySuccess(response); if (shouldCallback) { sendPendingPurchasesCallback(0, null, response); } }); } catch (Exception e) { Log.e(TAG, "[PAY-FLOW] queryPendingPurchases exception: " + e.getMessage()); boolean shouldCallback = finishPendingQueryFailed(); if (shouldCallback) { sendPendingPurchasesCallback(1, "query_pending_exception: " + e.getMessage(), new JSONArray()); } } }); }, () -> { // Log.w(TAG, "[PAY-FLOW] queryPendingPurchases skipped: billing not connected"); boolean shouldCallback = finishPendingQueryFailed(); if (shouldCallback) sendPendingPurchasesCallback(1, "billing_not_ready", new JSONArray()); }); } @Override public void queryPendingPurchases() { queryPendingPurchases(true); } private static boolean finishPendingQuerySuccess(JSONArray purchases) { synchronized (pendingQueryLock) { boolean shouldCallback = pendingPurchaseUnityCallbackWaiting; if (shouldCallback) { clearCachedPendingPurchases(); } else { replaceCachedPendingPurchases(purchases); } pendingPurchaseQueryInProgress = false; pendingPurchaseCacheReady = !shouldCallback || purchases == null || purchases.length() == 0; pendingPurchaseUnityCallbackWaiting = false; return shouldCallback; } } private static boolean finishPendingQueryFailed() { synchronized (pendingQueryLock) { pendingPurchaseQueryInProgress = false; pendingPurchaseCacheReady = false; boolean shouldCallback = pendingPurchaseUnityCallbackWaiting; pendingPurchaseUnityCallbackWaiting = false; return shouldCallback; } } private static boolean isPendingPurchaseCacheReady() { synchronized (pendingQueryLock) { return pendingPurchaseCacheReady; } } private static void markPendingPurchaseCacheConsumed(JSONArray purchases) { if (purchases == null || purchases.length() == 0) return; synchronized (pendingQueryLock) { pendingPurchaseCacheReady = false; } } private static JSONObject pendingPurchaseToJson(Purchase purchase) { if (purchase == null || purchase.getPurchaseState() != 0) return null; 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()); return result; } catch (JSONException e) { Log.e(TAG, "[PAY-FLOW] pendingPurchaseToJson error: " + e.getMessage()); return null; } } private static JSONArray drainCachedPendingPurchases() { synchronized (pendingPurchases) { // Log.i(TAG, "[PAY-FLOW] drainCachedPendingPurchases start cachedCount=" + pendingPurchases.size() // + " queryInProgress=" + pendingPurchaseQueryInProgress // + " cacheReady=" + pendingPurchaseCacheReady); JSONArray result = new JSONArray(); for (String item : pendingPurchases) { try { result.put(new JSONObject(item)); } catch (JSONException e) { Log.e(TAG, "[PAY-FLOW] drainCachedPendingPurchases json error: " + e.getMessage()); } } pendingPurchases.clear(); return result; } } private static void replaceCachedPendingPurchases(JSONArray purchases) { synchronized (pendingPurchases) { pendingPurchases.clear(); if (purchases == null) return; for (int i = 0; i < purchases.length(); i++) { JSONObject item = purchases.optJSONObject(i); if (item != null) pendingPurchases.add(item.toString()); } // Log.i(TAG, "[PAY-FLOW] replaceCachedPendingPurchases count=" + pendingPurchases.size()); } } private static void clearCachedPendingPurchases() { synchronized (pendingPurchases) { pendingPurchases.clear(); } } private static void sendPendingPurchasesCallback(int code, String errStr, JSONArray purchases) { try { JSONObject result = new JSONObject(); result.put("code", code); if (errStr != null) result.put("errStr", errStr); result.put("respObj", purchases == null ? "[]" : purchases.toString()); // Log.i(TAG, "[PAY-FLOW] PendingPurchasesResult callback: code=" + code // + " count=" + (purchases == null ? 0 : purchases.length())); UnityPlayer.UnitySendMessage(ConfigManager.UNITY_FACADE_NAME, "PendingPurchasesResult", result.toString()); } catch (JSONException e) { Log.e(TAG, "[PAY-FLOW] sendPendingPurchasesCallback json error: " + e.getMessage()); } } @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 AtomicBoolean completed = new AtomicBoolean(false); Log.i(TAG, "[FLEXION-SKU-PRICE] queryProductDetails start count=" + productIds.size() + " productIds=" + productIds + " serviceReady=" + (service != null && service.isReady())); mainHandler.postDelayed(() -> { if (!completed.compareAndSet(false, true)) return; Log.w(TAG, "[PAY-FLOW] queryProductDetails timeout count=" + productIds.size()); sendExtensionError("flexion_query_timeout"); }, 30000); billingExecutor.execute(() -> { try { service.queryProductDetailsAsync( new ProductDetailsParams("inapp", productIds), (billingResult, productDetails) -> { if (!completed.compareAndSet(false, true)) { Log.w(TAG, "[PAY-FLOW] queryProductDetails callback ignored after timeout"); return; } Log.i(TAG, "[FLEXION-SKU-PRICE] queryProductDetails callback code=" + (billingResult == null ? "null" : billingResult.getResponseCode()) + " debugMsg=" + (billingResult == null ? "billing_result_null" : billingResult.getDebugMessage()) + " count=" + (productDetails == null ? 0 : productDetails.size())); try { JSONObject result = new JSONObject(); if (billingResult != null && 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()); Log.i(TAG, "[FLEXION-SKU-PRICE] sdk product id=" + detail.getId() + " title=" + detail.getTitle() + " price=" + detail.getPrice() + " priceAmountMicros=" + detail.getPriceAmountMicros() + " currency=" + detail.getPriceCurrencyCode() + " type=" + detail.getType()); arr.put(item); } result.put("code", 0); result.put("respObj", arr.toString()); Log.i(TAG, "[FLEXION-SKU-PRICE] send Unity SKUListResult respObj=" + arr.toString()); } else { result.put("code", 1); result.put("thirdExtend_errStr", "flexion_query_failed: " + (billingResult == null ? "billing_result_null" : 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}"); } }); } catch (Exception e) { if (!completed.compareAndSet(false, true)) return; Log.e(TAG, "[PAY-FLOW] queryProductDetails exception: " + e.getMessage()); sendExtensionError("flexion_query_exception: " + e.getMessage()); } }); } 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 -> billingExecutor.execute(() -> { try { bs.consumeAsync( new ConsumeParams(token), r -> { // Log.i(TAG, "[PAY-FLOW] consume result: " + // (r.getResponseCode() == BillingResults.ResultCode.CONSUME_SUCCESS_CODE ? "ok" : "fail")); }); } catch (Exception e) { Log.e(TAG, "[PAY-FLOW] consumePurchase exception: " + e.getMessage()); } }), () -> Log.w(TAG, "[PAY-FLOW] consumePurchase skipped: billing not connected")); } public static void cancelPurchase() { // Log.i(TAG, "[PAY-FLOW] cancelPurchase: TODO - dismiss billing dialog"); } @Override public void review(Activity activity) { String storePageUri = getStorePageUri(activity); Log.w(TAG, "[ReviewDebug] review uri=" + storePageUri); if (TextUtils.isEmpty(storePageUri)) { Log.w(TAG, "review skipped: store page uri is empty"); return; } Activity targetActivity = activity != null ? activity : billingActivity; if (targetActivity == null) targetActivity = UnityPlayer.currentActivity; if (targetActivity == null) { Log.w(TAG, "review skipped: activity is null"); return; } try { targetActivity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(storePageUri))); } catch (Throwable e) { Log.w(TAG, "review exception: " + e.getMessage()); } } @Override public String getStorePageUri(Activity activity) { try { Activity targetActivity = activity != null ? activity : billingActivity; if (targetActivity == null) targetActivity = UnityPlayer.currentActivity; if (targetActivity == null) { Log.w(TAG, "getStorePageUri skipped: activity is null"); return ""; } Object storePageUri = FLX.createChannelInfoService(targetActivity) .getCurrentChannelInfo(targetActivity) .getStorePageUri(); return storePageUri != null ? storePageUri.toString() : ""; } catch (Throwable e) { Log.w(TAG, "getStorePageUri exception: " + e.getMessage()); return ""; } } 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}"); } } }