11package com .d4rk .androidtutorials .java .data .repository ;
22
33import android .content .Context ;
4+ import android .content .SharedPreferences ;
45
56import androidx .annotation .NonNull ;
67
8+ import com .android .billingclient .api .AcknowledgePurchaseParams ;
79import com .android .billingclient .api .BillingClient ;
810import com .android .billingclient .api .BillingClientStateListener ;
911import com .android .billingclient .api .BillingFlowParams ;
1012import com .android .billingclient .api .BillingResult ;
1113import com .android .billingclient .api .PendingPurchasesParams ;
1214import com .android .billingclient .api .ProductDetails ;
15+ import com .android .billingclient .api .Purchase ;
1316import com .android .billingclient .api .QueryProductDetailsParams ;
17+ import com .android .billingclient .api .QueryPurchasesParams ;
1418import com .d4rk .androidtutorials .java .ads .AdUtils ;
1519import com .google .android .gms .ads .AdRequest ;
1620
21+ import org .json .JSONException ;
22+ import org .json .JSONObject ;
23+
1724import java .util .ArrayList ;
1825import java .util .Collections ;
1926import java .util .HashMap ;
27+ import java .util .HashSet ;
2028import java .util .List ;
2129import java .util .Map ;
30+ import java .util .Set ;
2231
2332public class DefaultSupportRepository implements SupportRepository {
2433
34+ private static final String PREFS_NAME = "support_billing" ;
35+ private static final String KEY_GRANTED_PRODUCTS = "granted_products" ;
36+
2537 private final Context context ;
2638 private final Map <String , ProductDetails > productDetailsMap = new HashMap <>();
39+ private final SharedPreferences billingPrefs ;
40+ private final Set <String > grantedEntitlements ;
2741 private BillingClient billingClient ;
42+ private PurchaseStatusListener purchaseStatusListener ;
2843
2944 public DefaultSupportRepository (Context context ) {
3045 this .context = context .getApplicationContext ();
46+ this .billingPrefs = this .context .getSharedPreferences (PREFS_NAME , Context .MODE_PRIVATE );
47+ Set <String > granted = billingPrefs .getStringSet (KEY_GRANTED_PRODUCTS , Collections .emptySet ());
48+ this .grantedEntitlements = (granted != null ) ? new HashSet <>(granted ) : new HashSet <>();
3149 }
3250
3351 /**
3452 * Initialize the billing client and start the connection.
3553 *
3654 * @param onConnected Callback once the billing service is connected.
3755 */
56+ @ Override
3857 public void initBillingClient (Runnable onConnected ) {
3958 billingClient = BillingClient .newBuilder (context )
40- .setListener ((billingResult , purchases ) -> {
41- // To be implemented in a later release
42- })
59+ .setListener (this ::handlePurchaseUpdates )
4360 .enablePendingPurchases (
4461 PendingPurchasesParams .newBuilder ()
4562 .enableOneTimeProducts ()
@@ -53,6 +70,7 @@ public void initBillingClient(Runnable onConnected) {
5370 public void onBillingSetupFinished (@ NonNull BillingResult billingResult ) {
5471 if (billingResult .getResponseCode () == BillingClient .BillingResponseCode .OK ) {
5572 // The BillingClient is ready. You can query purchases here.
73+ refreshPurchases ();
5674 if (onConnected != null ) {
5775 onConnected .run ();
5876 }
@@ -72,6 +90,7 @@ public void onBillingServiceDisconnected() {
7290 * Query your product details for in-app items.
7391 * Typically called after billing client is connected.
7492 */
93+ @ Override
7594 public void queryProductDetails (List <String > productIds , OnProductDetailsListener listener ) {
7695 if (billingClient == null || !billingClient .isReady ()) {
7796 return ;
@@ -114,10 +133,10 @@ public void queryProductDetails(List<String> productIds, OnProductDetailsListene
114133 });
115134 }
116135
117-
118136 /**
119137 * Launch the billing flow for a particular product.
120138 */
139+ @ Override
121140 public BillingFlowLauncher initiatePurchase (String productId ) {
122141 ProductDetails details = productDetailsMap .get (productId );
123142 if (details != null && billingClient != null ) {
@@ -146,14 +165,131 @@ public BillingFlowLauncher initiatePurchase(String productId) {
146165 return null ;
147166 }
148167
149-
150168 /**
151169 * Initialize Mobile Ads (usually done once in your app, but
152170 * can be done here if needed for the support screen).
153171 */
172+ @ Override
154173 public AdRequest initMobileAds () {
155174 AdUtils .initialize (context );
156175 return new AdRequest .Builder ().build ();
157176 }
158177
159- }
178+ @ Override
179+ public void setPurchaseStatusListener (PurchaseStatusListener listener ) {
180+ this .purchaseStatusListener = listener ;
181+ if (listener != null ) {
182+ for (String productId : grantedEntitlements ) {
183+ listener .onPurchaseAcknowledged (productId , false );
184+ }
185+ }
186+ }
187+
188+ @ Override
189+ public void refreshPurchases () {
190+ if (billingClient == null || !billingClient .isReady ()) {
191+ return ;
192+ }
193+
194+ QueryPurchasesParams params = QueryPurchasesParams .newBuilder ()
195+ .setProductType (BillingClient .ProductType .INAPP )
196+ .build ();
197+
198+ billingClient .queryPurchasesAsync (params , (billingResult , purchasesList ) -> {
199+ if (billingResult .getResponseCode () == BillingClient .BillingResponseCode .OK ) {
200+ handlePurchaseUpdates (billingResult , purchasesList );
201+ }
202+ });
203+ }
204+
205+ private void handlePurchaseUpdates (BillingResult billingResult , List <Purchase > purchases ) {
206+ if (billingResult .getResponseCode () != BillingClient .BillingResponseCode .OK || purchases == null ) {
207+ return ;
208+ }
209+
210+ Set <String > activeEntitlements = new HashSet <>();
211+ boolean shouldPersist = false ;
212+
213+ for (Purchase purchase : purchases ) {
214+ List <String > productIds = purchase .getProducts ();
215+ if (productIds .isEmpty ()) {
216+ continue ;
217+ }
218+
219+ int rawState = getRawPurchaseState (purchase );
220+ if (rawState == Purchase .PurchaseState .PURCHASED ) {
221+ if (!purchase .isAcknowledged ()) {
222+ acknowledgePurchase (purchase );
223+ }
224+ for (String productId : productIds ) {
225+ activeEntitlements .add (productId );
226+ if (grantedEntitlements .add (productId )) {
227+ shouldPersist = true ;
228+ notifyPurchaseAcknowledged (productId , true );
229+ }
230+ }
231+ } else {
232+ for (String productId : productIds ) {
233+ if (grantedEntitlements .remove (productId )) {
234+ shouldPersist = true ;
235+ notifyPurchaseRevoked (productId );
236+ }
237+ }
238+ }
239+ }
240+
241+ Set <String > revokedProducts = new HashSet <>(grantedEntitlements );
242+ revokedProducts .removeAll (activeEntitlements );
243+ if (!revokedProducts .isEmpty ()) {
244+ shouldPersist = true ;
245+ }
246+ for (String productId : revokedProducts ) {
247+ if (grantedEntitlements .remove (productId )) {
248+ notifyPurchaseRevoked (productId );
249+ }
250+ }
251+
252+ if (shouldPersist ) {
253+ persistGrantedEntitlements ();
254+ }
255+ }
256+
257+ private void acknowledgePurchase (Purchase purchase ) {
258+ if (billingClient == null ) {
259+ return ;
260+ }
261+ AcknowledgePurchaseParams params = AcknowledgePurchaseParams .newBuilder ()
262+ .setPurchaseToken (purchase .getPurchaseToken ())
263+ .build ();
264+ billingClient .acknowledgePurchase (params , billingResult -> {
265+ // No-op: handled by entitlement updates once Google confirms the acknowledgement.
266+ });
267+ }
268+
269+ private void notifyPurchaseAcknowledged (String productId , boolean isNewPurchase ) {
270+ if (purchaseStatusListener != null ) {
271+ purchaseStatusListener .onPurchaseAcknowledged (productId , isNewPurchase );
272+ }
273+ }
274+
275+ private void notifyPurchaseRevoked (String productId ) {
276+ if (purchaseStatusListener != null ) {
277+ purchaseStatusListener .onPurchaseRevoked (productId );
278+ }
279+ }
280+
281+ private int getRawPurchaseState (Purchase purchase ) {
282+ try {
283+ JSONObject jsonObject = new JSONObject (purchase .getOriginalJson ());
284+ return jsonObject .optInt ("purchaseState" , Purchase .PurchaseState .UNSPECIFIED_STATE );
285+ } catch (JSONException exception ) {
286+ return purchase .getPurchaseState ();
287+ }
288+ }
289+
290+ private void persistGrantedEntitlements () {
291+ billingPrefs .edit ()
292+ .putStringSet (KEY_GRANTED_PRODUCTS , new HashSet <>(grantedEntitlements ))
293+ .apply ();
294+ }
295+ }
0 commit comments