Browse Source

实现google支付流程

zk 1 year ago
parent
commit
ab7a397d9e

+ 0 - 83
app/src/main/java/com/atmob/voiceai/data/repositories/AccountRepository.java

@@ -1,83 +0,0 @@
-package com.atmob.voiceai.data.repositories;
-
-
-import android.os.Handler;
-
-import androidx.lifecycle.LiveData;
-import androidx.lifecycle.MutableLiveData;
-
-import com.atmob.app.lib.handler.RxHttpHandler;
-import com.atmob.voiceai.data.api.AtmobApi;
-import com.atmob.voiceai.data.api.bean.MemberInfoBean;
-import com.atmob.voiceai.data.api.request.BaseRequest;
-import com.atmob.voiceai.data.api.response.UserInfoResponse;
-
-import java.util.concurrent.TimeUnit;
-
-import javax.inject.Inject;
-import javax.inject.Singleton;
-
-import atmob.reactivex.rxjava3.annotations.NonNull;
-import atmob.reactivex.rxjava3.core.Single;
-import atmob.reactivex.rxjava3.core.SingleObserver;
-import atmob.reactivex.rxjava3.disposables.Disposable;
-import atmob.rxjava.utils.RxJavaUtil;
-
-@Singleton
-public class AccountRepository {
-
-    private final AtmobApi atmobApi;
-    private final MemberRepository memberRepository;
-    private boolean requestUserInfoDisabled;
-    private final Handler refreshMemberHandler = new Handler();
-
-    @Inject
-    public AccountRepository(AtmobApi atmobApi, MemberRepository memberRepository) {
-        this.atmobApi = atmobApi;
-        this.memberRepository = memberRepository;
-        refreshUserData();
-    }
-
-
-    private void refreshUserData() {
-        if (requestUserInfoDisabled) {
-            return;
-        }
-        userInfo().subscribe(new SingleObserver<UserInfoResponse>() {
-
-
-            @Override
-            public void onSubscribe(@NonNull Disposable d) {
-                requestUserInfoDisabled = true;
-            }
-
-            @Override
-            public void onSuccess(@NonNull UserInfoResponse userInfoResponse) {
-                requestUserInfoDisabled = false;
-            }
-
-            @Override
-            public void onError(@NonNull Throwable e) {
-                requestUserInfoDisabled = false;
-            }
-        });
-    }
-
-
-    private Single<UserInfoResponse> userInfo() {
-        return atmobApi.userInfo(new BaseRequest())
-                .compose(RxHttpHandler.handle(true))
-                .retryWhen(RxJavaUtil.exponentialBackOff(null, 3, TimeUnit.SECONDS))
-                .compose(RxJavaUtil.SingleSchedule.io2Main())
-                .doOnSuccess(userInfoResponse -> {
-                    MemberInfoBean memberInfo = userInfoResponse.getMemberInfo();
-                    if (memberInfo != null) {
-                        memberRepository.setIsMember(memberInfo.isIsMember());
-                        refreshMemberHandler.removeCallbacksAndMessages(null);
-                        if (memberInfo.isIsMember()) {
-                            refreshMemberHandler.postDelayed(this::refreshUserData, memberInfo.getExpireTime() - memberInfo.getTimestamp());
-                        }
-                    }
-                });
-    }
-}

+ 92 - 4
app/src/main/java/com/atmob/voiceai/data/repositories/MemberRepository.java

@@ -1,19 +1,34 @@
 package com.atmob.voiceai.data.repositories;
 
 
+import android.os.Handler;
+
 import androidx.lifecycle.MutableLiveData;
 
+import com.android.billingclient.api.BillingClient;
 import com.atmob.app.lib.handler.RxHttpHandler;
+import com.atmob.common.logging.AtmobLog;
 import com.atmob.voiceai.data.api.AtmobApi;
+import com.atmob.voiceai.data.api.bean.MemberInfoBean;
+import com.atmob.voiceai.data.api.request.BaseRequest;
 import com.atmob.voiceai.data.api.request.OrderPayRequest;
