Skip to content

Commit e3ae4b3

Browse files
Fix BillingResult ObjectDisposedException using GC.KeepAlive approach
Co-authored-by: jonathanpeppers <[email protected]>
1 parent b077f2d commit e3ae4b3

File tree

1 file changed

+28
-73
lines changed
  • source/com.android.billingclient/billing/Additions

1 file changed

+28
-73
lines changed

source/com.android.billingclient/billing/Additions/Additions.cs

Lines changed: 28 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public void SetListener(Action<BillingResult, IList<Purchase>> handler)
6565
}
6666
}
6767

68-
public Task<BillingResult> AcknowledgePurchaseAsync(AcknowledgePurchaseParams acknowledgePurchaseParams)
68+
public async Task<BillingResult> AcknowledgePurchaseAsync(AcknowledgePurchaseParams acknowledgePurchaseParams)
6969
{
7070
var tcs = new TaskCompletionSource<BillingResult>();
7171

@@ -76,10 +76,12 @@ public Task<BillingResult> AcknowledgePurchaseAsync(AcknowledgePurchaseParams ac
7676

7777
AcknowledgePurchase(acknowledgePurchaseParams, listener);
7878

79-
return tcs.Task;
79+
var result = await tcs.Task;
80+
GC.KeepAlive(listener);
81+
return result;
8082
}
8183

82-
public Task<ConsumeResult> ConsumeAsync(ConsumeParams consumeParams)
84+
public async Task<ConsumeResult> ConsumeAsync(ConsumeParams consumeParams)
8385
{
8486
var tcs = new TaskCompletionSource<ConsumeResult>();
8587

@@ -94,7 +96,9 @@ public Task<ConsumeResult> ConsumeAsync(ConsumeParams consumeParams)
9496

9597
Consume(consumeParams, listener);
9698

97-
return tcs.Task;
99+
var result = await tcs.Task;
100+
GC.KeepAlive(listener);
101+
return result;
98102
}
99103

100104
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<QuerySkuDetailsResult> QuerySkuDetailsAsync(SkuDetailsParams skuDeta
125129
throw new NotSupportedException(QuerySkuDetailsNotSupported);
126130
}
127131

128-
public Task<QueryProductDetailsResult> QueryProductDetailsAsync(QueryProductDetailsParams productDetailsParams)
132+
public async Task<QueryProductDetailsResult> QueryProductDetailsAsync(QueryProductDetailsParams productDetailsParams)
129133
{
130134
var tcs = new TaskCompletionSource<QueryProductDetailsResult>();
131135

@@ -136,10 +140,12 @@ public Task<QueryProductDetailsResult> QueryProductDetailsAsync(QueryProductDeta
136140

137141
QueryProductDetails(productDetailsParams, listener);
138142

139-
return tcs.Task;
143+
var result = await tcs.Task;
144+
GC.KeepAlive(listener);
145+
return result;
140146
}
141147

142-
public Task<QueryPurchasesResult> QueryPurchasesAsync(QueryPurchasesParams purchasesParams)
148+
public async Task<QueryPurchasesResult> QueryPurchasesAsync(QueryPurchasesParams purchasesParams)
143149
{
144150
var tcs = new TaskCompletionSource<QueryPurchasesResult>();
145151

@@ -154,10 +160,12 @@ public Task<QueryPurchasesResult> QueryPurchasesAsync(QueryPurchasesParams purch
154160

155161
QueryPurchases(purchasesParams, listener);
156162

157-
return tcs.Task;
163+
var result = await tcs.Task;
164+
GC.KeepAlive(listener);
165+
return result;
158166
}
159167

160-
public Task<BillingResult> StartConnectionAsync(Action onDisconnected = null)
168+
public async Task<BillingResult> StartConnectionAsync(Action onDisconnected = null)
161169
{
162170
var tcs = new TaskCompletionSource<BillingResult>();
163171

@@ -174,7 +182,9 @@ public Task<BillingResult> StartConnectionAsync(Action onDisconnected = null)
174182

175183
StartConnection(listener);
176184

177-
return tcs.Task;
185+
var result = await tcs.Task;
186+
GC.KeepAlive(listener);
187+
return result;
178188
}
179189

180190
public void StartConnection(Action<BillingResult> setupFinished, Action onDisconnected)
@@ -196,15 +206,7 @@ internal class InternalAcknowledgePurchaseResponseListener : Java.Lang.Object, I
196206
public Action<BillingResult> AcknowledgePurchaseResponseHandler { get; set; }
197207

198208
public void OnAcknowledgePurchaseResponse(BillingResult result)
199-
{
200-
// Create a copy of the BillingResult to ensure it stays alive after the callback
201-
var resultCopy = BillingResult.NewBuilder()
202-
.SetResponseCode((int)result.ResponseCode)
203-
.SetDebugMessage(result.DebugMessage)
204-
.SetOnPurchasesUpdatedSubResponseCode(result.OnPurchasesUpdatedSubResponseCode)
205-
.Build();
206-
AcknowledgePurchaseResponseHandler?.Invoke(resultCopy);
207-
}
209+
=> AcknowledgePurchaseResponseHandler?.Invoke(result);
208210
}
209211

210212
internal class InternalBillingClientStateListener : Java.Lang.Object, IBillingClientStateListener
@@ -217,76 +219,36 @@ public void OnBillingServiceDisconnected()
217219
=> BillingServiceDisconnectedHandler?.Invoke();
218220

219221
public void OnBillingSetupFinished(BillingResult result)
220-
{
221-
// Create a copy of the BillingResult to ensure it stays alive after the callback
222-
var resultCopy = BillingResult.NewBuilder()
223-
.SetResponseCode((int)result.ResponseCode)
224-
.SetDebugMessage(result.DebugMessage)
225-
.SetOnPurchasesUpdatedSubResponseCode(result.OnPurchasesUpdatedSubResponseCode)
226-
.Build();
227-
BillingSetupFinishedHandler?.Invoke(resultCopy);
228-
}
222+
=> BillingSetupFinishedHandler?.Invoke(result);
229223
}
230224

231225
internal class InternalConsumeResponseListener : Java.Lang.Object, IConsumeResponseListener
232226
{
233227
public Action<BillingResult, string> ConsumeResponseHandler { get; set; }
234228
public void OnConsumeResponse(BillingResult result, string str)
235-
{
236-
// Create a copy of the BillingResult to ensure it stays alive after the callback
237-
var resultCopy = BillingResult.NewBuilder()
238-
.SetResponseCode((int)result.ResponseCode)
239-
.SetDebugMessage(result.DebugMessage)
240-
.SetOnPurchasesUpdatedSubResponseCode(result.OnPurchasesUpdatedSubResponseCode)
241-
.Build();
242-
ConsumeResponseHandler?.Invoke(resultCopy, str);
243-
}
229+
=> ConsumeResponseHandler?.Invoke(result, str);
244230
}
245231

246232
internal class InternalPriceChangeConfirmationListener : Java.Lang.Object //, IPriceChangeConfirmationListener
247233
{
248234
public Action<BillingResult> PriceChangeConfirmationHandler { get; set; }
249235
public void OnPriceChangeConfirmationResult(BillingResult result)
250-
{
251-
// Create a copy of the BillingResult to ensure it stays alive after the callback
252-
var resultCopy = BillingResult.NewBuilder()
253-
.SetResponseCode((int)result.ResponseCode)
254-
.SetDebugMessage(result.DebugMessage)
255-
.SetOnPurchasesUpdatedSubResponseCode(result.OnPurchasesUpdatedSubResponseCode)
256-
.Build();
257-
PriceChangeConfirmationHandler?.Invoke(resultCopy);
258-
}
236+
=> PriceChangeConfirmationHandler?.Invoke(result);
259237
}
260238

261239
internal class InternalPurchaseHistoryResponseListener : Java.Lang.Object, IPurchaseHistoryResponseListener
262240
{
263241
public Action<BillingResult, IList<PurchaseHistoryRecord>> PurchaseHistoryResponseHandler { get; set; }
264242

265243
public void OnPurchaseHistoryResponse(BillingResult result, IList<PurchaseHistoryRecord> history)
266-
{
267-
// Create a copy of the BillingResult to ensure it stays alive after the callback
268-
var resultCopy = BillingResult.NewBuilder()
269-
.SetResponseCode((int)result.ResponseCode)
270-
.SetDebugMessage(result.DebugMessage)
271-
.SetOnPurchasesUpdatedSubResponseCode(result.OnPurchasesUpdatedSubResponseCode)
272-
.Build();
273-
PurchaseHistoryResponseHandler?.Invoke(resultCopy, history);
274-
}
244+
=> PurchaseHistoryResponseHandler?.Invoke(result, history);
275245
}
276246

277247
internal class InternalPurchasesUpdatedListener : Java.Lang.Object, IPurchasesUpdatedListener
278248
{
279249
public Action<BillingResult, IList<Purchase>> PurchasesUpdatedHandler { get; set; }
280250
public void OnPurchasesUpdated(BillingResult result, IList<Purchase> purchases)
281-
{
282-
// Create a copy of the BillingResult to ensure it stays alive after the callback
283-
var resultCopy = BillingResult.NewBuilder()
284-
.SetResponseCode((int)result.ResponseCode)
285-
.SetDebugMessage(result.DebugMessage)
286-
.SetOnPurchasesUpdatedSubResponseCode(result.OnPurchasesUpdatedSubResponseCode)
287-
.Build();
288-
PurchasesUpdatedHandler?.Invoke(resultCopy, purchases);
289-
}
251+
=> PurchasesUpdatedHandler?.Invoke(result, purchases);
290252
}
291253

292254
[Obsolete("Use QueryProductDetailsAsync(QueryProductDetailsParams) instead")]
@@ -305,15 +267,8 @@ internal class InternalProductDetailsResponseListener : Java.Lang.Object, IProdu
305267
public void OnProductDetailsResponse(BillingResult result, QueryProductDetailsResult queryResult)
306268
{
307269
queryResult ??= new();
308-
// Create a copy of the BillingResult to ensure it stays alive after the callback
309-
// The original result may be disposed by the native side, causing ObjectDisposedException
310-
var resultCopy = BillingResult.NewBuilder()
311-
.SetResponseCode((int)result.ResponseCode)
312-
.SetDebugMessage(result.DebugMessage)
313-
.SetOnPurchasesUpdatedSubResponseCode(result.OnPurchasesUpdatedSubResponseCode)
314-
.Build();
315-
queryResult.Result = resultCopy;
316-
ProductDetailsResponseHandler?.Invoke(resultCopy, queryResult);
270+
queryResult.Result = result;
271+
ProductDetailsResponseHandler?.Invoke(result, queryResult);
317272
}
318273
}
319274

0 commit comments

Comments
 (0)