Skip to content

Commit 69058e0

Browse files
authored
Allow subscriptions and in-app products to be registered at the same time. (#282)
1 parent d0c953a commit 69058e0

File tree

2 files changed

+81
-30
lines changed

2 files changed

+81
-30
lines changed

gdx-pay-android-googlebilling/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ android {
77
buildToolsVersion androidBuildToolsVersion
88

99
defaultConfig {
10-
minSdkVersion 14
10+
minSdkVersion 24
1111
targetSdkVersion androidTargetSdkVersion
1212
testInstrumentationRunner "android.test.InstrumentationTestRunner"
1313
testOptions {

gdx-pay-android-googlebilling/src/com/badlogic/gdx/pay/android/googlebilling/PurchaseManagerGoogleBilling.java

Lines changed: 80 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public PurchaseManagerGoogleBilling(Activity activity) {
4949
.enableOneTimeProducts()
5050
.enablePrepaidPlans()
5151
.build();
52-
52+
5353
mBillingClient = BillingClient.newBuilder(activity)
5454
.setListener(this)
5555
.enablePendingPurchases(params)
@@ -139,53 +139,104 @@ private void fetchOfferDetails() {
139139
productDetailsMap.clear();
140140
int offerSize = config.getOfferCount();
141141

142-
List<QueryProductDetailsParams.Product> productList = new ArrayList<>();
142+
List<QueryProductDetailsParams.Product> inAppProducts = new ArrayList<>();
143+
List<QueryProductDetailsParams.Product> subsProducts = new ArrayList<>();
143144

144145
for (int z = 0; z < offerSize; z++) {
145146
Offer offer = config.getOffer(z);
146-
productList.add(QueryProductDetailsParams.Product.newBuilder()
147+
String productType = mapOfferType(offer.getType());
148+
QueryProductDetailsParams.Product product = QueryProductDetailsParams.Product.newBuilder()
147149
.setProductId(offer.getIdentifierForStore(storeName()))
148-
.setProductType(mapOfferType(offer.getType()))
149-
.build());
150+
.setProductType(productType)
151+
.build();
152+
153+
if (ProductType.SUBS.equals(productType)) {
154+
subsProducts.add(product);
155+
} else {
156+
inAppProducts.add(product);
157+
}
150158
}
151159

152-
if (productList.isEmpty()) {
160+
if (inAppProducts.isEmpty() && subsProducts.isEmpty()) {
153161
Gdx.app.debug(TAG, "No products configured");
154162
setInstalledAndNotifyObserver();
155163
return;
156164
}
157165

166+
// Execute per-product-type queries and only complete install after all succeed.
167+
int totalBatches = (inAppProducts.isEmpty() ? 0 : 1) + (subsProducts.isEmpty() ? 0 : 1);
168+
final int[] remainingBatches = new int[] { totalBatches };
169+
final boolean[] failed = new boolean[] { false };
170+
171+
Runnable onBatchSuccess = () -> {
172+
if (remainingBatches[0] > 0) {
173+
remainingBatches[0] -= 1;
174+
}
175+
if (remainingBatches[0] == 0 && !failed[0]) {
176+
setInstalledAndNotifyObserver();
177+
}
178+
};
179+
180+
java.util.function.Consumer<Exception> onBatchError = (Exception e) -> {
181+
// Mark failure to prevent later handleInstall() and notify observer once.
182+
failed[0] = true;
183+
installationComplete = true; // clear "installing" state to avoid deadlock
184+
Gdx.app.error(TAG, "Failed to fetch product details", e);
185+
if (observer != null) {
186+
observer.handleInstallError(e);
187+
}
188+
};
189+
190+
if (!inAppProducts.isEmpty()) {
191+
queryProductDetailsForProducts(inAppProducts, ProductType.INAPP, onBatchSuccess, onBatchError);
192+
}
193+
if (!subsProducts.isEmpty()) {
194+
queryProductDetailsForProducts(subsProducts, ProductType.SUBS, onBatchSuccess, onBatchError);
195+
}
196+
}
197+
198+
private void queryProductDetailsForProducts(List<QueryProductDetailsParams.Product> productList,
199+
String productTypeHint,
200+
Runnable onSuccess,
201+
java.util.function.Consumer<Exception> onError) {
202+
// We avoid mixing types in a single request by design (split beforehand).
203+
// If productList is empty, treat as success to allow UI to show "Unavailable".
204+
if (productList == null || productList.isEmpty()) {
205+
Gdx.app.debug(TAG, "Empty product list for type: " + productTypeHint + " — treating as success");
206+
onSuccess.run();
207+
return;
208+
}
209+
158210
QueryProductDetailsParams params = QueryProductDetailsParams.newBuilder()
159211
.setProductList(productList)
160212
.build();
161213

162-
Gdx.app.debug(TAG, "QueryProductDetailsParams: " + params);
214+
Gdx.app.debug(TAG, "QueryProductDetailsParams (type: " + productTypeHint + "): " + params);
163215
mBillingClient.queryProductDetailsAsync(
164216
params,
165-
new ProductDetailsResponseListener() {
166-
@Override
167-
public void onProductDetailsResponse(@NonNull BillingResult billingResult, @NonNull QueryProductDetailsResult productDetailsResult) {
168-
int responseCode = billingResult.getResponseCode();
169-
// it might happen that this was already disposed until the response comes back
170-
if (observer == null || Gdx.app == null)
171-
return;
172-
173-
if (responseCode != BillingClient.BillingResponseCode.OK) {
174-
Gdx.app.error(TAG, "onProductDetailsResponse failed, error code is " + responseCode);
175-
if (!installationComplete) {
176-
observer.handleInstallError(new FetchItemInformationException(String.valueOf(responseCode)));
177-
}
178-
} else {
179-
List<ProductDetails> productDetailsList = productDetailsResult.getProductDetailsList();
180-
Gdx.app.debug(TAG,"Retrieved product count: " + productDetailsList.size());
181-
for (ProductDetails productDetails : productDetailsList) {
182-
informationMap.put(productDetails.getProductId(), convertProductDetailsToInformation(productDetails));
183-
productDetailsMap.put(productDetails.getProductId(), productDetails);
184-
}
217+
(billingResult, productDetailsResult) -> {
218+
// it might happen that this was already disposed until the response comes back
219+
if (observer == null || Gdx.app == null) return;
220+
221+
int responseCode = billingResult.getResponseCode();
222+
if (responseCode != BillingClient.BillingResponseCode.OK) {
223+
onError.accept(new FetchItemInformationException(String.valueOf(responseCode)));
224+
return;
225+
}
185226

186-
setInstalledAndNotifyObserver();
187-
}
227+
List<ProductDetails> productDetailsList = productDetailsResult.getProductDetailsList();
228+
if (productDetailsList == null) {
229+
productDetailsList = Collections.emptyList();
188230
}
231+
232+
Gdx.app.debug(TAG, "Retrieved product count (batch " + productTypeHint + "): " + productDetailsList.size());
233+
for (ProductDetails productDetails : productDetailsList) {
234+
informationMap.put(productDetails.getProductId(), convertProductDetailsToInformation(productDetails));
235+
productDetailsMap.put(productDetails.getProductId(), productDetails);
236+
}
237+
238+
// Even if empty, we still consider this batch a success to allow the app to show "Unavailable"
239+
onSuccess.run();
189240
}
190241
);
191242
}

0 commit comments

Comments
 (0)