package org.telegram.messenger; import android.app.Activity; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.net.Uri; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.util.Consumer; import com.android.billingclient.api.BillingClient; import com.android.billingclient.api.BillingClientStateListener; import com.android.billingclient.api.BillingFlowParams; import com.android.billingclient.api.BillingResult; import com.android.billingclient.api.ConsumeParams; import com.android.billingclient.api.ProductDetails; import com.android.billingclient.api.ProductDetailsResponseListener; import com.android.billingclient.api.Purchase; import com.android.billingclient.api.PurchasesResponseListener; import com.android.billingclient.api.PurchasesUpdatedListener; import com.android.billingclient.api.QueryProductDetailsParams; import com.android.billingclient.api.QueryPurchasesParams; import com.google.android.exoplayer2.util.Util; import org.json.JSONObject; import org.telegram.tgnet.ConnectionsManager; import org.telegram.tgnet.TLRPC; import org.telegram.ui.PremiumPreviewFragment; import java.io.InputStream; import java.text.NumberFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Currency; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; public class BillingController implements PurchasesUpdatedListener, BillingClientStateListener { public final static String PREMIUM_PRODUCT_ID = "telegram_premium"; public final static QueryProductDetailsParams.Product PREMIUM_PRODUCT = QueryProductDetailsParams.Product.newBuilder() .setProductType(BillingClient.ProductType.SUBS) .setProductId(PREMIUM_PRODUCT_ID) .build(); @Nullable public static ProductDetails PREMIUM_PRODUCT_DETAILS; private static BillingController instance; private Map> resultListeners = new HashMap<>(); private List requestingTokens = new ArrayList<>(); private Map currencyExpMap = new HashMap<>(); public static BillingController getInstance() { if (instance == null) { instance = new BillingController(ApplicationLoader.applicationContext); } return instance; } private BillingClient billingClient; private BillingController(Context ctx) { billingClient = BillingClient.newBuilder(ctx) .enablePendingPurchases() .setListener(this) .build(); } public String formatCurrency(long amount, String currency) { return formatCurrency(amount, currency, getCurrencyExp(currency)); } public String formatCurrency(long amount, String currency, int exp) { if (currency.isEmpty()) { return String.valueOf(amount); } Currency cur = Currency.getInstance(currency); if (cur != null) { NumberFormat numberFormat = NumberFormat.getCurrencyInstance(); numberFormat.setCurrency(cur); return numberFormat.format(amount / Math.pow(10, exp)); } return amount + " " + currency; } public int getCurrencyExp(String currency) { Integer exp = currencyExpMap.get(currency); if (exp == null) { return 0; } return exp; } public void startConnection() { if (isReady()) { return; } try { Context ctx = ApplicationLoader.applicationContext; InputStream in = ctx.getAssets().open("currencies.json"); JSONObject obj = new JSONObject(new String(Util.toByteArray(in), "UTF-8")); parseCurrencies(obj); in.close(); } catch (Exception e) { FileLog.e(e); } if (!BuildVars.useInvoiceBilling()) { billingClient.startConnection(this); } } private void parseCurrencies(JSONObject obj) { Iterator it = obj.keys(); while (it.hasNext()) { String key = it.next(); JSONObject currency = obj.optJSONObject(key); currencyExpMap.put(key, currency.optInt("exp")); } } public boolean isReady() { return billingClient.isReady(); } public void queryProductDetails(List products, ProductDetailsResponseListener responseListener) { if (!isReady()) { throw new IllegalStateException("Billing controller should be ready for this call!"); } billingClient.queryProductDetailsAsync(QueryProductDetailsParams.newBuilder().setProductList(products).build(), responseListener); } public void queryPurchases(String productType, PurchasesResponseListener responseListener) { billingClient.queryPurchasesAsync(QueryPurchasesParams.newBuilder().setProductType(productType).build(), responseListener); } public boolean startManageSubscription(Context ctx, String productId) { try { ctx.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(String.format("https://play.google.com/store/account/subscriptions?sku=%s&package=%s", productId, ctx.getPackageName())))); return true; } catch (ActivityNotFoundException e) { return false; } } public void addResultListener(String productId, Consumer listener) { resultListeners.put(productId, listener); } public void launchBillingFlow(Activity activity, AccountInstance accountInstance, TLRPC.InputStorePaymentPurpose paymentPurpose, List productDetails) { launchBillingFlow(activity, accountInstance, paymentPurpose, productDetails, false); } public void launchBillingFlow(Activity activity, AccountInstance accountInstance, TLRPC.InputStorePaymentPurpose paymentPurpose, List productDetails, boolean checkedConsume) { if (!isReady() || activity == null) { return; } if (paymentPurpose instanceof TLRPC.TL_inputStorePaymentGiftPremium && !checkedConsume) { queryPurchases(BillingClient.ProductType.INAPP, (billingResult, list) -> { if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) { Runnable callback = () -> launchBillingFlow(activity, accountInstance, paymentPurpose, productDetails, true); AtomicInteger productsToBeConsumed = new AtomicInteger(0); List productsConsumed = new ArrayList<>(); for (Purchase purchase : list) { if (purchase.isAcknowledged()) { for (BillingFlowParams.ProductDetailsParams params : productDetails) { String productId = params.zza().getProductId(); if (purchase.getProducts().contains(productId)) { productsToBeConsumed.incrementAndGet(); billingClient.consumeAsync(ConsumeParams.newBuilder() .setPurchaseToken(purchase.getPurchaseToken()) .build(), (billingResult1, s) -> { if (billingResult1.getResponseCode() == BillingClient.BillingResponseCode.OK) { productsConsumed.add(productId); if (productsToBeConsumed.get() == productsConsumed.size()) { callback.run(); } } }); break; } } } else { onPurchasesUpdated(BillingResult.newBuilder().setResponseCode(BillingClient.BillingResponseCode.OK).build(), Collections.singletonList(purchase)); return; } } if (productsToBeConsumed.get() == 0) { callback.run(); } } }); return; } boolean ok = billingClient.launchBillingFlow(activity, BillingFlowParams.newBuilder() .setProductDetailsParamsList(productDetails) .build()).getResponseCode() == BillingClient.BillingResponseCode.OK; if (ok) { for (BillingFlowParams.ProductDetailsParams params : productDetails) { accountInstance.getUserConfig().billingPaymentPurpose = paymentPurpose; accountInstance.getUserConfig().awaitBillingProductIds.add(params.zza().getProductId()); // params.getProductDetails().getProductId() } accountInstance.getUserConfig().saveConfig(false); } } @Override public void onPurchasesUpdated(@NonNull BillingResult billingResult, @Nullable List list) { FileLog.d("Billing purchases updated: " + billingResult + ", " + list); if (billingResult.getResponseCode() != BillingClient.BillingResponseCode.OK) { if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.USER_CANCELED) { PremiumPreviewFragment.sentPremiumBuyCanceled(); } for (int i = 0; i < UserConfig.MAX_ACCOUNT_COUNT; i++) { AccountInstance acc = AccountInstance.getInstance(i); if (!acc.getUserConfig().awaitBillingProductIds.isEmpty()) { acc.getUserConfig().awaitBillingProductIds.clear(); acc.getUserConfig().billingPaymentPurpose = null; acc.getUserConfig().saveConfig(false); } } return; } if (list == null) { return; } for (Purchase purchase : list) { if (!requestingTokens.contains(purchase.getPurchaseToken())) { for (int i = 0; i < UserConfig.MAX_ACCOUNT_COUNT; i++) { AccountInstance acc = AccountInstance.getInstance(i); if (acc.getUserConfig().awaitBillingProductIds.containsAll(purchase.getProducts()) && purchase.getPurchaseState() != Purchase.PurchaseState.PENDING) { acc.getUserConfig().awaitBillingProductIds.removeAll(purchase.getProducts()); acc.getUserConfig().saveConfig(false); if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) { if (!purchase.isAcknowledged()) { requestingTokens.add(purchase.getPurchaseToken()); TLRPC.TL_payments_assignPlayMarketTransaction req = new TLRPC.TL_payments_assignPlayMarketTransaction(); req.receipt = new TLRPC.TL_dataJSON(); req.receipt.data = purchase.getOriginalJson(); req.purpose = acc.getUserConfig().billingPaymentPurpose; acc.getConnectionsManager().sendRequest(req, (response, error) -> { if (response instanceof TLRPC.Updates) { acc.getMessagesController().processUpdates((TLRPC.Updates) response, false); requestingTokens.remove(purchase.getPurchaseToken()); for (String productId : purchase.getProducts()) { Consumer listener = resultListeners.remove(productId); if (listener != null) { listener.accept(billingResult); } } if (req.purpose instanceof TLRPC.TL_inputStorePaymentGiftPremium) { billingClient.consumeAsync(ConsumeParams.newBuilder() .setPurchaseToken(purchase.getPurchaseToken()) .build(), (billingResult1, s) -> {}); } } }, ConnectionsManager.RequestFlagFailOnServerErrors | ConnectionsManager.RequestFlagInvokeAfter); acc.getUserConfig().billingPaymentPurpose = null; acc.getUserConfig().saveConfig(false); } } } } } } } @Override public void onBillingServiceDisconnected() { FileLog.d("Billing service disconnected"); } @Override public void onBillingSetupFinished(@NonNull BillingResult setupBillingResult) { if (setupBillingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) { queryProductDetails(Collections.singletonList(PREMIUM_PRODUCT), (billingResult, list) -> { if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) { for (ProductDetails details : list) { if (details.getProductId().equals(PREMIUM_PRODUCT_ID)) { PREMIUM_PRODUCT_DETAILS = details; } } AndroidUtilities.runOnUIThread(() -> NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.billingProductDetailsUpdated)); } }); queryPurchases(BillingClient.ProductType.SUBS, this::onPurchasesUpdated); } } }