diff --git a/ExtraPluginCfgs/Android/flexion/ChannelEntry.java b/ExtraPluginCfgs/Android/flexion/ChannelEntry.java index d7eb824..fdf3275 100644 --- a/ExtraPluginCfgs/Android/flexion/ChannelEntry.java +++ b/ExtraPluginCfgs/Android/flexion/ChannelEntry.java @@ -5,7 +5,7 @@ public class ChannelEntry { public static final String CHANNEL = "Flexion"; public static SDKManager.IChannelSDKAdapter createSDKAdapter() { - return new FlexionSDKService(); + return new FlexionSDKAdapter(); } diff --git a/ExtraPluginCfgs/Android/flexion/FlexionSDKService.java b/ExtraPluginCfgs/Android/flexion/FlexionSDKAdapter.java similarity index 59% rename from ExtraPluginCfgs/Android/flexion/FlexionSDKService.java rename to ExtraPluginCfgs/Android/flexion/FlexionSDKAdapter.java index 73bee1e..690149b 100644 --- a/ExtraPluginCfgs/Android/flexion/FlexionSDKService.java +++ b/ExtraPluginCfgs/Android/flexion/FlexionSDKAdapter.java @@ -1,6 +1,8 @@ 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; @@ -30,17 +32,38 @@ import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -public class FlexionSDKService implements +public class FlexionSDKAdapter implements SDKManager.IChannelSDKAdapter, SDKManager.IPaymentAdapter, SDKManager.IProductQueryAdapter, - SDKManager.IPendingPurchaseAdapter { - private static final String TAG = "FlexionSDKService"; + 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)); @@ -100,12 +123,7 @@ public class FlexionSDKService implements 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; - } + Log.i(TAG, "[PAY-FLOW] pay called: productId=" + productId + " billingConnected=" + billingConnected); String developerPayload = ""; try { byte[] decoded = Base64.getDecoder().decode(appInfo); @@ -127,31 +145,32 @@ public class FlexionSDKService implements 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); - } - }); + 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 @@ -162,36 +181,119 @@ public class FlexionSDKService implements @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); + ensureBillingReady(null, "queryProducts", + bs -> queryProductDetails(bs, ids), + () -> sendExtensionError("billing_not_ready")); } 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(); - } - } + ensureBillingReady(activity, "initBillingService", + bs -> queryPendingPurchases(), + () -> Log.w(TAG, "[PAY-FLOW] initBillingService failed: billing not ready")); + } - @Override - public void onBillingServiceDisconnected() { - Log.w(TAG, "[PAY-FLOW] billing service disconnected"); + 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() @@ -208,17 +310,15 @@ public class FlexionSDKService implements 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); - }); + 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) { @@ -272,10 +372,33 @@ public class FlexionSDKService implements } } - private static void queryProductDetails(List productIds) { - billingService.queryProductDetailsAsync( + @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 @@ -338,15 +461,17 @@ public class FlexionSDKService implements } public static void consumePurchase(String token) { - if (billingService == null || token == null || token.isEmpty()) { - Log.w(TAG, "[PAY-FLOW] consumePurchase: billingService or token is null"); + if (token == null || token.isEmpty()) { + Log.w(TAG, "[PAY-FLOW] consumePurchase: 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"))); + 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() { diff --git a/sdk-intergration/Assets/Editor/ChannelBuildTool.cs b/sdk-intergration/Assets/Editor/ChannelBuildTool.cs index 98aab8a..0ace3e2 100644 --- a/sdk-intergration/Assets/Editor/ChannelBuildTool.cs +++ b/sdk-intergration/Assets/Editor/ChannelBuildTool.cs @@ -61,6 +61,8 @@ public class ChannelBuildTool File.Delete(oldJava); foreach (var oldJava in Directory.GetFiles(TysdkPlugins, "*SDKService.java")) File.Delete(oldJava); + foreach (var oldJava in Directory.GetFiles(TysdkPlugins, "*SDKAdapter.java")) + File.Delete(oldJava); var oldConfig = Path.Combine(TysdkPlugins, "ConfigManager.java"); if (File.Exists(oldConfig)) File.Delete(oldConfig); diff --git a/sdk-intergration/Packages/tysdk/Plugins/Android/SDKManager.java b/sdk-intergration/Packages/tysdk/Plugins/Android/SDKManager.java index 8996aca..8a35b5d 100644 --- a/sdk-intergration/Packages/tysdk/Plugins/Android/SDKManager.java +++ b/sdk-intergration/Packages/tysdk/Plugins/Android/SDKManager.java @@ -76,6 +76,10 @@ public class SDKManager { String getPendingPurchases(); } + public interface IBillingLifecycleAdapter { + void refreshBilling(); + } + private static IChannelSDKAdapter sdkAdapter; public static void queryProductsByIDs(String productIds) { @@ -100,9 +104,16 @@ public class SDKManager { return "[]"; } + public static void RefreshBillingService() { + Log.i(TAG, "RefreshBillingService"); + if (sdkAdapter instanceof IBillingLifecycleAdapter) { + ((IBillingLifecycleAdapter) sdkAdapter).refreshBilling(); + } + } + public static void CancelBillingFlow() { Log.w(TAG, "CancelBillingFlow"); -// FlexionSDKService.cancelPurchase(); +// FlexionSDKAdapter.cancelPurchase(); } // ------------------------------------------------------------------------- diff --git a/sdk-intergration/Packages/tysdk/Runtime/TYSdkFacade.cs b/sdk-intergration/Packages/tysdk/Runtime/TYSdkFacade.cs index 1253bac..1468d5b 100644 --- a/sdk-intergration/Packages/tysdk/Runtime/TYSdkFacade.cs +++ b/sdk-intergration/Packages/tysdk/Runtime/TYSdkFacade.cs @@ -322,6 +322,9 @@ namespace tysdk result.isSuccess = true; result.userId = _accountInfo.userId; SetUserInfo(); +#if UNITY_ANDROID + UnityBridgeFunc.RefreshBillingService(); +#endif } TYSDKCallbackManager.Instance.TryTriggerCallback(result); @@ -414,6 +417,9 @@ namespace tysdk { _accountInfo.AddLinkedAccount(accoutType); _accountInfo.Save(); +#if UNITY_ANDROID + UnityBridgeFunc.RefreshBillingService(); +#endif } return result; } diff --git a/sdk-intergration/Packages/tysdk/Runtime/TYSdkFacade_Pay.cs b/sdk-intergration/Packages/tysdk/Runtime/TYSdkFacade_Pay.cs index 9231c26..c7c7cdc 100644 --- a/sdk-intergration/Packages/tysdk/Runtime/TYSdkFacade_Pay.cs +++ b/sdk-intergration/Packages/tysdk/Runtime/TYSdkFacade_Pay.cs @@ -160,10 +160,18 @@ namespace tysdk var result = new ProductListInfo(); return result; #elif UNITY_ANDROID || UNITY_IOS - var task = TYSDKCallbackManager.Instance.RegisterCallback(); + var task = TYSDKCallbackManager.Instance.RegisterCallback(TimeSpan.FromSeconds(45)); UnityBridgeFunc.GetSKUList(); - var result = await task; - return result; + try + { + var result = await task; + return result; + } + catch (TaskCanceledException e) + { + Debug.LogWarning($"[TYSdkFacade] GetSKUList timeout: {e.Message}"); + return new ProductListInfo() { code = 1, msg = "sku_callback_timeout" }; + } #endif } @@ -176,10 +184,18 @@ namespace tysdk await Task.Yield(); return new ProductListInfo(); #elif UNITY_ANDROID - var task = TYSDKCallbackManager.Instance.RegisterCallback(); + var task = TYSDKCallbackManager.Instance.RegisterCallback(TimeSpan.FromSeconds(45)); UnityBridgeFunc.GetSKUListByIapIds(productIds); - var result = await task; - return result; + try + { + var result = await task; + return result; + } + catch (TaskCanceledException e) + { + Debug.LogWarning($"[TYSdkFacade] GetSKUListByIapIds timeout: {e.Message}"); + return new ProductListInfo() { code = 1, msg = "sku_callback_timeout" }; + } #else return new ProductListInfo() { code = -1, msg = "unsupported platform" }; #endif diff --git a/sdk-intergration/Packages/tysdk/Runtime/UnityBridgeFunc.cs b/sdk-intergration/Packages/tysdk/Runtime/UnityBridgeFunc.cs index 104d934..abbc65b 100644 --- a/sdk-intergration/Packages/tysdk/Runtime/UnityBridgeFunc.cs +++ b/sdk-intergration/Packages/tysdk/Runtime/UnityBridgeFunc.cs @@ -48,6 +48,8 @@ namespace tysdk public static string GetPendingPurchases() => "[]"; + public static void RefreshBillingService() { } + //打点 public static void SetGaUserInfo(string userId){} public static void SetGaCommonInfo(string SetGaCommonInfo){} @@ -224,6 +226,15 @@ namespace tysdk } } + public static void RefreshBillingService() + { + UnityEngine.Debug.Log("UnityBridgeFunc.RefreshBillingService"); + using(var sdkManager = new AndroidJavaClass(SDK_CLASS)) + { + sdkManager.CallStatic("RefreshBillingService"); + } + } + public static void CancelBillingFlow() { UnityEngine.Debug.Log("UnityBridgeFunc.CancelBillingFlow"); @@ -470,6 +481,8 @@ namespace tysdk public static string GetPendingPurchases() => "[]"; + public static void RefreshBillingService() { } + #endif } }