+import com.atmob.voiceai.data.api.request.OrderStatusRequest;
 import com.atmob.voiceai.data.api.request.PayGoodsRequest;
 import com.atmob.voiceai.data.api.response.OrderPayResponse;
+import com.atmob.voiceai.data.api.response.OrderStatusResponse;
 import com.atmob.voiceai.data.api.response.PayGoodsResponse;
+import com.atmob.voiceai.data.api.response.UserInfoResponse;
+import com.atmob.voiceai.utils.BoxingUtil;
+
+import java.util.concurrent.TimeUnit;
 
 import javax.inject.Inject;
 import javax.inject.Singleton;
 
+import atmob.reactivex.rxjava3.annotations.NonNull;
 import atmob.reactivex.rxjava3.core.Single;
+import atmob.reactivex.rxjava3.core.SingleObserver;
+import atmob.reactivex.rxjava3.disposables.Disposable;
 import atmob.rxjava.utils.RxJavaUtil;
 
 @Singleton
@@ -22,11 +37,13 @@ public class MemberRepository {
 
     private final MutableLiveData<Boolean> isMember = new MutableLiveData<>();
     private final AtmobApi atmobApi;
-
+    private boolean requestUserInfoDisabled;
+    private final Handler refreshMemberHandler = new Handler();
 
     @Inject
     public MemberRepository(AtmobApi atmobApi) {
         this.atmobApi = atmobApi;
+        refreshUserData();
     }
 
 
@@ -34,9 +51,7 @@ public class MemberRepository {
         return isMember;
     }
 
-    public void setIsMember(Boolean isMember) {
-        this.isMember.setValue(isMember);
-    }
+
 
 
     public Single<PayGoodsResponse> payGoodsList() {
@@ -50,4 +65,77 @@ public class MemberRepository {
                 .compose(RxHttpHandler.handle(false))
                 .compose(RxJavaUtil.SingleSchedule.io2Main());
     }
+
+    /**
+     * 0-查询失败,继续轮询
+     * 1-未支付,继续轮询
+     * 2-支付成功
+     * 3-支付关闭
+     * 4-已退款
+     *
+     * @param outTradeNo
+     * @param receiptData
+     * @return
+     */
+    public Single<Boolean> orderStatus(String outTradeNo, String receiptData) {
+        return atmobApi.orderStatus(new OrderStatusRequest(outTradeNo, receiptData))
+                .compose(RxHttpHandler.handle(false))
+                .map(OrderStatusResponse::getPayStatus)
+                .map(integer -> {
+                    AtmobLog.d("orderStatus", "status ==> " + integer);
+                    if (BoxingUtil.boxing(integer) == 0 || BoxingUtil.boxing(integer) == 1) {
+                        throw new Exception("支付未完成, status ==> " + integer);
+                    }
+                    return integer;
+                })
+                .retryWhen(RxJavaUtil.retryWhen(null, 100, 3, TimeUnit.SECONDS))
+                .onErrorReturnItem(0)
+                .map(integer -> integer == 2)
+                .compose(RxJavaUtil.SingleSchedule.io2Main())
+                .doOnTerminate(this::refreshUserData);
+    }
+
+
+    public void refreshUserData() {
+        if (requestUserInfoDisabled) {
+            return;
+        }
+        userInfo().subscribe(new SingleObserver<UserInfoResponse>() {
+
+
+            @Override
+            public void onSubscribe(@NonNull Disposable d) {
+                requestUserInfoDisabled = true;
+            }
+
+            @Override
+            public void onSuccess(@NonNull UserInfoResponse userInfoResponse) {
+                requestUserInfoDisabled = false;
+            }
+
+            @Override
+            public void onError(@NonNull Throwable e) {
+                requestUserInfoDisabled = false;
+            }
+        });
+    }
+
+
+    private Single<UserInfoResponse> userInfo() {
+        return atmobApi.userInfo(new BaseRequest())
+                .compose(RxHttpHandler.handle(true))
+                .retryWhen(RxJavaUtil.exponentialBackOff(null, 3, TimeUnit.SECONDS))
+                .compose(RxJavaUtil.SingleSchedule.io2Main())
+                .doOnSuccess(userInfoResponse -> {
+                    MemberInfoBean memberInfo = userInfoResponse.getMemberInfo();
+                    if (memberInfo != null) {
+                        isMember.setValue(memberInfo.isIsMember());
+                        refreshMemberHandler.removeCallbacksAndMessages(null);
+                        if (memberInfo.isIsMember()) {
+                            refreshMemberHandler.postDelayed(this::refreshUserData, memberInfo.getExpireTime() - memberInfo.getTimestamp());
+                        }
+                    }
+                });
+    }
+
 }

+ 2 - 2
app/src/main/java/com/atmob/voiceai/module/main/MainViewModel.java

@@ -1,7 +1,7 @@
 package com.atmob.voiceai.module.main;
 
 import com.atmob.app.lib.base.BaseViewModel;
-import com.atmob.voiceai.data.repositories.AccountRepository;
+import com.atmob.voiceai.data.repositories.MemberRepository;
 
 import javax.inject.Inject;
 
@@ -13,6 +13,6 @@ public class MainViewModel extends BaseViewModel {
 
 
     @Inject
-    public MainViewModel(AccountRepository accountRepository) {
+    public MainViewModel(MemberRepository memberRepository) {
     }
 }

+ 52 - 3
app/src/main/java/com/atmob/voiceai/module/subscription/SubscriptionPageViewModel.java

@@ -7,23 +7,26 @@ import androidx.lifecycle.MutableLiveData;
 
 import com.android.billingclient.api.BillingClient;
 import com.android.billingclient.api.BillingResult;
+import com.android.billingclient.api.Purchase;
 import com.atmob.app.lib.base.BaseViewModel;
+import com.atmob.app.lib.handler.RxHttpHandler;
 import com.atmob.app.lib.livedata.SingleLiveEvent;
 import com.atmob.common.data.KVUtils;
 import com.atmob.common.logging.AtmobLog;
 import com.atmob.common.runtime.ActivityUtil;
 import com.atmob.common.runtime.ContextUtil;
 import com.atmob.voiceai.R;
+import com.atmob.voiceai.data.api.AtmobApi;
 import com.atmob.voiceai.data.api.bean.GoodsBean;
 import com.atmob.voiceai.data.api.bean.PayOptionsBean;
 import com.atmob.voiceai.data.api.response.OrderPayResponse;
 import com.atmob.voiceai.data.repositories.MemberRepository;
 import com.atmob.voiceai.helper.ErrorHelper;
 import com.atmob.voiceai.sdk.billing.GPBillingClient;
+import com.atmob.voiceai.utils.BoxingUtil;
 import com.atmob.voiceai.utils.SpannableUtil;
 import com.atmob.voiceai.utils.ToastUtil;
 
-import java.util.HashMap;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
 
@@ -33,6 +36,7 @@ import atmob.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
 import atmob.reactivex.rxjava3.annotations.NonNull;
 import atmob.reactivex.rxjava3.core.Observable;
 import atmob.reactivex.rxjava3.core.ObservableSource;
+import atmob.reactivex.rxjava3.core.Single;
 import atmob.reactivex.rxjava3.core.SingleObserver;
 import atmob.reactivex.rxjava3.core.SingleSource;
 import atmob.reactivex.rxjava3.disposables.Disposable;
@@ -61,7 +65,6 @@ public class SubscriptionPageViewModel extends BaseViewModel {
     private GoodsBean checkGoodsBean;
     private boolean isRequestSubmitOrder;
 
-    private final HashMap<String, GoodsBean> orderIdItemMap = new HashMap<>();
 
     @Inject
     public SubscriptionPageViewModel(MemberRepository memberRepository, GPBillingClient gpBillingClient) {
@@ -125,7 +128,22 @@ public class SubscriptionPageViewModel extends BaseViewModel {
     }
 
     public void onRestoreClick() {
+        queryPurchase()
+                .subscribe(new SingleObserver<List<Purchase>>() {
+                    @Override
+                    public void onSubscribe(@NonNull Disposable d) {
+                        addDisposable(d);
+                    }
 
+                    @Override
+                    public void onSuccess(@NonNull List<Purchase> purchases) {
+                    }
+
+                    @Override
+                    public void onError(@NonNull Throwable e) {
+
+                    }
+                });
     }
 
 
@@ -201,13 +219,13 @@ public class SubscriptionPageViewModel extends BaseViewModel {
                     @Override
                     public void onSuccess(@NonNull OrderPayResponse orderPayResponse) {
                         isRequestSubmitOrder = false;
-                        orderIdItemMap.put(orderPayResponse.getOutTradeNo(), checkGoodsBean);
                         requestPayment(checkGoodsBean, orderPayResponse.getOutTradeNo());
                     }
 
                     @Override
                     public void onError(@NonNull Throwable e) {
                         isRequestSubmitOrder = false;
+                        ErrorHelper.errorThrowableToast(e, ToastUtil.LENGTH_SHORT);
                     }
                 });
     }
@@ -225,7 +243,30 @@ public class SubscriptionPageViewModel extends BaseViewModel {
                     @Override
                     public void onSuccess(@NonNull BillingResult billingResult) {
                         AtmobLog.d(TAG, "requestPayment onSuccess: " + billingResult);
+                        gpBillingClient.registerFlowListener(this, orderNo, purchase -> queryOrderStatus(bean, orderNo, purchase));
+                    }
 
+                    @Override
+                    public void onError(@NonNull Throwable e) {
+
+                    }
+                });
+    }
+
+    private void queryOrderStatus(GoodsBean bean, String orderNo, Purchase purchase) {
+        memberRepository.orderStatus(orderNo, purchase.getPurchaseToken())
+                .subscribe(new SingleObserver<Boolean>() {
+                    @Override
+                    public void onSubscribe(@NonNull Disposable d) {
+                        addDisposable(d);
+                    }
+
+                    @Override
+                    public void onSuccess(@NonNull Boolean aBoolean) {
+                        if (BoxingUtil.boxing(aBoolean)) {
+                            ToastUtil.show(R.string.pay_success, ToastUtil.LENGTH_SHORT);
+                            finishEvent.call();
+                        }
                     }
 
                     @Override
@@ -240,9 +281,17 @@ public class SubscriptionPageViewModel extends BaseViewModel {
     }
 
 
+    private Single<List<Purchase>> queryPurchase() {
+        return gpBillingClient.queryPurchase(BillingClient.ProductType.SUBS)
+                .filter(purchases -> purchases != null && !purchases.isEmpty())
+                .toSingle()
+                .compose(RxJavaUtil.SingleSchedule.io2Main());
+    }
+
     @Override
     protected void onCleared() {
         super.onCleared();
         gpBillingClient.endUrgentConnection();
+        gpBillingClient.unregisterAllFlowListener(this);
     }
 }

+ 10 - 2
app/src/main/java/com/atmob/voiceai/sdk/billing/BillingStrategy.java

@@ -8,11 +8,16 @@ import androidx.annotation.Nullable;
 
 import com.android.billingclient.api.BillingClient;
 import com.android.billingclient.api.BillingResult;
+import com.android.billingclient.api.Purchase;
 import com.atmob.voiceai.sdk.billing.bean.GPProductInfo;
-import com.atmob.voiceai.sdk.billing.operation.QueryGoods;
+import com.atmob.voiceai.sdk.billing.operation.Purchases;
+import com.atmob.voiceai.sdk.billing.operation.QueryDetails;
 import com.atmob.voiceai.sdk.billing.operation.Subscription;
 
+import java.util.List;
+
 import atmob.reactivex.rxjava3.core.Single;
+import atmob.reactivex.rxjava3.core.SingleSource;
 
 public class BillingStrategy {
 
@@ -27,7 +32,7 @@ public class BillingStrategy {
     }
 
     public Single<GPProductInfo> queryGoodsDetails(String productType, @NonNull String productId, @Nullable String basePlanId, @Nullable String legacyProductId) {
-        return QueryGoods.queryGoodsDetails(billingClient, productType, productId, basePlanId, legacyProductId);
+        return QueryDetails.queryGoodsDetails(billingClient, productType, productId, basePlanId, legacyProductId);
     }
 
 
@@ -36,4 +41,7 @@ public class BillingStrategy {
     }
 
 
+    public SingleSource<List<Purchase>> queryPurchase(@BillingClient.ProductType String productType) {
+        return Purchases.queryPurchase(billingClient, productType);
+    }
 }

+ 61 - 1
app/src/main/java/com/atmob/voiceai/sdk/billing/GPBillingClient.java

@@ -10,6 +10,7 @@ import androidx.lifecycle.DefaultLifecycleObserver;
 import androidx.lifecycle.LifecycleOwner;
 import androidx.lifecycle.ProcessLifecycleOwner;
 
+import com.android.billingclient.api.AccountIdentifiers;
 import com.android.billingclient.api.BillingClient;
 import com.android.billingclient.api.BillingClientStateListener;
 import com.android.billingclient.api.BillingResult;
@@ -21,7 +22,10 @@ import com.atmob.common.runtime.ProcessUtil;
 import com.atmob.voiceai.sdk.billing.bean.GPProductInfo;
 import com.atmob.voiceai.utils.ToastUtil;
 
+import java.util.HashMap;
+import java.util.Iterator;
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
@@ -67,6 +71,8 @@ public class GPBillingClient implements PurchasesUpdatedListener {
 
     private final BillingStrategy billingStrategy;
 
+    private final HashMap<Object, HashMap<String, BillingFlowListener>> billingFlowListenerMap = new HashMap<>();
+
     @Inject
     public GPBillingClient(Application context) {
         billingClient = BillingClient.newBuilder(context)
@@ -270,8 +276,44 @@ public class GPBillingClient implements PurchasesUpdatedListener {
 
     @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 + "]");
+        if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && list != null) {
+            for (Purchase purchase : list) {
+                AccountIdentifiers accountIdentifiers = purchase.getAccountIdentifiers();
+                if (accountIdentifiers == null) {
+                    continue;
+                }
+                String obfuscatedAccountId = accountIdentifiers.getObfuscatedAccountId();
+                Iterator<Map.Entry<Object, HashMap<String, BillingFlowListener>>> iterator = billingFlowListenerMap.entrySet().iterator();
+                while (iterator.hasNext()) {
+                    Map.Entry<Object, HashMap<String, BillingFlowListener>> next = iterator.next();
+                    HashMap<String, BillingFlowListener> value = next.getValue();
+                    BillingFlowListener billingFlowListener = value.get(obfuscatedAccountId);
+                    if (billingFlowListener != null) {
+                        billingFlowListener.onSuccess(purchase);
+                        iterator.remove();
+                    }
+                }
+            }
+        }
+        billingFlowListenerMap.clear();
+    }
+
+
+    public Single<List<Purchase>> queryPurchase(@BillingClient.ProductType String productType) {
+        return Single.fromCallable(() -> {
+                    if (ProcessUtil.isMainThread()) {
+                        throw new IllegalStateException("queryPurchase 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<List<Purchase>>>) aBoolean
+                        -> billingStrategy.queryPurchase(productType));
     }
 
 
@@ -314,4 +356,22 @@ public class GPBillingClient implements PurchasesUpdatedListener {
                 .flatMap((Function<Boolean, SingleSource<GPProductInfo>>) aBoolean
                         -> billingStrategy.queryGoodsDetails(productType, productId, basePlanId, legacyProductId));
     }
+
+
+    public void registerFlowListener(Object tag, String orderId, BillingFlowListener listener) {
+        HashMap<String, BillingFlowListener> map = billingFlowListenerMap.get(tag);
+        if (map == null) {
+            map = new HashMap<>();
+            billingFlowListenerMap.put(tag, map);
+        }
+        map.put(orderId, listener);
+    }
+
+    public void unregisterAllFlowListener(Object tag) {
+        billingFlowListenerMap.remove(tag);
+    }
+
+    public interface BillingFlowListener {
+        void onSuccess(Purchase purchase);
+    }
 }

+ 31 - 0
app/src/main/java/com/atmob/voiceai/sdk/billing/operation/Purchases.java

@@ -0,0 +1,31 @@
+package com.atmob.voiceai.sdk.billing.operation;
+
+import com.android.billingclient.api.BillingClient;
+import com.android.billingclient.api.QueryPurchasesParams;
+import com.atmob.common.logging.AtmobLog;
+
+import java.util.List;
+
+import atmob.reactivex.rxjava3.core.Single;
+
+public class Purchases {
+
+    private static final String TAG = Purchases.class.getSimpleName();
+
+    public static Single<List<com.android.billingclient.api.Purchase>> queryPurchase(BillingClient billingClient, @BillingClient.ProductType String productType) {
+        return Single.create(emitter -> billingClient.queryPurchasesAsync(
+                QueryPurchasesParams.newBuilder()
+                        .setProductType(productType)
+                        .build(),
+                (billingResult, list) -> {
+                    AtmobLog.d(TAG, "Query purchase finished with result: " + billingResult.getResponseCode() + " " + billingResult.getDebugMessage());
+                    if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
+                        AtmobLog.d(TAG, "Query purchase finished with result: " + list);
+                        emitter.onSuccess(list);
+                    } else {
+                        emitter.onError(new Exception("Query purchase failed with result: " + billingResult.getResponseCode() + " " + billingResult.getDebugMessage()));
+                    }
+                }
+        ));
+    }
+}

+ 5 - 2
app/src/main/java/com/atmob/voiceai/sdk/billing/operation/QueryGoods.java

@@ -5,7 +5,9 @@ import androidx.annotation.Nullable;
 
 import com.android.billingclient.api.BillingClient;
 import com.android.billingclient.api.ProductDetails;
+import com.android.billingclient.api.Purchase;
 import com.android.billingclient.api.QueryProductDetailsParams;
+import com.android.billingclient.api.QueryPurchasesParams;
 import com.android.billingclient.api.SkuDetails;
 import com.android.billingclient.api.SkuDetailsParams;
 import com.atmob.common.logging.AtmobLog;
@@ -17,9 +19,10 @@ import java.util.Objects;
 
 import atmob.reactivex.rxjava3.core.Single;
 
-public class QueryGoods {
+public class QueryDetails {
+
+    private static final String TAG = QueryDetails.class.getSimpleName();
 
-    private static final String TAG = QueryGoods.class.getSimpleName();
 
     public static Single<GPProductInfo> queryGoodsDetails(BillingClient billingClient, String productType, @NonNull String productId, @Nullable String basePlanId, @Nullable String legacyProductId) {
         if (billingClient.isFeatureSupported(BillingClient.FeatureType.PRODUCT_DETAILS).getResponseCode() == BillingClient.BillingResponseCode.OK) {

+ 1 - 0
app/src/main/res/values/strings.xml

@@ -105,4 +105,5 @@
     <string name="dialog_retention_cancel">No thanks,I will miss out</string>
     <string name="please_select_goods">Please select a subscription product.</string>
     <string name="no_pay_way">Unable to make a payment at the moment.</string>
+    <string name="pay_success">Subscription Successful</string>
 </resources>