From 09116d8b613f5c6e04cade23fa95b06080b899e5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 25 Jul 2025 13:27:29 +0000 Subject: [PATCH 1/9] Initial plan From 59dfc949c61f27b0846c15d52ded7f1454e7b5f4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 25 Jul 2025 14:16:48 +0000 Subject: [PATCH 2/9] Initial analysis of Billing 8.0.0.1 ObjectDisposedException issue Co-authored-by: jonathanpeppers <840039+jonathanpeppers@users.noreply.github.com> --- .../billing/PublicAPI/PublicAPI.Unshipped.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/source/com.android.billingclient/billing/PublicAPI/PublicAPI.Unshipped.txt b/source/com.android.billingclient/billing/PublicAPI/PublicAPI.Unshipped.txt index 7df2d4eb1..208f63051 100644 --- a/source/com.android.billingclient/billing/PublicAPI/PublicAPI.Unshipped.txt +++ b/source/com.android.billingclient/billing/PublicAPI/PublicAPI.Unshipped.txt @@ -315,7 +315,12 @@ Android.BillingClient.Api.QueryProductDetailsParams.Product.Zza() -> string! Android.BillingClient.Api.QueryProductDetailsParams.Product.Zzb() -> string! Android.BillingClient.Api.QueryProductDetailsParams.Zzb() -> string! Android.BillingClient.Api.QueryProductDetailsResult +Android.BillingClient.Api.QueryProductDetailsResult.ProductDetails.get -> System.Collections.Generic.IList! +Android.BillingClient.Api.QueryProductDetailsResult.ProductDetails.set -> void Android.BillingClient.Api.QueryProductDetailsResult.ProductDetailsList.get -> System.Collections.Generic.IList! +Android.BillingClient.Api.QueryProductDetailsResult.QueryProductDetailsResult() -> void +Android.BillingClient.Api.QueryProductDetailsResult.Result.get -> Android.BillingClient.Api.BillingResult! +Android.BillingClient.Api.QueryProductDetailsResult.Result.set -> void Android.BillingClient.Api.QueryProductDetailsResult.UnfetchedProductList.get -> System.Collections.Generic.IList! Android.BillingClient.Api.QueryPurchaseHistoryParams Android.BillingClient.Api.QueryPurchaseHistoryParams.Builder From b077f2d8935b99891fee197dfaff541085ffdf2d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 25 Jul 2025 14:19:11 +0000 Subject: [PATCH 3/9] Fix BillingResult ObjectDisposedException by creating persistent copies Co-authored-by: jonathanpeppers <840039+jonathanpeppers@users.noreply.github.com> --- .../billing/Additions/Additions.cs | 71 ++++++++++++++++--- 1 file changed, 63 insertions(+), 8 deletions(-) diff --git a/source/com.android.billingclient/billing/Additions/Additions.cs b/source/com.android.billingclient/billing/Additions/Additions.cs index 0c13c4a69..a2ab98cdc 100644 --- a/source/com.android.billingclient/billing/Additions/Additions.cs +++ b/source/com.android.billingclient/billing/Additions/Additions.cs @@ -196,7 +196,15 @@ internal class InternalAcknowledgePurchaseResponseListener : Java.Lang.Object, I public Action AcknowledgePurchaseResponseHandler { get; set; } public void OnAcknowledgePurchaseResponse(BillingResult result) - => AcknowledgePurchaseResponseHandler?.Invoke(result); + { + // Create a copy of the BillingResult to ensure it stays alive after the callback + var resultCopy = BillingResult.NewBuilder() + .SetResponseCode((int)result.ResponseCode) + .SetDebugMessage(result.DebugMessage) + .SetOnPurchasesUpdatedSubResponseCode(result.OnPurchasesUpdatedSubResponseCode) + .Build(); + AcknowledgePurchaseResponseHandler?.Invoke(resultCopy); + } } internal class InternalBillingClientStateListener : Java.Lang.Object, IBillingClientStateListener @@ -209,21 +217,45 @@ public void OnBillingServiceDisconnected() => BillingServiceDisconnectedHandler?.Invoke(); public void OnBillingSetupFinished(BillingResult result) - => BillingSetupFinishedHandler?.Invoke(result); + { + // Create a copy of the BillingResult to ensure it stays alive after the callback + var resultCopy = BillingResult.NewBuilder() + .SetResponseCode((int)result.ResponseCode) + .SetDebugMessage(result.DebugMessage) + .SetOnPurchasesUpdatedSubResponseCode(result.OnPurchasesUpdatedSubResponseCode) + .Build(); + BillingSetupFinishedHandler?.Invoke(resultCopy); + } } internal class InternalConsumeResponseListener : Java.Lang.Object, IConsumeResponseListener { public Action ConsumeResponseHandler { get; set; } public void OnConsumeResponse(BillingResult result, string str) - => ConsumeResponseHandler?.Invoke(result, str); + { + // Create a copy of the BillingResult to ensure it stays alive after the callback + var resultCopy = BillingResult.NewBuilder() + .SetResponseCode((int)result.ResponseCode) + .SetDebugMessage(result.DebugMessage) + .SetOnPurchasesUpdatedSubResponseCode(result.OnPurchasesUpdatedSubResponseCode) + .Build(); + ConsumeResponseHandler?.Invoke(resultCopy, str); + } } internal class InternalPriceChangeConfirmationListener : Java.Lang.Object //, IPriceChangeConfirmationListener { public Action PriceChangeConfirmationHandler { get; set; } public void OnPriceChangeConfirmationResult(BillingResult result) - => PriceChangeConfirmationHandler?.Invoke(result); + { + // Create a copy of the BillingResult to ensure it stays alive after the callback + var resultCopy = BillingResult.NewBuilder() + .SetResponseCode((int)result.ResponseCode) + .SetDebugMessage(result.DebugMessage) + .SetOnPurchasesUpdatedSubResponseCode(result.OnPurchasesUpdatedSubResponseCode) + .Build(); + PriceChangeConfirmationHandler?.Invoke(resultCopy); + } } internal class InternalPurchaseHistoryResponseListener : Java.Lang.Object, IPurchaseHistoryResponseListener @@ -231,14 +263,30 @@ internal class InternalPurchaseHistoryResponseListener : Java.Lang.Object, IPurc public Action> PurchaseHistoryResponseHandler { get; set; } public void OnPurchaseHistoryResponse(BillingResult result, IList history) - => PurchaseHistoryResponseHandler?.Invoke(result, history); + { + // Create a copy of the BillingResult to ensure it stays alive after the callback + var resultCopy = BillingResult.NewBuilder() + .SetResponseCode((int)result.ResponseCode) + .SetDebugMessage(result.DebugMessage) + .SetOnPurchasesUpdatedSubResponseCode(result.OnPurchasesUpdatedSubResponseCode) + .Build(); + PurchaseHistoryResponseHandler?.Invoke(resultCopy, history); + } } internal class InternalPurchasesUpdatedListener : Java.Lang.Object, IPurchasesUpdatedListener { public Action> PurchasesUpdatedHandler { get; set; } public void OnPurchasesUpdated(BillingResult result, IList purchases) - => PurchasesUpdatedHandler?.Invoke(result, purchases); + { + // Create a copy of the BillingResult to ensure it stays alive after the callback + var resultCopy = BillingResult.NewBuilder() + .SetResponseCode((int)result.ResponseCode) + .SetDebugMessage(result.DebugMessage) + .SetOnPurchasesUpdatedSubResponseCode(result.OnPurchasesUpdatedSubResponseCode) + .Build(); + PurchasesUpdatedHandler?.Invoke(resultCopy, purchases); + } } [Obsolete("Use QueryProductDetailsAsync(QueryProductDetailsParams) instead")] @@ -257,8 +305,15 @@ internal class InternalProductDetailsResponseListener : Java.Lang.Object, IProdu public void OnProductDetailsResponse(BillingResult result, QueryProductDetailsResult queryResult) { queryResult ??= new(); - queryResult.Result = result; - ProductDetailsResponseHandler?.Invoke(result, queryResult); + // Create a copy of the BillingResult to ensure it stays alive after the callback + // The original result may be disposed by the native side, causing ObjectDisposedException + var resultCopy = BillingResult.NewBuilder() + .SetResponseCode((int)result.ResponseCode) + .SetDebugMessage(result.DebugMessage) + .SetOnPurchasesUpdatedSubResponseCode(result.OnPurchasesUpdatedSubResponseCode) + .Build(); + queryResult.Result = resultCopy; + ProductDetailsResponseHandler?.Invoke(resultCopy, queryResult); } } From e3ae4b3019a3db96ecff5eda3800dbe3cd229d36 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 25 Jul 2025 17:16:41 +0000 Subject: [PATCH 4/9] Fix BillingResult ObjectDisposedException using GC.KeepAlive approach Co-authored-by: jonathanpeppers <840039+jonathanpeppers@users.noreply.github.com> --- .../billing/Additions/Additions.cs | 101 +++++------------- 1 file changed, 28 insertions(+), 73 deletions(-) diff --git a/source/com.android.billingclient/billing/Additions/Additions.cs b/source/com.android.billingclient/billing/Additions/Additions.cs index a2ab98cdc..f72206555 100644 --- a/source/com.android.billingclient/billing/Additions/Additions.cs +++ b/source/com.android.billingclient/billing/Additions/Additions.cs @@ -65,7 +65,7 @@ public void SetListener(Action> handler) } } - public Task AcknowledgePurchaseAsync(AcknowledgePurchaseParams acknowledgePurchaseParams) + public async Task AcknowledgePurchaseAsync(AcknowledgePurchaseParams acknowledgePurchaseParams) { var tcs = new TaskCompletionSource(); @@ -76,10 +76,12 @@ public Task AcknowledgePurchaseAsync(AcknowledgePurchaseParams ac AcknowledgePurchase(acknowledgePurchaseParams, listener); - return tcs.Task; + var result = await tcs.Task; + GC.KeepAlive(listener); + return result; } - public Task ConsumeAsync(ConsumeParams consumeParams) + public async Task ConsumeAsync(ConsumeParams consumeParams) { var tcs = new TaskCompletionSource(); @@ -94,7 +96,9 @@ public Task ConsumeAsync(ConsumeParams consumeParams) Consume(consumeParams, listener); - return tcs.Task; + var result = await tcs.Task; + GC.KeepAlive(listener); + return result; } const string QueryPurchaseHistoryNotSupported = "QueryPurchaseHistory method was removed in Billing Client v8.0.0. Use QueryPurchasesAsync instead. See: https://developer.android.com/google/play/billing/migrate"; @@ -125,7 +129,7 @@ public Task QuerySkuDetailsAsync(SkuDetailsParams skuDeta throw new NotSupportedException(QuerySkuDetailsNotSupported); } - public Task QueryProductDetailsAsync(QueryProductDetailsParams productDetailsParams) + public async Task QueryProductDetailsAsync(QueryProductDetailsParams productDetailsParams) { var tcs = new TaskCompletionSource(); @@ -136,10 +140,12 @@ public Task QueryProductDetailsAsync(QueryProductDeta QueryProductDetails(productDetailsParams, listener); - return tcs.Task; + var result = await tcs.Task; + GC.KeepAlive(listener); + return result; } - public Task QueryPurchasesAsync(QueryPurchasesParams purchasesParams) + public async Task QueryPurchasesAsync(QueryPurchasesParams purchasesParams) { var tcs = new TaskCompletionSource(); @@ -154,10 +160,12 @@ public Task QueryPurchasesAsync(QueryPurchasesParams purch QueryPurchases(purchasesParams, listener); - return tcs.Task; + var result = await tcs.Task; + GC.KeepAlive(listener); + return result; } - public Task StartConnectionAsync(Action onDisconnected = null) + public async Task StartConnectionAsync(Action onDisconnected = null) { var tcs = new TaskCompletionSource(); @@ -174,7 +182,9 @@ public Task StartConnectionAsync(Action onDisconnected = null) StartConnection(listener); - return tcs.Task; + var result = await tcs.Task; + GC.KeepAlive(listener); + return result; } public void StartConnection(Action setupFinished, Action onDisconnected) @@ -196,15 +206,7 @@ internal class InternalAcknowledgePurchaseResponseListener : Java.Lang.Object, I public Action AcknowledgePurchaseResponseHandler { get; set; } public void OnAcknowledgePurchaseResponse(BillingResult result) - { - // Create a copy of the BillingResult to ensure it stays alive after the callback - var resultCopy = BillingResult.NewBuilder() - .SetResponseCode((int)result.ResponseCode) - .SetDebugMessage(result.DebugMessage) - .SetOnPurchasesUpdatedSubResponseCode(result.OnPurchasesUpdatedSubResponseCode) - .Build(); - AcknowledgePurchaseResponseHandler?.Invoke(resultCopy); - } + => AcknowledgePurchaseResponseHandler?.Invoke(result); } internal class InternalBillingClientStateListener : Java.Lang.Object, IBillingClientStateListener @@ -217,45 +219,21 @@ public void OnBillingServiceDisconnected() => BillingServiceDisconnectedHandler?.Invoke(); public void OnBillingSetupFinished(BillingResult result) - { - // Create a copy of the BillingResult to ensure it stays alive after the callback - var resultCopy = BillingResult.NewBuilder() - .SetResponseCode((int)result.ResponseCode) - .SetDebugMessage(result.DebugMessage) - .SetOnPurchasesUpdatedSubResponseCode(result.OnPurchasesUpdatedSubResponseCode) - .Build(); - BillingSetupFinishedHandler?.Invoke(resultCopy); - } + => BillingSetupFinishedHandler?.Invoke(result); } internal class InternalConsumeResponseListener : Java.Lang.Object, IConsumeResponseListener { public Action ConsumeResponseHandler { get; set; } public void OnConsumeResponse(BillingResult result, string str) - { - // Create a copy of the BillingResult to ensure it stays alive after the callback - var resultCopy = BillingResult.NewBuilder() - .SetResponseCode((int)result.ResponseCode) - .SetDebugMessage(result.DebugMessage) - .SetOnPurchasesUpdatedSubResponseCode(result.OnPurchasesUpdatedSubResponseCode) - .Build(); - ConsumeResponseHandler?.Invoke(resultCopy, str); - } + => ConsumeResponseHandler?.Invoke(result, str); } internal class InternalPriceChangeConfirmationListener : Java.Lang.Object //, IPriceChangeConfirmationListener { public Action PriceChangeConfirmationHandler { get; set; } public void OnPriceChangeConfirmationResult(BillingResult result) - { - // Create a copy of the BillingResult to ensure it stays alive after the callback - var resultCopy = BillingResult.NewBuilder() - .SetResponseCode((int)result.ResponseCode) - .SetDebugMessage(result.DebugMessage) - .SetOnPurchasesUpdatedSubResponseCode(result.OnPurchasesUpdatedSubResponseCode) - .Build(); - PriceChangeConfirmationHandler?.Invoke(resultCopy); - } + => PriceChangeConfirmationHandler?.Invoke(result); } internal class InternalPurchaseHistoryResponseListener : Java.Lang.Object, IPurchaseHistoryResponseListener @@ -263,30 +241,14 @@ internal class InternalPurchaseHistoryResponseListener : Java.Lang.Object, IPurc public Action> PurchaseHistoryResponseHandler { get; set; } public void OnPurchaseHistoryResponse(BillingResult result, IList history) - { - // Create a copy of the BillingResult to ensure it stays alive after the callback - var resultCopy = BillingResult.NewBuilder() - .SetResponseCode((int)result.ResponseCode) - .SetDebugMessage(result.DebugMessage) - .SetOnPurchasesUpdatedSubResponseCode(result.OnPurchasesUpdatedSubResponseCode) - .Build(); - PurchaseHistoryResponseHandler?.Invoke(resultCopy, history); - } + => PurchaseHistoryResponseHandler?.Invoke(result, history); } internal class InternalPurchasesUpdatedListener : Java.Lang.Object, IPurchasesUpdatedListener { public Action> PurchasesUpdatedHandler { get; set; } public void OnPurchasesUpdated(BillingResult result, IList purchases) - { - // Create a copy of the BillingResult to ensure it stays alive after the callback - var resultCopy = BillingResult.NewBuilder() - .SetResponseCode((int)result.ResponseCode) - .SetDebugMessage(result.DebugMessage) - .SetOnPurchasesUpdatedSubResponseCode(result.OnPurchasesUpdatedSubResponseCode) - .Build(); - PurchasesUpdatedHandler?.Invoke(resultCopy, purchases); - } + => PurchasesUpdatedHandler?.Invoke(result, purchases); } [Obsolete("Use QueryProductDetailsAsync(QueryProductDetailsParams) instead")] @@ -305,15 +267,8 @@ internal class InternalProductDetailsResponseListener : Java.Lang.Object, IProdu public void OnProductDetailsResponse(BillingResult result, QueryProductDetailsResult queryResult) { queryResult ??= new(); - // Create a copy of the BillingResult to ensure it stays alive after the callback - // The original result may be disposed by the native side, causing ObjectDisposedException - var resultCopy = BillingResult.NewBuilder() - .SetResponseCode((int)result.ResponseCode) - .SetDebugMessage(result.DebugMessage) - .SetOnPurchasesUpdatedSubResponseCode(result.OnPurchasesUpdatedSubResponseCode) - .Build(); - queryResult.Result = resultCopy; - ProductDetailsResponseHandler?.Invoke(resultCopy, queryResult); + queryResult.Result = result; + ProductDetailsResponseHandler?.Invoke(result, queryResult); } } From 1d9adb5c760ab0de11f7fc4a9532a4ce2bc41cf6 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Mon, 28 Jul 2025 14:44:12 -0500 Subject: [PATCH 5/9] Revert "Fix BillingResult ObjectDisposedException using GC.KeepAlive approach" This reverts commit e3ae4b3019a3db96ecff5eda3800dbe3cd229d36. --- .../billing/Additions/Additions.cs | 101 +++++++++++++----- 1 file changed, 73 insertions(+), 28 deletions(-) diff --git a/source/com.android.billingclient/billing/Additions/Additions.cs b/source/com.android.billingclient/billing/Additions/Additions.cs index f72206555..a2ab98cdc 100644 --- a/source/com.android.billingclient/billing/Additions/Additions.cs +++ b/source/com.android.billingclient/billing/Additions/Additions.cs @@ -65,7 +65,7 @@ public void SetListener(Action> handler) } } - public async Task AcknowledgePurchaseAsync(AcknowledgePurchaseParams acknowledgePurchaseParams) + public Task AcknowledgePurchaseAsync(AcknowledgePurchaseParams acknowledgePurchaseParams) { var tcs = new TaskCompletionSource(); @@ -76,12 +76,10 @@ public async Task AcknowledgePurchaseAsync(AcknowledgePurchasePar AcknowledgePurchase(acknowledgePurchaseParams, listener); - var result = await tcs.Task; - GC.KeepAlive(listener); - return result; + return tcs.Task; } - public async Task ConsumeAsync(ConsumeParams consumeParams) + public Task ConsumeAsync(ConsumeParams consumeParams) { var tcs = new TaskCompletionSource(); @@ -96,9 +94,7 @@ public async Task ConsumeAsync(ConsumeParams consumeParams) Consume(consumeParams, listener); - var result = await tcs.Task; - GC.KeepAlive(listener); - return result; + return tcs.Task; } const string QueryPurchaseHistoryNotSupported = "QueryPurchaseHistory method was removed in Billing Client v8.0.0. Use QueryPurchasesAsync instead. See: https://developer.android.com/google/play/billing/migrate"; @@ -129,7 +125,7 @@ public Task QuerySkuDetailsAsync(SkuDetailsParams skuDeta throw new NotSupportedException(QuerySkuDetailsNotSupported); } - public async Task QueryProductDetailsAsync(QueryProductDetailsParams productDetailsParams) + public Task QueryProductDetailsAsync(QueryProductDetailsParams productDetailsParams) { var tcs = new TaskCompletionSource(); @@ -140,12 +136,10 @@ public async Task QueryProductDetailsAsync(QueryProdu QueryProductDetails(productDetailsParams, listener); - var result = await tcs.Task; - GC.KeepAlive(listener); - return result; + return tcs.Task; } - public async Task QueryPurchasesAsync(QueryPurchasesParams purchasesParams) + public Task QueryPurchasesAsync(QueryPurchasesParams purchasesParams) { var tcs = new TaskCompletionSource(); @@ -160,12 +154,10 @@ public async Task QueryPurchasesAsync(QueryPurchasesParams QueryPurchases(purchasesParams, listener); - var result = await tcs.Task; - GC.KeepAlive(listener); - return result; + return tcs.Task; } - public async Task StartConnectionAsync(Action onDisconnected = null) + public Task StartConnectionAsync(Action onDisconnected = null) { var tcs = new TaskCompletionSource(); @@ -182,9 +174,7 @@ public async Task StartConnectionAsync(Action onDisconnected = nu StartConnection(listener); - var result = await tcs.Task; - GC.KeepAlive(listener); - return result; + return tcs.Task; } public void StartConnection(Action setupFinished, Action onDisconnected) @@ -206,7 +196,15 @@ internal class InternalAcknowledgePurchaseResponseListener : Java.Lang.Object, I public Action AcknowledgePurchaseResponseHandler { get; set; } public void OnAcknowledgePurchaseResponse(BillingResult result) - => AcknowledgePurchaseResponseHandler?.Invoke(result); + { + // Create a copy of the BillingResult to ensure it stays alive after the callback + var resultCopy = BillingResult.NewBuilder() + .SetResponseCode((int)result.ResponseCode) + .SetDebugMessage(result.DebugMessage) + .SetOnPurchasesUpdatedSubResponseCode(result.OnPurchasesUpdatedSubResponseCode) + .Build(); + AcknowledgePurchaseResponseHandler?.Invoke(resultCopy); + } } internal class InternalBillingClientStateListener : Java.Lang.Object, IBillingClientStateListener @@ -219,21 +217,45 @@ public void OnBillingServiceDisconnected() => BillingServiceDisconnectedHandler?.Invoke(); public void OnBillingSetupFinished(BillingResult result) - => BillingSetupFinishedHandler?.Invoke(result); + { + // Create a copy of the BillingResult to ensure it stays alive after the callback + var resultCopy = BillingResult.NewBuilder() + .SetResponseCode((int)result.ResponseCode) + .SetDebugMessage(result.DebugMessage) + .SetOnPurchasesUpdatedSubResponseCode(result.OnPurchasesUpdatedSubResponseCode) + .Build(); + BillingSetupFinishedHandler?.Invoke(resultCopy); + } } internal class InternalConsumeResponseListener : Java.Lang.Object, IConsumeResponseListener { public Action ConsumeResponseHandler { get; set; } public void OnConsumeResponse(BillingResult result, string str) - => ConsumeResponseHandler?.Invoke(result, str); + { + // Create a copy of the BillingResult to ensure it stays alive after the callback + var resultCopy = BillingResult.NewBuilder() + .SetResponseCode((int)result.ResponseCode) + .SetDebugMessage(result.DebugMessage) + .SetOnPurchasesUpdatedSubResponseCode(result.OnPurchasesUpdatedSubResponseCode) + .Build(); + ConsumeResponseHandler?.Invoke(resultCopy, str); + } } internal class InternalPriceChangeConfirmationListener : Java.Lang.Object //, IPriceChangeConfirmationListener { public Action PriceChangeConfirmationHandler { get; set; } public void OnPriceChangeConfirmationResult(BillingResult result) - => PriceChangeConfirmationHandler?.Invoke(result); + { + // Create a copy of the BillingResult to ensure it stays alive after the callback + var resultCopy = BillingResult.NewBuilder() + .SetResponseCode((int)result.ResponseCode) + .SetDebugMessage(result.DebugMessage) + .SetOnPurchasesUpdatedSubResponseCode(result.OnPurchasesUpdatedSubResponseCode) + .Build(); + PriceChangeConfirmationHandler?.Invoke(resultCopy); + } } internal class InternalPurchaseHistoryResponseListener : Java.Lang.Object, IPurchaseHistoryResponseListener @@ -241,14 +263,30 @@ internal class InternalPurchaseHistoryResponseListener : Java.Lang.Object, IPurc public Action> PurchaseHistoryResponseHandler { get; set; } public void OnPurchaseHistoryResponse(BillingResult result, IList history) - => PurchaseHistoryResponseHandler?.Invoke(result, history); + { + // Create a copy of the BillingResult to ensure it stays alive after the callback + var resultCopy = BillingResult.NewBuilder() + .SetResponseCode((int)result.ResponseCode) + .SetDebugMessage(result.DebugMessage) + .SetOnPurchasesUpdatedSubResponseCode(result.OnPurchasesUpdatedSubResponseCode) + .Build(); + PurchaseHistoryResponseHandler?.Invoke(resultCopy, history); + } } internal class InternalPurchasesUpdatedListener : Java.Lang.Object, IPurchasesUpdatedListener { public Action> PurchasesUpdatedHandler { get; set; } public void OnPurchasesUpdated(BillingResult result, IList purchases) - => PurchasesUpdatedHandler?.Invoke(result, purchases); + { + // Create a copy of the BillingResult to ensure it stays alive after the callback + var resultCopy = BillingResult.NewBuilder() + .SetResponseCode((int)result.ResponseCode) + .SetDebugMessage(result.DebugMessage) + .SetOnPurchasesUpdatedSubResponseCode(result.OnPurchasesUpdatedSubResponseCode) + .Build(); + PurchasesUpdatedHandler?.Invoke(resultCopy, purchases); + } } [Obsolete("Use QueryProductDetailsAsync(QueryProductDetailsParams) instead")] @@ -267,8 +305,15 @@ internal class InternalProductDetailsResponseListener : Java.Lang.Object, IProdu public void OnProductDetailsResponse(BillingResult result, QueryProductDetailsResult queryResult) { queryResult ??= new(); - queryResult.Result = result; - ProductDetailsResponseHandler?.Invoke(result, queryResult); + // Create a copy of the BillingResult to ensure it stays alive after the callback + // The original result may be disposed by the native side, causing ObjectDisposedException + var resultCopy = BillingResult.NewBuilder() + .SetResponseCode((int)result.ResponseCode) + .SetDebugMessage(result.DebugMessage) + .SetOnPurchasesUpdatedSubResponseCode(result.OnPurchasesUpdatedSubResponseCode) + .Build(); + queryResult.Result = resultCopy; + ProductDetailsResponseHandler?.Invoke(resultCopy, queryResult); } } From 8deb034e44102048cbd3a6dd416818615f1f61ef Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Mon, 28 Jul 2025 14:44:15 -0500 Subject: [PATCH 6/9] Revert "Fix BillingResult ObjectDisposedException by creating persistent copies" This reverts commit b077f2d8935b99891fee197dfaff541085ffdf2d. --- .../billing/Additions/Additions.cs | 71 +++---------------- 1 file changed, 8 insertions(+), 63 deletions(-) diff --git a/source/com.android.billingclient/billing/Additions/Additions.cs b/source/com.android.billingclient/billing/Additions/Additions.cs index a2ab98cdc..0c13c4a69 100644 --- a/source/com.android.billingclient/billing/Additions/Additions.cs +++ b/source/com.android.billingclient/billing/Additions/Additions.cs @@ -196,15 +196,7 @@ internal class InternalAcknowledgePurchaseResponseListener : Java.Lang.Object, I public Action AcknowledgePurchaseResponseHandler { get; set; } public void OnAcknowledgePurchaseResponse(BillingResult result) - { - // Create a copy of the BillingResult to ensure it stays alive after the callback - var resultCopy = BillingResult.NewBuilder() - .SetResponseCode((int)result.ResponseCode) - .SetDebugMessage(result.DebugMessage) - .SetOnPurchasesUpdatedSubResponseCode(result.OnPurchasesUpdatedSubResponseCode) - .Build(); - AcknowledgePurchaseResponseHandler?.Invoke(resultCopy); - } + => AcknowledgePurchaseResponseHandler?.Invoke(result); } internal class InternalBillingClientStateListener : Java.Lang.Object, IBillingClientStateListener @@ -217,45 +209,21 @@ public void OnBillingServiceDisconnected() => BillingServiceDisconnectedHandler?.Invoke(); public void OnBillingSetupFinished(BillingResult result) - { - // Create a copy of the BillingResult to ensure it stays alive after the callback - var resultCopy = BillingResult.NewBuilder() - .SetResponseCode((int)result.ResponseCode) - .SetDebugMessage(result.DebugMessage) - .SetOnPurchasesUpdatedSubResponseCode(result.OnPurchasesUpdatedSubResponseCode) - .Build(); - BillingSetupFinishedHandler?.Invoke(resultCopy); - } + => BillingSetupFinishedHandler?.Invoke(result); } internal class InternalConsumeResponseListener : Java.Lang.Object, IConsumeResponseListener { public Action ConsumeResponseHandler { get; set; } public void OnConsumeResponse(BillingResult result, string str) - { - // Create a copy of the BillingResult to ensure it stays alive after the callback - var resultCopy = BillingResult.NewBuilder() - .SetResponseCode((int)result.ResponseCode) - .SetDebugMessage(result.DebugMessage) - .SetOnPurchasesUpdatedSubResponseCode(result.OnPurchasesUpdatedSubResponseCode) - .Build(); - ConsumeResponseHandler?.Invoke(resultCopy, str); - } + => ConsumeResponseHandler?.Invoke(result, str); } internal class InternalPriceChangeConfirmationListener : Java.Lang.Object //, IPriceChangeConfirmationListener { public Action PriceChangeConfirmationHandler { get; set; } public void OnPriceChangeConfirmationResult(BillingResult result) - { - // Create a copy of the BillingResult to ensure it stays alive after the callback - var resultCopy = BillingResult.NewBuilder() - .SetResponseCode((int)result.ResponseCode) - .SetDebugMessage(result.DebugMessage) - .SetOnPurchasesUpdatedSubResponseCode(result.OnPurchasesUpdatedSubResponseCode) - .Build(); - PriceChangeConfirmationHandler?.Invoke(resultCopy); - } + => PriceChangeConfirmationHandler?.Invoke(result); } internal class InternalPurchaseHistoryResponseListener : Java.Lang.Object, IPurchaseHistoryResponseListener @@ -263,30 +231,14 @@ internal class InternalPurchaseHistoryResponseListener : Java.Lang.Object, IPurc public Action> PurchaseHistoryResponseHandler { get; set; } public void OnPurchaseHistoryResponse(BillingResult result, IList history) - { - // Create a copy of the BillingResult to ensure it stays alive after the callback - var resultCopy = BillingResult.NewBuilder() - .SetResponseCode((int)result.ResponseCode) - .SetDebugMessage(result.DebugMessage) - .SetOnPurchasesUpdatedSubResponseCode(result.OnPurchasesUpdatedSubResponseCode) - .Build(); - PurchaseHistoryResponseHandler?.Invoke(resultCopy, history); - } + => PurchaseHistoryResponseHandler?.Invoke(result, history); } internal class InternalPurchasesUpdatedListener : Java.Lang.Object, IPurchasesUpdatedListener { public Action> PurchasesUpdatedHandler { get; set; } public void OnPurchasesUpdated(BillingResult result, IList purchases) - { - // Create a copy of the BillingResult to ensure it stays alive after the callback - var resultCopy = BillingResult.NewBuilder() - .SetResponseCode((int)result.ResponseCode) - .SetDebugMessage(result.DebugMessage) - .SetOnPurchasesUpdatedSubResponseCode(result.OnPurchasesUpdatedSubResponseCode) - .Build(); - PurchasesUpdatedHandler?.Invoke(resultCopy, purchases); - } + => PurchasesUpdatedHandler?.Invoke(result, purchases); } [Obsolete("Use QueryProductDetailsAsync(QueryProductDetailsParams) instead")] @@ -305,15 +257,8 @@ internal class InternalProductDetailsResponseListener : Java.Lang.Object, IProdu public void OnProductDetailsResponse(BillingResult result, QueryProductDetailsResult queryResult) { queryResult ??= new(); - // Create a copy of the BillingResult to ensure it stays alive after the callback - // The original result may be disposed by the native side, causing ObjectDisposedException - var resultCopy = BillingResult.NewBuilder() - .SetResponseCode((int)result.ResponseCode) - .SetDebugMessage(result.DebugMessage) - .SetOnPurchasesUpdatedSubResponseCode(result.OnPurchasesUpdatedSubResponseCode) - .Build(); - queryResult.Result = resultCopy; - ProductDetailsResponseHandler?.Invoke(resultCopy, queryResult); + queryResult.Result = result; + ProductDetailsResponseHandler?.Invoke(result, queryResult); } } From bfb29dbad6b2a555e55933ff433324ccab5f6efa Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Mon, 28 Jul 2025 14:58:04 -0500 Subject: [PATCH 7/9] Revert back to what we had Context: https://github.com/dotnet/android-libraries/commit/1de2bde06f786d32f4aa291362fdbb5218e5f379 Added a NOTE comment --- .../billing/Additions/Additions.cs | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/source/com.android.billingclient/billing/Additions/Additions.cs b/source/com.android.billingclient/billing/Additions/Additions.cs index 0c13c4a69..1b2edb00d 100644 --- a/source/com.android.billingclient/billing/Additions/Additions.cs +++ b/source/com.android.billingclient/billing/Additions/Additions.cs @@ -33,12 +33,7 @@ public QueryProductDetailsResult() { } public BillingResult Result { get; set; } - [Obsolete ($"Use {nameof(ProductDetailsList)} instead")] - public IList ProductDetails - { - get => ProductDetailsList; - set { /* Obsolete property setter does nothing */ } - } + public IList ProductDetails { get; set; } } public class QueryPurchasesResult @@ -129,9 +124,14 @@ public Task QueryProductDetailsAsync(QueryProductDeta { var tcs = new TaskCompletionSource(); + // NOTE: this creates a new QueryProductDetailsResult to avoid ObjectDisposedException var listener = new InternalProductDetailsResponseListener { - ProductDetailsResponseHandler = (r, queryResult) => tcs.TrySetResult(queryResult) + ProductDetailsResponseHandler = (r, s) => tcs.TrySetResult(new QueryProductDetailsResult + { + Result = r, + ProductDetails = s + }) }; QueryProductDetails(productDetailsParams, listener); @@ -252,13 +252,11 @@ public void OnSkuDetailsResponse(BillingResult result, IList skuDeta internal class InternalProductDetailsResponseListener : Java.Lang.Object, IProductDetailsResponseListener { - public Action ProductDetailsResponseHandler { get; set; } + public Action> ProductDetailsResponseHandler { get; set; } public void OnProductDetailsResponse(BillingResult result, QueryProductDetailsResult queryResult) { - queryResult ??= new(); - queryResult.Result = result; - ProductDetailsResponseHandler?.Invoke(result, queryResult); + ProductDetailsResponseHandler?.Invoke(result, queryResult?.ProductDetailsList); } } From d8d6034bed0b3eee28191482b839988663e723d9 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Mon, 4 Aug 2025 15:13:51 -0500 Subject: [PATCH 8/9] Still not quite working, but pushing my change --- .../billing/Additions/Additions.cs | 45 ++++++++++++++++--- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/source/com.android.billingclient/billing/Additions/Additions.cs b/source/com.android.billingclient/billing/Additions/Additions.cs index 1b2edb00d..763f0ed2e 100644 --- a/source/com.android.billingclient/billing/Additions/Additions.cs +++ b/source/com.android.billingclient/billing/Additions/Additions.cs @@ -4,6 +4,18 @@ namespace Android.BillingClient.Api { + public partial class BillingResult + { + internal BillingResult Clone() + { + return BillingResult.NewBuilder() + .SetResponseCode((int)ResponseCode) + .SetDebugMessage(DebugMessage) + .SetOnPurchasesUpdatedSubResponseCode(OnPurchasesUpdatedSubResponseCode) + .Build(); + } + } + public class ConsumeResult { public BillingResult BillingResult { get; set; } @@ -196,7 +208,10 @@ internal class InternalAcknowledgePurchaseResponseListener : Java.Lang.Object, I public Action AcknowledgePurchaseResponseHandler { get; set; } public void OnAcknowledgePurchaseResponse(BillingResult result) - => AcknowledgePurchaseResponseHandler?.Invoke(result); + { + // Create a copy of the BillingResult to ensure it stays alive after the callback + AcknowledgePurchaseResponseHandler?.Invoke(result.Clone()); + } } internal class InternalBillingClientStateListener : Java.Lang.Object, IBillingClientStateListener @@ -209,21 +224,30 @@ public void OnBillingServiceDisconnected() => BillingServiceDisconnectedHandler?.Invoke(); public void OnBillingSetupFinished(BillingResult result) - => BillingSetupFinishedHandler?.Invoke(result); + { + // Create a copy of the BillingResult to ensure it stays alive after the callback + BillingSetupFinishedHandler?.Invoke(result.Clone()); + } } internal class InternalConsumeResponseListener : Java.Lang.Object, IConsumeResponseListener { public Action ConsumeResponseHandler { get; set; } public void OnConsumeResponse(BillingResult result, string str) - => ConsumeResponseHandler?.Invoke(result, str); + { + // Create a copy of the BillingResult to ensure it stays alive after the callback + ConsumeResponseHandler?.Invoke(result.Clone(), str); + } } internal class InternalPriceChangeConfirmationListener : Java.Lang.Object //, IPriceChangeConfirmationListener { public Action PriceChangeConfirmationHandler { get; set; } public void OnPriceChangeConfirmationResult(BillingResult result) - => PriceChangeConfirmationHandler?.Invoke(result); + { + // Create a copy of the BillingResult to ensure it stays alive after the callback + PriceChangeConfirmationHandler?.Invoke(result.Clone()); + } } internal class InternalPurchaseHistoryResponseListener : Java.Lang.Object, IPurchaseHistoryResponseListener @@ -231,14 +255,20 @@ internal class InternalPurchaseHistoryResponseListener : Java.Lang.Object, IPurc public Action> PurchaseHistoryResponseHandler { get; set; } public void OnPurchaseHistoryResponse(BillingResult result, IList history) - => PurchaseHistoryResponseHandler?.Invoke(result, history); + { + // Create a copy of the BillingResult to ensure it stays alive after the callback + PurchaseHistoryResponseHandler?.Invoke(result.Clone(), history); + } } internal class InternalPurchasesUpdatedListener : Java.Lang.Object, IPurchasesUpdatedListener { public Action> PurchasesUpdatedHandler { get; set; } public void OnPurchasesUpdated(BillingResult result, IList purchases) - => PurchasesUpdatedHandler?.Invoke(result, purchases); + { + // Create a copy of the BillingResult to ensure it stays alive after the callback + PurchasesUpdatedHandler?.Invoke(result.Clone(), purchases); + } } [Obsolete("Use QueryProductDetailsAsync(QueryProductDetailsParams) instead")] @@ -256,7 +286,8 @@ internal class InternalProductDetailsResponseListener : Java.Lang.Object, IProdu public void OnProductDetailsResponse(BillingResult result, QueryProductDetailsResult queryResult) { - ProductDetailsResponseHandler?.Invoke(result, queryResult?.ProductDetailsList); + // Create a copy of the BillingResult to ensure it stays alive after the callback + ProductDetailsResponseHandler?.Invoke(result.Clone(), queryResult?.ProductDetailsList); } } From 6720fe3cb548df739198f5b25b09013a5803126e Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Thu, 30 Oct 2025 09:53:32 -0500 Subject: [PATCH 9/9] Revert "Still not quite working, but pushing my change" This reverts commit d8d6034bed0b3eee28191482b839988663e723d9. --- .../billing/Additions/Additions.cs | 45 +++---------------- 1 file changed, 7 insertions(+), 38 deletions(-) diff --git a/source/com.android.billingclient/billing/Additions/Additions.cs b/source/com.android.billingclient/billing/Additions/Additions.cs index 763f0ed2e..1b2edb00d 100644 --- a/source/com.android.billingclient/billing/Additions/Additions.cs +++ b/source/com.android.billingclient/billing/Additions/Additions.cs @@ -4,18 +4,6 @@ namespace Android.BillingClient.Api { - public partial class BillingResult - { - internal BillingResult Clone() - { - return BillingResult.NewBuilder() - .SetResponseCode((int)ResponseCode) - .SetDebugMessage(DebugMessage) - .SetOnPurchasesUpdatedSubResponseCode(OnPurchasesUpdatedSubResponseCode) - .Build(); - } - } - public class ConsumeResult { public BillingResult BillingResult { get; set; } @@ -208,10 +196,7 @@ internal class InternalAcknowledgePurchaseResponseListener : Java.Lang.Object, I public Action AcknowledgePurchaseResponseHandler { get; set; } public void OnAcknowledgePurchaseResponse(BillingResult result) - { - // Create a copy of the BillingResult to ensure it stays alive after the callback - AcknowledgePurchaseResponseHandler?.Invoke(result.Clone()); - } + => AcknowledgePurchaseResponseHandler?.Invoke(result); } internal class InternalBillingClientStateListener : Java.Lang.Object, IBillingClientStateListener @@ -224,30 +209,21 @@ public void OnBillingServiceDisconnected() => BillingServiceDisconnectedHandler?.Invoke(); public void OnBillingSetupFinished(BillingResult result) - { - // Create a copy of the BillingResult to ensure it stays alive after the callback - BillingSetupFinishedHandler?.Invoke(result.Clone()); - } + => BillingSetupFinishedHandler?.Invoke(result); } internal class InternalConsumeResponseListener : Java.Lang.Object, IConsumeResponseListener { public Action ConsumeResponseHandler { get; set; } public void OnConsumeResponse(BillingResult result, string str) - { - // Create a copy of the BillingResult to ensure it stays alive after the callback - ConsumeResponseHandler?.Invoke(result.Clone(), str); - } + => ConsumeResponseHandler?.Invoke(result, str); } internal class InternalPriceChangeConfirmationListener : Java.Lang.Object //, IPriceChangeConfirmationListener { public Action PriceChangeConfirmationHandler { get; set; } public void OnPriceChangeConfirmationResult(BillingResult result) - { - // Create a copy of the BillingResult to ensure it stays alive after the callback - PriceChangeConfirmationHandler?.Invoke(result.Clone()); - } + => PriceChangeConfirmationHandler?.Invoke(result); } internal class InternalPurchaseHistoryResponseListener : Java.Lang.Object, IPurchaseHistoryResponseListener @@ -255,20 +231,14 @@ internal class InternalPurchaseHistoryResponseListener : Java.Lang.Object, IPurc public Action> PurchaseHistoryResponseHandler { get; set; } public void OnPurchaseHistoryResponse(BillingResult result, IList history) - { - // Create a copy of the BillingResult to ensure it stays alive after the callback - PurchaseHistoryResponseHandler?.Invoke(result.Clone(), history); - } + => PurchaseHistoryResponseHandler?.Invoke(result, history); } internal class InternalPurchasesUpdatedListener : Java.Lang.Object, IPurchasesUpdatedListener { public Action> PurchasesUpdatedHandler { get; set; } public void OnPurchasesUpdated(BillingResult result, IList purchases) - { - // Create a copy of the BillingResult to ensure it stays alive after the callback - PurchasesUpdatedHandler?.Invoke(result.Clone(), purchases); - } + => PurchasesUpdatedHandler?.Invoke(result, purchases); } [Obsolete("Use QueryProductDetailsAsync(QueryProductDetailsParams) instead")] @@ -286,8 +256,7 @@ internal class InternalProductDetailsResponseListener : Java.Lang.Object, IProdu public void OnProductDetailsResponse(BillingResult result, QueryProductDetailsResult queryResult) { - // Create a copy of the BillingResult to ensure it stays alive after the callback - ProductDetailsResponseHandler?.Invoke(result.Clone(), queryResult?.ProductDetailsList); + ProductDetailsResponseHandler?.Invoke(result, queryResult?.ProductDetailsList); } }