|
|
@@ -0,0 +1,317 @@
|
|
|
+package com.atmob.voiceai.sdk.billing;
|
|
|
+
|
|
|
+
|
|
|
+import android.app.Activity;
|
|
|
+import android.app.Application;
|
|
|
+
|
|
|
+import androidx.annotation.NonNull;
|
|
|
+import androidx.annotation.Nullable;
|
|
|
+import androidx.lifecycle.DefaultLifecycleObserver;
|
|
|
+import androidx.lifecycle.LifecycleOwner;
|
|
|
+import androidx.lifecycle.ProcessLifecycleOwner;
|
|
|
+
|
|
|
+import com.android.billingclient.api.BillingClient;
|
|
|
+import com.android.billingclient.api.BillingClientStateListener;
|
|
|
+import com.android.billingclient.api.BillingResult;
|
|
|
+import com.android.billingclient.api.PendingPurchasesParams;
|
|
|
+import com.android.billingclient.api.Purchase;
|
|
|
+import com.android.billingclient.api.PurchasesUpdatedListener;
|
|
|
+import com.atmob.common.logging.AtmobLog;
|
|
|
+import com.atmob.common.runtime.ProcessUtil;
|
|
|
+import com.atmob.voiceai.sdk.billing.bean.GPProductInfo;
|
|
|
+import com.atmob.voiceai.utils.ToastUtil;
|
|
|
+
|
|
|
+import java.util.List;
|
|
|
+import java.util.concurrent.LinkedBlockingQueue;
|
|
|
+import java.util.concurrent.ThreadPoolExecutor;
|
|
|
+import java.util.concurrent.TimeUnit;
|
|
|
+import java.util.concurrent.TimeoutException;
|
|
|
+import java.util.concurrent.atomic.AtomicInteger;
|
|
|
+import java.util.concurrent.locks.ReentrantLock;
|
|
|
+
|
|
|
+import javax.inject.Inject;
|
|
|
+import javax.inject.Singleton;
|
|
|
+
|
|
|
+import atmob.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
|
|
+import atmob.reactivex.rxjava3.core.Observable;
|
|
|
+import atmob.reactivex.rxjava3.core.ObservableSource;
|
|
|
+import atmob.reactivex.rxjava3.core.Observer;
|
|
|
+import atmob.reactivex.rxjava3.core.Single;
|
|
|
+import atmob.reactivex.rxjava3.core.SingleSource;
|
|
|
+import atmob.reactivex.rxjava3.disposables.Disposable;
|
|
|
+import atmob.reactivex.rxjava3.functions.Function;
|
|
|
+import atmob.reactivex.rxjava3.schedulers.Schedulers;
|
|
|
+
|
|
|
+@Singleton
|
|
|
+public class GPBillingClient implements PurchasesUpdatedListener {
|
|
|
+
|
|
|
+ private static final String TAG = GPBillingClient.class.getSimpleName();
|
|
|
+
|
|
|
+ private final int BILLING_CLIENT_CONNECTED_TIMEOUT = 5000;
|
|
|
+ private final int TRY_CON_MAX_POW_TIMES = 3;
|
|
|
+
|
|
|
+ private final int TRY_URGENT_CON_TIMES = 300;
|
|
|
+
|
|
|
+ private final int TRY_URGENT_DELAY_TIME_MILLISECONDS = 200;
|
|
|
+
|
|
|
+ private final BillingClient billingClient;
|
|
|
+
|
|
|
+ private Disposable powConnectionDisposable;
|
|
|
+ private Disposable urgentConnectionDisposable;
|
|
|
+
|
|
|
+ private final ThreadPoolExecutor threadPoolExecutor;
|
|
|
+
|
|
|
+ private final ReentrantLock lock = new ReentrantLock();
|
|
|
+
|
|
|
+ private Runnable urgentConnectionRunnable;
|
|
|
+
|
|
|
+ private final BillingStrategy billingStrategy;
|
|
|
+
|
|
|
+ @Inject
|
|
|
+ public GPBillingClient(Application context) {
|
|
|
+ billingClient = BillingClient.newBuilder(context)
|
|
|
+ .setListener(this)
|
|
|
+ .enablePendingPurchases(PendingPurchasesParams.newBuilder().enablePrepaidPlans().enableOneTimeProducts().build())
|
|
|
+ .build();
|
|
|
+ billingStrategy = new BillingStrategy(billingClient);
|
|
|
+ threadPoolExecutor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>());
|
|
|
+ addApplicationLifecycleListener();
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 开启重要的尝试连接操作
|
|
|
+ */
|
|
|
+ public void startUrgentConnection() {
|
|
|
+ urgentConnectionRunnable = () -> {
|
|
|
+ if (billingClient.isReady()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ doUrgentConnection();
|
|
|
+ };
|
|
|
+ threadPoolExecutor.execute(urgentConnectionRunnable);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 关闭重要的尝试连接操作
|
|
|
+ */
|
|
|
+ public void endUrgentConnection() {
|
|
|
+ AtmobLog.d(TAG, "End connection.——Urgent");
|
|
|
+ if (urgentConnectionDisposable != null && !urgentConnectionDisposable.isDisposed()) {
|
|
|
+ urgentConnectionDisposable.dispose();
|
|
|
+ }
|
|
|
+ while (lock.getHoldCount() > 0) {
|
|
|
+ lock.unlock();
|
|
|
+ }
|
|
|
+ threadPoolExecutor.remove(urgentConnectionRunnable);
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ private void doUrgentConnection() {
|
|
|
+ Observable.create(emitter -> {
|
|
|
+ billingClient.startConnection(new BillingClientStateListener() {
|
|
|
+ @Override
|
|
|
+ public void onBillingServiceDisconnected() {
|
|
|
+ AtmobLog.e(TAG, "onBillingServiceDisconnected() called.——Urgent");
|
|
|
+ emitter.onError(new RuntimeException("Billing service disconnected.——Urgent"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onBillingSetupFinished(@NonNull BillingResult billingResult) {
|
|
|
+ AtmobLog.d(TAG, "onBillingSetupFinished() called with: billingResult = [" + billingResult + "]——Urgent");
|
|
|
+ if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
|
|
|
+ emitter.onNext(billingResult);
|
|
|
+ emitter.onComplete();
|
|
|
+ } else {
|
|
|
+ emitter.onError(new RuntimeException("Billing setup failed.——Urgent"));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ })
|
|
|
+ .retryWhen(new Function<Observable<Throwable>, ObservableSource<?>>() {
|
|
|
+
|
|
|
+ private final AtomicInteger count = new AtomicInteger(0);
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public ObservableSource<?> apply(Observable<Throwable> throwableObservable) {
|
|
|
+ return throwableObservable.takeWhile(throwable -> {
|
|
|
+ if (count.getAndIncrement() < TRY_URGENT_CON_TIMES) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ throw throwable;
|
|
|
+ })
|
|
|
+ .flatMap((Function<Throwable, ObservableSource<?>>) throwable -> Observable.timer(TRY_URGENT_DELAY_TIME_MILLISECONDS, TimeUnit.MILLISECONDS));
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ .subscribe(new Observer<Object>() {
|
|
|
+ @Override
|
|
|
+ public void onSubscribe(@NonNull Disposable d) {
|
|
|
+ urgentConnectionDisposable = d;
|
|
|
+ lock.lock();
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onNext(@NonNull Object o) {
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onError(@NonNull Throwable e) {
|
|
|
+ AtmobLog.d(TAG, "Billing connection retry times exceed max retry times, stop retry.——Urgent");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onComplete() {
|
|
|
+ AtmobLog.d(TAG, "Billing connection success.——Urgent");
|
|
|
+ lock.unlock();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 应用启动时尝试连接 billing client
|
|
|
+ */
|
|
|
+ private void addApplicationLifecycleListener() {
|
|
|
+ ProcessLifecycleOwner.get().getLifecycle().addObserver(new DefaultLifecycleObserver() {
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onCreate(@NonNull LifecycleOwner owner) {
|
|
|
+ if (!billingClient.isReady()) {
|
|
|
+ AtmobLog.d(TAG, "Billing client is not ready, start connection.");
|
|
|
+ startPowConnection();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onDestroy(@NonNull LifecycleOwner owner) {
|
|
|
+ endPowConnection();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ private void endPowConnection() {
|
|
|
+ AtmobLog.d(TAG, "End connection.");
|
|
|
+ if (billingClient.isReady()) {
|
|
|
+ AtmobLog.d(TAG, "Billing client is ready, end connection.");
|
|
|
+ billingClient.endConnection();
|
|
|
+ }
|
|
|
+ if (powConnectionDisposable != null && !powConnectionDisposable.isDisposed()) {
|
|
|
+ powConnectionDisposable.dispose();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void startPowConnection() {
|
|
|
+ Observable.create(emitter -> {
|
|
|
+ billingClient.startConnection(new BillingClientStateListener() {
|
|
|
+ @Override
|
|
|
+ public void onBillingServiceDisconnected() {
|
|
|
+ AtmobLog.d(TAG, "Billing service disconnected.");
|
|
|
+ emitter.onError(new RuntimeException("Billing service disconnected."));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onBillingSetupFinished(@NonNull BillingResult billingResult) {
|
|
|
+ AtmobLog.d(TAG, "Billing setup finished with result: " + billingResult.getResponseCode() + " " + billingResult.getDebugMessage());
|
|
|
+ if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
|
|
|
+ emitter.onNext(billingResult);
|
|
|
+ emitter.onComplete();
|
|
|
+ } else {
|
|
|
+ emitter.onError(new RuntimeException("Billing setup failed."));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ })
|
|
|
+ .retryWhen(new Function<Observable<Throwable>, ObservableSource<?>>() {
|
|
|
+
|
|
|
+ private final AtomicInteger count = new AtomicInteger(0);
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public ObservableSource<?> apply(Observable<Throwable> throwableObservable) {
|
|
|
+ return throwableObservable.takeWhile(throwable -> {
|
|
|
+ if (count.getAndIncrement() < TRY_CON_MAX_POW_TIMES) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ throw throwable;
|
|
|
+ })
|
|
|
+ .flatMap((Function<Throwable, ObservableSource<?>>) throwable -> {
|
|
|
+ int i = count.get();
|
|
|
+ int delay = (int) Math.pow(2, i);
|
|
|
+ return Observable.timer(delay, TimeUnit.SECONDS);
|
|
|
+ });
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .subscribeOn(Schedulers.io())
|
|
|
+ .observeOn(AndroidSchedulers.mainThread())
|
|
|
+ .subscribe(new Observer<Object>() {
|
|
|
+ @Override
|
|
|
+ public void onSubscribe(@NonNull Disposable d) {
|
|
|
+ powConnectionDisposable = d;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onNext(@NonNull Object o) {
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onError(@NonNull Throwable e) {
|
|
|
+ AtmobLog.d(TAG, "Billing connection retry times exceed max retry times, stop retry.");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onComplete() {
|
|
|
+ AtmobLog.d(TAG, "Billing connection success.");
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onPurchasesUpdated(@NonNull BillingResult billingResult, @Nullable List<Purchase> list) {
|
|
|
+ ToastUtil.show("onPurchasesUpdated() called with: billingResult = [" + billingResult + "], list = [" + list + "]", ToastUtil.LENGTH_SHORT);
|
|
|
+ AtmobLog.d(TAG, "onPurchasesUpdated() called with: billingResult = [" + billingResult + "], list = [" + list + "]");
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ public Single<BillingResult> subscriptionGoods(Activity activity, Object productDetails, String basePlanId, String orderNo) {
|
|
|
+ return Single.fromCallable(() -> {
|
|
|
+ if (ProcessUtil.isMainThread()) {
|
|
|
+ throw new IllegalStateException("launchBillingFlow must not be called on the main thread");
|
|
|
+ }
|
|
|
+ if (lock.tryLock(BILLING_CLIENT_CONNECTED_TIMEOUT, TimeUnit.MILLISECONDS)) {
|
|
|
+ lock.unlock();
|
|
|
+ return true;
|
|
|
+ } else {
|
|
|
+ throw new TimeoutException();
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .flatMap((Function<Boolean, SingleSource<BillingResult>>) aBoolean
|
|
|
+ -> billingStrategy.subscriptionGoods(activity, productDetails, basePlanId, orderNo))
|
|
|
+ .map(billingResult -> {
|
|
|
+ if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
|
|
|
+ return billingResult;
|
|
|
+ } else {
|
|
|
+ throw new IllegalStateException("launchBillingFlow failed billingResult: " + billingResult);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ public Single<GPProductInfo> querySkuDetails(String productType, @NonNull String productId, @Nullable String basePlanId, @Nullable String legacyProductId) {
|
|
|
+ return Single.fromCallable(() -> {
|
|
|
+ if (ProcessUtil.isMainThread()) {
|
|
|
+ throw new IllegalStateException("querySkuDetails must not be called on the main thread");
|
|
|
+ }
|
|
|
+ if (lock.tryLock(BILLING_CLIENT_CONNECTED_TIMEOUT, TimeUnit.MILLISECONDS)) {
|
|
|
+ lock.unlock();
|
|
|
+ return true;
|
|
|
+ } else {
|
|
|
+ throw new TimeoutException();
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .flatMap((Function<Boolean, SingleSource<GPProductInfo>>) aBoolean
|
|
|
+ -> billingStrategy.queryGoodsDetails(productType, productId, basePlanId, legacyProductId));
|
|
|
+ }
|
|
|
+}
|