@@ -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