詳解定期購入

934a9e49edc3174d09ab2e09daed5062?s=47 ymnder
February 07, 2019

 詳解定期購入

Deep dive into subscriptions
DroidKaigi2019, Room2 - 2019/02/07 16:00-16:50
https://droidkaigi.jp/2019/timetable/70670

934a9e49edc3174d09ab2e09daed5062?s=128

ymnder

February 07, 2019
Tweet

Transcript

  1. 7.
  2. 8.

    8

  3. 11.
  4. 12.

    12

  5. 13.

    13

  6. 15.

    15

  7. 16.

    16

  8. 17.
  9. 26.
  10. 27.
  11. 31.

    BillingClient BillingClient 31 var client = BillingClient.newBuilder(context) .setListener(object : PurchasesUpdatedListener

    { override fun onPurchasesUpdated(responseCode: Int, purchases: MutableList<Purchase>?) { // call if purchase has done } }) .build()
  12. 32.

    Service startConnection 32 client.startConnection(object : BillingClientStateListener { override fun onBillingSetupFinished(@BillingClient.BillingResponse

    billingResponseCode: Int) { if (billingResponseCode == BillingClient.BillingResponse.OK) { // The billing client is ready. You can query purchases here. } } override fun onBillingServiceDisconnected() { // Try to restart the connection on the next request to // Google Play by calling the startConnection() method. } })
  13. 33.

    launchBillingFlow PurchasesUpdatedListener 踏 33 val flowParams = BillingFlowParams.newBuilder() .setSku("skuId") .setType(BillingClient.SkuType.SUBS)

    // SkuType.SUB for subscription .build() val responseCode = client.launchBillingFlow(activity, flowParams)
  14. 34.
  15. 35.

    BillingClient / BillingClientImpl BillingClient PlayStore 35 public static final class

    Builder { private final Context mContext; private PurchasesUpdatedListener mListener; private Builder(Context context) { mContext = context; } @UiThread public Builder setListener(PurchasesUpdatedListener listener) { mListener = listener; return this; } @UiThread public BillingClient build() { if (mContext == null) { throw new IllegalArgumentException("Please provide a valid Context."); } if (mListener == null) { throw new IllegalArgumentException(/…/); } return new BillingClientImpl(mContext, mListener); } }
  16. 36.

    BillingClient / BillingClientImpl PurchasesUpdatedListener 踏 Client 36 @UiThread public BillingClient

    build() { if (mContext == null) { throw new IllegalArgumentException("Please provide a valid Context."); } if (mListener == null) { throw new IllegalArgumentException( "Please provide a valid listener for" + " purchases updates."); } return new BillingClientImpl(mContext, mListener); }
  17. 38.
  18. 39.

    39 public @interface ClientState { /** This client was not

    yet connected to billing service or was already disconnected from it. */ int DISCONNECTED = 0; /** This client is currently in process of connecting to billing service. */ int CONNECTING = 1; /** This client is currently connected to billing service. */ int CONNECTED = 2; /** This client was already closed and shouldn't be used again. */ int CLOSED = 3; }
  19. 40.

    isReady startConnection endConnection 40 @UiThread public abstract boolean isReady(); @UiThread

    public abstract void startConnection(@NonNull final BillingClientStateListener listener); @UiThread public abstract void endConnection();
  20. 41.

    isReady 41 public boolean isReady() { return mClientState == ClientState.CONNECTED

    && mService != null && mServiceConnection != null; }
  21. 42.

    startConnection 42 public void startConnection(@NonNull BillingClientStateListener listener) { if (isReady())

    { listener.onBillingSetupFinished(BillingResponse.OK); return; } if (mClientState == ClientState.CONNECTING) { listener.onBillingSetupFinished(BillingResponse.DEVELOPER_ERROR); return; } if (mClientState == ClientState.CLOSED) { listener.onBillingSetupFinished(BillingResponse.DEVELOPER_ERROR); return; } mClientState = ClientState.CONNECTING; //… if connected, set ClientState.CONNECTED in BillingServiceConnection mClientState = ClientState.DISCONNECTED; BillingHelper.logVerbose(TAG, "Billing service unavailable on device."); listener.onBillingSetupFinished(BillingResponse.BILLING_UNAVAILABLE); }
  22. 44.

    endConnection 44 public void endConnection() { try { LocalBroadcastManager.getInstance(mApplicationContext) .unregisterReceiver(onPurchaseFinishedReceiver);

    mBroadcastManager.destroy(); if (mServiceConnection != null && mService != null) { BillingHelper.logVerbose(TAG, "Unbinding from service."); mApplicationContext.unbindService(mServiceConnection); mServiceConnection = null; } mService = null; if (mExecutorService != null) { mExecutorService.shutdownNow(); mExecutorService = null; } } catch (Exception ex) { BillingHelper.logWarn(TAG, "There was an exception while ending connection: " + ex); } finally { mClientState = ClientState.CLOSED; } }
  23. 45.

    endConnection 45 public void endConnection() { try { LocalBroadcastManager.getInstance(mApplicationContext) .unregisterReceiver(onPurchaseFinishedReceiver);

    mBroadcastManager.destroy(); if (mServiceConnection != null && mService != null) { BillingHelper.logVerbose(TAG, "Unbinding from service."); mApplicationContext.unbindService(mServiceConnection); mServiceConnection = null; } mService = null; if (mExecutorService != null) { mExecutorService.shutdownNow(); mExecutorService = null; } } catch (Exception ex) { BillingHelper.logWarn(TAG, "There was an exception while ending connection: " + ex); } finally { mClientState = ClientState.CLOSED; } }
  24. 47.

    isFeatureSupported PlayStore 47 public @BillingResponse int isFeatureSupported(@FeatureType String feature) {

    if (!isReady()) { return BillingResponse.SERVICE_DISCONNECTED; } switch (feature) { case FeatureType.SUBSCRIPTIONS: return mSubscriptionsSupported ? BillingResponse.OK : BillingResponse.FEATURE_NOT_SUPPORTED; case FeatureType.SUBSCRIPTIONS_UPDATE: return mSubscriptionUpdateSupported ? BillingResponse.OK: BillingResponse.FEATURE_NOT_SUPPORTED; case FeatureType.IN_APP_ITEMS_ON_VR: return isBillingSupportedOnVr(SkuType.INAPP); case FeatureType.SUBSCRIPTIONS_ON_VR: return isBillingSupportedOnVr(SkuType.SUBS); default: return BillingResponse.DEVELOPER_ERROR; } }
  25. 48.

    launchBillingFlow 48 public int launchBillingFlow(Activity activity, BillingFlowParams params) { if

    (!isReady()) { return broadcastFailureAndReturnBillingResponse(BillingResponse.SERVICE_DISCONNECTED); } @SkuType String skuType = params.getSkuType(); String newSku = params.getSku(); if (newSku == null) { BillingHelper.logWarn(TAG, "Please fix the input params. SKU can't be null."); return broadcastFailureAndReturnBillingResponse(BillingResponse.DEVELOPER_ERROR); } try { //… Intent intent = new Intent(activity, ProxyBillingActivity.class); PendingIntent pendingIntent = buyIntentBundle.getParcelable(RESPONSE_BUY_INTENT); intent.putExtra(RESPONSE_BUY_INTENT, pendingIntent); activity.startActivity(intent); } catch (RemoteException e) { //… } return BillingResponse.OK; }
  26. 49.

    launchBillingFlow 49 public int launchBillingFlow(Activity activity, BillingFlowParams params) { if

    (!isReady()) { return broadcastFailureAndReturnBillingResponse(BillingResponse.SERVICE_DISCONNECTED); } @SkuType String skuType = params.getSkuType(); String newSku = params.getSku(); if (newSku == null) { BillingHelper.logWarn(TAG, "Please fix the input params. SKU can't be null."); return broadcastFailureAndReturnBillingResponse(BillingResponse.DEVELOPER_ERROR); } try { //… Intent intent = new Intent(activity, ProxyBillingActivity.class); PendingIntent pendingIntent = buyIntentBundle.getParcelable(RESPONSE_BUY_INTENT); intent.putExtra(RESPONSE_BUY_INTENT, pendingIntent); activity.startActivity(intent); } catch (RemoteException e) { //… } return BillingResponse.OK; }
  27. 52.

    launchBillingFlow 52 public int launchBillingFlow(Activity activity, BillingFlowParams params) { if

    (!isReady()) { return broadcastFailureAndReturnBillingResponse(BillingResponse.SERVICE_DISCONNECTED); } @SkuType String skuType = params.getSkuType(); String newSku = params.getSku(); if (newSku == null) { BillingHelper.logWarn(TAG, "Please fix the input params. SKU can't be null."); return broadcastFailureAndReturnBillingResponse(BillingResponse.DEVELOPER_ERROR); } try { //… Intent intent = new Intent(activity, ProxyBillingActivity.class); PendingIntent pendingIntent = buyIntentBundle.getParcelable(RESPONSE_BUY_INTENT); intent.putExtra(RESPONSE_BUY_INTENT, pendingIntent); activity.startActivity(intent); } catch (RemoteException e) { //… } return BillingResponse.OK; }
  28. 56.

    queryPurchaseHistoryAsync 56 public void queryPurchaseHistoryAsync( final @SkuType String skuType, final

    PurchaseHistoryResponseListener listener) { if (!isReady()) { listener.onPurchaseHistoryResponse( BillingResponse.SERVICE_DISCONNECTED, /* purchasesList */ null); return; } executeAsync( new Runnable() { @Override public void run() { final PurchasesResult result = queryPurchasesInternal(skuType, /* queryHistory */ true); // Post the result to main thread postToUiThread( new Runnable() { @Override public void run() { listener.onPurchaseHistoryResponse( result.getResponseCode(), result.getPurchasesList()); } }); } }); }
  29. 57.

    queryPurchaseHistoryAsync 57 public void queryPurchaseHistoryAsync( final @SkuType String skuType, final

    PurchaseHistoryResponseListener listener) { if (!isReady()) { listener.onPurchaseHistoryResponse( BillingResponse.SERVICE_DISCONNECTED, /* purchasesList */ null); return; } executeAsync( new Runnable() { @Override public void run() { final PurchasesResult result = queryPurchasesInternal(skuType, /* queryHistory */ true); // Post the result to main thread postToUiThread( new Runnable() { @Override public void run() { listener.onPurchaseHistoryResponse( result.getResponseCode(), result.getPurchasesList()); } }); } }); }
  30. 60.

    queryPurchases 60 public PurchasesResult queryPurchases(@SkuType String skuType) { if (!isReady())

    { return new PurchasesResult(BillingResponse.SERVICE_DISCONNECTED, /* purchasesList */ null); } // Checking for the mandatory argument if (TextUtils.isEmpty(skuType)) { BillingHelper.logWarn(TAG, "Please provide a valid SKU type."); return new PurchasesResult(BillingResponse.DEVELOPER_ERROR, /* purchasesList */ null); } return queryPurchasesInternal(skuType, false /* queryHistory */); }
  31. 63.

    63

  32. 66.

    launchBillingFlow 66 { "autoRenewing": true, "orderId": "GPA.0000-1111-2222-33444", "packageName": "com.nikkei.newspaper", "productId":

    "skuId", "purchaseState": 0, "purchaseTime": 1549417790186, "purchaseToken": “…” }
  33. 68.

    queryPurchases 68 { "autoRenewing": true, "orderId": "GPA.0000-1111-2222-33444", "packageName": "com.nikkei.newspaper", "productId":

    "skuId", "purchaseState": 0, "purchaseTime": 1549417790186, "purchaseToken": “…” }
  34. 69.

    queryPurchases 69 { "autoRenewing": false, "orderId": "GPA.0000-1111-2222-33444", "packageName": "com.nikkei.newspaper", "productId":

    “skuId", "purchaseState": 0, "purchaseTime": 1549417790186, "purchaseToken": “…” }
  35. 72.

    BillingResponse 72 public @interface BillingResponse { int FEATURE_NOT_SUPPORTED = -2;

    int SERVICE_DISCONNECTED = -1; /** Success */ int OK = 0; /** User pressed back or canceled a dialog */ int USER_CANCELED = 1; /** Network connection is down */ int SERVICE_UNAVAILABLE = 2; /** Billing API version is not supported for the type requested */ int BILLING_UNAVAILABLE = 3; /** Requested product is not available for purchase */ int ITEM_UNAVAILABLE = 4; int DEVELOPER_ERROR = 5; /** Fatal error during the API action */ int ERROR = 6; /** Failure to purchase since item is already owned */ int ITEM_ALREADY_OWNED = 7; /** Failure to consume since item is not owned */ int ITEM_NOT_OWNED = 8; }
  36. 73.

    BillingResponse startConnection 73 return when (resultCode) { BillingClient.BillingResponse.FEATURE_NOT_SUPPORTED -> "FEATURE_NOT_SUPPORTED"

    BillingClient.BillingResponse.SERVICE_DISCONNECTED -> "SERVICE_DISCONNECTED" BillingClient.BillingResponse.OK -> "OK" BillingClient.BillingResponse.USER_CANCELED -> "USER_CANCELED" BillingClient.BillingResponse.SERVICE_UNAVAILABLE -> "SERVICE_UNAVAILABLE" BillingClient.BillingResponse.BILLING_UNAVAILABLE -> "BILLING_UNAVAILABLE" BillingClient.BillingResponse.ITEM_UNAVAILABLE -> "ITEM_UNAVAILABLE" BillingClient.BillingResponse.DEVELOPER_ERROR -> "DEVELOPER_ERROR" BillingClient.BillingResponse.ERROR -> "ERROR" BillingClient.BillingResponse.ITEM_ALREADY_OWNED -> "ITEM_ALREADY_OWNED" BillingClient.BillingResponse.ITEM_NOT_OWNED -> "ITEM_NOT_OWNED" else -> "UNDEFINED:$resultCode" }
  37. 75.
  38. 77.

    77

  39. 78.
  40. 79.

    79

  41. 80.
  42. 83.

    83

  43. 85.
  44. 87.

    87

  45. 98.
  46. 99.
  47. 101.

    BillingActivity Activity 踏 踏 Activity BillingClient Activity Activity issue ref:

    https://github.com/googlesamples/android-play-billing/ issues/95 101
  48. 108.

    launchBilling ow PublishSubject Subject Subscriber Observable Listener subscribe onNext instance

    callback PublishSubject callback onNext 踏 Rx Remote Single Wrap Repository 108
  49. 109.

    BillingManager 109 interface PlayBillingManager { fun observePurchasesUpdated(): PublishSubject<PurchasesUpdatedResponse> fun isReady():

    Boolean fun startConnection(listener: BillingClientStateListener) fun launchBillingFlow(activity: Activity, params: BillingFlowParams): Int fun queryPurchases(): Purchase.PurchasesResult fun endConnection() fun queryPurchaseHistoryAsync(skuId: String): Single<PurchasesUpdatedResponse> }
  50. 110.

    BillingApiManager 110 @Singleton class PlayBillingManagerImpl @Inject constructor( private val context:

    Context ) : PlayBillingManager, PurchasesUpdatedListener { private val publishSubject = PublishSubject.create<PurchasesUpdatedResponse>() override fun onPurchasesUpdated(responseCode: Int, purchases: MutableList<Purchase>?) { if (responseCode == BillingClient.BillingResponse.OK) { publishSubject.onNext(PurchasesUpdatedResponse(responseCode, purchases)) } else { publishSubject.onNext(PurchasesUpdatedResponse(responseCode)) } } override fun observePurchasesUpdated(): PublishSubject<PurchasesUpdatedResponse> { return publishSubject } override fun launchBillingFlow(activity: Activity, params: BillingFlowParams): Int { return billingClient.launchBillingFlow(activity, params) } }
  51. 111.

    BillingApiManager 111 @Singleton class PlayBillingManagerImpl @Inject constructor( private val context:

    Context ) : PlayBillingManager, PurchasesUpdatedListener { private val publishSubject = PublishSubject.create<PurchasesUpdatedResponse>() override fun onPurchasesUpdated(responseCode: Int, purchases: MutableList<Purchase>?) { if (responseCode == BillingClient.BillingResponse.OK) { publishSubject.onNext(PurchasesUpdatedResponse(responseCode, purchases)) } else { publishSubject.onNext(PurchasesUpdatedResponse(responseCode)) } } override fun observePurchasesUpdated(): PublishSubject<PurchasesUpdatedResponse> { return publishSubject } override fun launchBillingFlow(activity: Activity, params: BillingFlowParams): Int { return billingClient.launchBillingFlow(activity, params) } }
  52. 112.

    Remote 112 @Singleton class RemotePlayBillingImpl @Inject constructor( private val billingClient:

    PlayBillingManager ) : RemotePlayBilling { override fun launchBillingFlow(activity: Activity, skuId: String): Single<PurchasePaperResponse> { return Single.create { val flowParams = BillingFlowParams.newBuilder() .setSku(skuId) .setType(TYPE_SUBSCRIPTION) .build() val responseCode = billingClient.launchBillingFlow(activity, flowParams) if (responseCode == BillingClient.BillingResponse.OK) { it.onSuccess(PurchasePaperResponse.PurchaseSuccess(responseCode)) } else { it.onError(PurchaseException(responseCode, "ߪಡΤϥʔ")) } } } override fun observePurchasesUpdated(): Observable<PurchasesUpdatedResponse> { return billingClient.observePurchasesUpdated() } }
  53. 113.

    Remote 113 @Singleton class RemotePlayBillingImpl @Inject constructor( private val billingClient:

    PlayBillingManager ) : RemotePlayBilling { override fun launchBillingFlow(activity: Activity, skuId: String): Single<PurchasePaperResponse> { return Single.create { val flowParams = BillingFlowParams.newBuilder() .setSku(skuId) .setType(TYPE_SUBSCRIPTION) .build() val responseCode = billingClient.launchBillingFlow(activity, flowParams) if (responseCode == BillingClient.BillingResponse.OK) { it.onSuccess(PurchasePaperResponse.PurchaseSuccess(responseCode)) } else { it.onError(PurchaseException(responseCode, "ߪಡΤϥʔ")) } } } override fun observePurchasesUpdated(): Observable<PurchasesUpdatedResponse> { return billingClient.observePurchasesUpdated() } }
  54. 115.

    Repository 115 class PlayBillingRepositoryImpl @Inject constructor( private val remote: RemotePlayBilling

    ) : PlayBillingRepository { override fun startSubscription(activity: Activity, skuId: String): Observable<PurchaseResponse> { return remote.connect() .flatMapObservable { remote.launchBillingFlow(activity, skuId) .flatMapObservable { remote.observePurchasesUpdated() .flatMap { if (it.result != BillingClient.BillingResponse.OK) { return@flatMap Observable.error<PurchasesUpdatedResponse>(…) } return@flatMap Observable.just(it) } //… } } } }
  55. 116.

    Repository 116 class PlayBillingRepositoryImpl @Inject constructor( private val remote: RemotePlayBilling

    ) : PlayBillingRepository { override fun startSubscription(activity: Activity, skuId: String): Observable<PurchaseResponse> { return remote.connect() .flatMapObservable { remote.launchBillingFlow(activity, skuId) .flatMapObservable { remote.observePurchasesUpdated() .flatMap { if (it.result != BillingClient.BillingResponse.OK) { return@flatMap Observable.error<PurchasesUpdatedResponse>(…) } return@flatMap Observable.just(it) } //… } } } }
  56. 117.

    RemoteTest 117 @Before fun setUp() { manager = mock(PlayBillingManager::class.java) remote

    = RemotePlayBillingImpl(manager) } 
 @Test fun connect_isReady() { `when`(manager.isReady()).thenReturn(true) val response = remote.connect() .test() .await() .values()[0] assertThat(response).isNotNull() assertThat(response.result).isEqualTo(BillingClient.BillingResponse.OK) }
  57. 118.

    RemoteTest 118 @Test fun observePurchasesUpdated_error() { val subject = spy(PublishSubject.create<PurchasesUpdatedResponse>())

    `when`(remote.observePurchasesUpdated()).thenReturn(subject) val latch = CountDownLatch(2) var responseCode = 0 remote.observePurchasesUpdated() .subscribe({ responseCode += 1 latch.countDown() }, { latch.countDown() }) subject.onNext(PurchasesUpdatedResponse(0, null)) subject.onError(PurchaseException(BillingClient.BillingResponse.ERROR, "ΤϥʔͰ͢ʂʂ")) subject.onNext(PurchasesUpdatedResponse(1, null)) subject.onError(PurchaseException(BillingClient.BillingResponse.ERROR, "ΤϥʔͰ͢ʂʂ")) latch.await() assertThat(responseCode).isEqualTo(1) verify(subject, times(2)).onNext(any()) verify(subject, times(2)).onError(any()) }
  58. 119.
  59. 120.
  60. 122.

    122

  61. 123.
  62. 124.
  63. 125.
  64. 126.

    126

  65. 127.

    127

  66. 129.
  67. 133.