From 64be7461f73a4d1658a067b78ada83aad400df74 Mon Sep 17 00:00:00 2001 From: Alessandro Yuichi Okimoto Date: Mon, 20 Feb 2017 15:20:11 +0900 Subject: [PATCH 1/4] Refactoring the code for tests --- README.md | 27 +- extension-rxjava/README.md | 62 +- .../rxjava/BillingProcessorObservable.java | 120 +++- .../alessandro/android/iab/BaseProcessor.java | 340 ---------- .../android/iab/BillingContext.java | 12 +- .../android/iab/BillingProcessor.java | 609 +++++++++++++++--- .../android/iab/BillingService.java | 85 +++ .../jp/alessandro/android/iab/Constants.java | 73 ++- .../jp/alessandro/android/iab/ItemGetter.java | 40 +- .../alessandro/android/iab/ItemProcessor.java | 99 --- .../android/iab/PurchaseFlowLauncher.java | 83 +-- .../android/iab/PurchaseGetter.java | 112 ++-- .../jp/alessandro/android/iab/Security.java | 70 +- .../alessandro/android/iab/ServiceBinder.java | 71 +- .../android/iab/SubscriptionProcessor.java | 63 -- .../java/jp/alessandro/android/iab/Util.java | 182 ++++++ .../android/iab/handler/PurchasesHandler.java | 30 + .../android/iab/logger/DiscardLogger.java | 5 + .../alessandro/android/iab/logger/Logger.java | 2 + .../android/iab/logger/SystemLogger.java | 5 + 20 files changed, 1290 insertions(+), 800 deletions(-) delete mode 100644 library/src/main/java/jp/alessandro/android/iab/BaseProcessor.java create mode 100644 library/src/main/java/jp/alessandro/android/iab/BillingService.java delete mode 100644 library/src/main/java/jp/alessandro/android/iab/ItemProcessor.java delete mode 100644 library/src/main/java/jp/alessandro/android/iab/SubscriptionProcessor.java create mode 100644 library/src/main/java/jp/alessandro/android/iab/Util.java create mode 100644 library/src/main/java/jp/alessandro/android/iab/handler/PurchasesHandler.java diff --git a/README.md b/README.md index 13524bf..8fec309 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,12 @@ ![API](https://img.shields.io/badge/API-9%2B-brightgreen.svg?style=flat) [![Build Status](https://travis-ci.org/alessandrojp/easy-checkout.svg)](https://travis-ci.org/alessandrojp/easy-checkout) [![Bintray](https://img.shields.io/bintray/v/alessandrojp/android/easy-checkout.svg)](https://bintray.com/alessandrojp/android/easy-checkout/view) +[![codecov](https://codecov.io/gh/alessandrojp/easy-checkout/branch/master/graph/badge.svg)](https://codecov.io/gh/alessandrojp/easy-checkout) [![License](http://img.shields.io/:license-apache-brightgreen.svg)](http://www.apache.org/licenses/LICENSE-2.0.html) Fast and easy checkout library (Android In-App Billing) for Android apps with RxJava support. -This library supports both non-consumable/consumable items and upgrading/downgrading of subscriptions. +This library supports both non-consumable/consumable items and upgrading/downgrading of subscriptions. For **RxJava** please check [here](https://github.com/alessandrojp/easy-checkout/tree/master/extension-rxjava). @@ -20,11 +21,12 @@ The api version 5 automatically will be used only in this case. * For Eclipse users, download the latest jar version on [Bintray][] and add it as a dependency. -* For Gradle users, add this into your build.gradle file: +* For Gradle users, the library is available in the jcenter and mavenCentral. Add this into your build.gradle file: ```groovy repositories { jcenter() + mavenCentral() } dependencies { compile 'jp.alessandro.android:easy-checkout:vX.X.X' @@ -157,7 +159,7 @@ As a result you will get a [Purchase](#purchase-object) object. ```java String itemId = "YOUR_ITEM_ID"; -mBillingProcessor.consume(itemId, new ConsumeItemHandler() { +mBillingProcessor.consumePurchase(itemId, new ConsumeItemHandler() { @Override public void onSuccess() { // Item was consumed successfully @@ -207,7 +209,7 @@ As a result you will get a [Purchase](#purchase-object) object through of the Pu ```java PurchaseType purchaseType = PurchaseType.IN_APP; // PurchaseType.SUBSCRIPTIONS for subscriptions -mBillingProcessor.getInventory(purchaseType, new InventoryHandler() { +mBillingProcessor.getPurchases(purchaseType, new PurchasesHandler() { @Override public void onSuccess(Purchases purchases) { // Do your stuff with the list of purchases @@ -244,6 +246,23 @@ mBillingProcessor.getItemDetails(purchaseType, itemIds, new ItemDetailListHandle } }); ``` + +# Cancel +* Cancel the all purchase flows. It will clear the pending purchase flows and ignore any event until a new request.
If you don't need the BillingProcessor instance any more, call directly [Release](#release) instead. +
**Note: By canceling it will not cancel the purchase process since the purchase process is not controlled by the app.** + +```java +mBillingProcessor.cancel(); +``` + +# Release +* Release the handlers. Once you release it, you **MUST** to create a new instance. +
**Note: By releasing it will not cancel the purchase process since the purchase process is not controlled by the app.** + +```java +mBillingProcessor.release(); +``` + As a result you will get a list of [Item](#item-object) detail objects. # Check In-App Billing service availability diff --git a/extension-rxjava/README.md b/extension-rxjava/README.md index b5e9418..3c7f2fa 100644 --- a/extension-rxjava/README.md +++ b/extension-rxjava/README.md @@ -5,12 +5,16 @@ ### Installation -* Add this into your build.gradle file: +* For Eclipse users, download the latest jar version on [Bintray][] and add it as a dependency. + +* For Gradle users, the library is available in the jcenter and mavenCentral. Add this into your build.gradle file: ```groovy repositories { jcenter() + mavenCentral() } + dependencies { compile 'jp.alessandro.android:easy-checkout:vX.X.X' compile 'jp.alessandro.android:easy-checkout-extension-rxjava:vX.X.X' @@ -147,7 +151,7 @@ As a result you will get a [Purchase](#purchase-object) object through of the Pu ```java String itemId = "YOUR_ITEM_ID"; -mBillingProcessor.consume(itemId) +mBillingProcessor.consumePurchase(itemId) .observeOn(AndroidSchedulers.mainThread()) .subscribeOn(Schedulers.io()) .subscribe(new Action1() { @@ -201,7 +205,7 @@ As a result you will get a [Purchase](#purchase-object) object through of the Pu ```java PurchaseType purchaseType = PurchaseType.IN_APP; // PurchaseType.SUBSCRIPTIONS for subscriptions -mBillingProcessor.getInventory(purchaseType) +mBillingProcessor.getPurchases(purchaseType) .observeOn(AndroidSchedulers.mainThread()) .subscribeOn(Schedulers.io()) .subscribe(new Action1() { @@ -246,6 +250,22 @@ mBillingProcessor.getItemDetails(purchaseType, itemIds) ``` As a result you will get a list of [Item](#item-object) detail objects. +# Cancel +* Cancel the all purchase flows. It will clear the pending purchase flows and ignore any event until a new request.
If you don't need the BillingProcessor instance any more, call directly [Release](#release) instead. +
**Note: By canceling it will not cancel the purchase process since the purchase process is not controlled by the app.** + +```java +mBillingProcessor.cancel(); +``` + +# Release +* Release the handlers. Once you release it, you **MUST** to create a new instance. +
**Note: By releasing it will not cancel the purchase process since the purchase process is not controlled by the app.** + +```java +mBillingProcessor.release(); +``` + # Check In-App Billing service availability * In some devices, In-App Billing may not be available. Therefore, it is advisable to check whether it is available or not by calling `BillingProcessorObservable.isServiceAvailable` as follows: @@ -261,28 +281,28 @@ if (!isAvailable) { * This object contains all the information of a purchase ```java -public String getOriginalJson(); -public String getOrderId(); -public String getPackageName(); -public String getSku(); -public long getPurchaseTime(); -public int getPurchaseState(); -public String getDeveloperPayload(); -public String getToken(); -public boolean isAutoRenewing(); -public String getSignature(); + public String getOriginalJson(); + public String getOrderId(); + public String getPackageName(); + public String getSku(); + public long getPurchaseTime(); + public int getPurchaseState(); + public String getDeveloperPayload(); + public String getToken(); + public boolean isAutoRenewing(); + public String getSignature(); ``` # Item Object * This object contains all the information of an item ```java -public String getOriginalJson(); -public String getSku(); -public String getType(); -public String getTitle(); -public String getDescription(); -public String getCurrency(); -public String getPrice(); -public long getPriceMicros(); + public String getOriginalJson(); + public String getSku(); + public String getType(); + public String getTitle(); + public String getDescription(); + public String getCurrency(); + public String getPrice(); + public long getPriceMicros(); ``` \ No newline at end of file diff --git a/extension-rxjava/src/main/java/jp/alessandro/android/iab/rxjava/BillingProcessorObservable.java b/extension-rxjava/src/main/java/jp/alessandro/android/iab/rxjava/BillingProcessorObservable.java index f2088b2..8f6fbb7 100644 --- a/extension-rxjava/src/main/java/jp/alessandro/android/iab/rxjava/BillingProcessorObservable.java +++ b/extension-rxjava/src/main/java/jp/alessandro/android/iab/rxjava/BillingProcessorObservable.java @@ -35,7 +35,10 @@ import jp.alessandro.android.iab.handler.InventoryHandler; import jp.alessandro.android.iab.handler.ItemDetailsHandler; import jp.alessandro.android.iab.handler.PurchaseHandler; +import jp.alessandro.android.iab.handler.PurchasesHandler; import jp.alessandro.android.iab.handler.StartActivityHandler; +import rx.Completable; +import rx.CompletableEmitter; import rx.Emitter; import rx.Observable; import rx.functions.Action1; @@ -72,15 +75,15 @@ public static boolean isServiceAvailable(Context context) { * @param purchaseType IN_APP or SUBSCRIPTION * @param developerPayload optional argument to be sent back with the purchase information. It helps to identify the user */ - public Observable startPurchase(final Activity activity, - final int requestCode, - final String itemId, - final PurchaseType purchaseType, - final String developerPayload) { + public Completable startPurchase(final Activity activity, + final int requestCode, + final String itemId, + final PurchaseType purchaseType, + final String developerPayload) { - return Observable.fromEmitter(new Action1>() { + return Completable.fromEmitter(new Action1() { @Override - public void call(final Emitter emitter) { + public void call(final CompletableEmitter emitter) { mBillingProcessor.startPurchase(activity, requestCode, itemId, @@ -89,7 +92,6 @@ public void call(final Emitter emitter) { new StartActivityHandler() { @Override public void onSuccess() { - emitter.onNext(null); emitter.onCompleted(); } @@ -99,7 +101,7 @@ public void onError(BillingException e) { } }); } - }, Emitter.BackpressureMode.LATEST); + }); } /** @@ -116,20 +118,19 @@ public void onError(BillingException e) { * @param itemId new subscription item id * @param developerPayload optional argument to be sent back with the purchase information. It helps to identify the user */ - public Observable updateSubscription(final Activity activity, - final int requestCode, - final List oldItemIds, - final String itemId, - final String developerPayload) { + public Completable updateSubscription(final Activity activity, + final int requestCode, + final List oldItemIds, + final String itemId, + final String developerPayload) { - return Observable.fromEmitter(new Action1>() { + return Completable.fromEmitter(new Action1() { @Override - public void call(final Emitter emitter) { + public void call(final CompletableEmitter emitter) { mBillingProcessor.updateSubscription(activity, requestCode, oldItemIds, itemId, developerPayload, new StartActivityHandler() { @Override public void onSuccess() { - emitter.onNext(null); emitter.onCompleted(); } @@ -139,7 +140,37 @@ public void onError(BillingException e) { } }); } - }, Emitter.BackpressureMode.LATEST); + }); + } + + /** + * Method deprecated, please use @{link {@link BillingProcessorObservable#consumePurchase(String)}} + *

+ * Consumes previously purchased item to be purchased again + * This will be executed from Work Thread + * See http://developer.android.com/google/play/billing/billing_integrate.html#Consume + * + * @param itemId consumable item id + */ + @Deprecated + public Completable consume(final String itemId) { + return Completable.fromEmitter(new Action1() { + + @Override + public void call(final CompletableEmitter emitter) { + mBillingProcessor.consume(itemId, new ConsumeItemHandler() { + @Override + public void onSuccess() { + emitter.onCompleted(); + } + + @Override + public void onError(BillingException e) { + emitter.onError(e); + } + }); + } + }); } /** @@ -149,14 +180,42 @@ public void onError(BillingException e) { * * @param itemId consumable item id */ - public Observable consume(final String itemId) { - return Observable.fromEmitter(new Action1>() { + public Completable consumePurchase(final String itemId) { + return Completable.fromEmitter(new Action1() { + @Override - public void call(final Emitter emitter) { + public void call(final CompletableEmitter emitter) { mBillingProcessor.consume(itemId, new ConsumeItemHandler() { @Override public void onSuccess() { - emitter.onNext(null); + emitter.onCompleted(); + } + + @Override + public void onError(BillingException e) { + emitter.onError(e); + } + }); + } + }); + } + + /** + * Get the information about inventory of purchases made by a user from your app + * This method will get all the purchases even if there are more than 500 + * This will be executed from Work Thread + * See http://developer.android.com/google/play/billing/billing_integrate.html#QueryPurchases + * + * @param purchaseType IN_APP or SUBSCRIPTION + */ + public Observable getPurchases(final PurchaseType purchaseType) { + return Observable.fromEmitter(new Action1>() { + @Override + public void call(final Emitter emitter) { + mBillingProcessor.getPurchases(purchaseType, new PurchasesHandler() { + @Override + public void onSuccess(Purchases purchases) { + emitter.onNext(purchases); emitter.onCompleted(); } @@ -170,6 +229,8 @@ public void onError(BillingException e) { } /** + * Method deprecated, please use @{link {@link BillingProcessorObservable#getPurchases(PurchaseType)}} + *

* Get the information about inventory of purchases made by a user from your app * This method will get all the purchases even if there are more than 500 * This will be executed from Work Thread @@ -177,6 +238,7 @@ public void onError(BillingException e) { * * @param purchaseType IN_APP or SUBSCRIPTION */ + @Deprecated public Observable getInventory(final PurchaseType purchaseType) { return Observable.fromEmitter(new Action1>() { @Override @@ -239,6 +301,20 @@ public boolean onActivityResult(int requestCode, int resultCode, Intent data) { return mBillingProcessor.onActivityResult(requestCode, resultCode, data); } + /** + * Cancel the all purchase flows + * It will clear the pending purchase flows and ignore any event until a new request + *

+ * If you don't need the BillingProcessor any more, + * call directly @{@link BillingProcessorObservable#release()} instead + *

+ * By canceling it will not cancel the purchase process + * since the purchase process is not controlled by the app. + */ + public void cancel() { + mBillingProcessor.cancel(); + } + /** * Release the handlers * By releasing it will not cancel the purchase process diff --git a/library/src/main/java/jp/alessandro/android/iab/BaseProcessor.java b/library/src/main/java/jp/alessandro/android/iab/BaseProcessor.java deleted file mode 100644 index 2d670c6..0000000 --- a/library/src/main/java/jp/alessandro/android/iab/BaseProcessor.java +++ /dev/null @@ -1,340 +0,0 @@ -/* - * Copyright (C) 2016 Alessandro Yuichi Okimoto - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - * Contact email: alessandro@alessandro.jp - */ - -package jp.alessandro.android.iab; - -import android.app.Activity; -import android.content.Intent; -import android.os.Handler; -import android.os.RemoteException; -import android.util.SparseArray; - -import com.android.vending.billing.IInAppBillingService; - -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; - -import jp.alessandro.android.iab.handler.ErrorHandler; -import jp.alessandro.android.iab.handler.InventoryHandler; -import jp.alessandro.android.iab.handler.ItemDetailsHandler; -import jp.alessandro.android.iab.handler.PurchaseHandler; -import jp.alessandro.android.iab.handler.StartActivityHandler; -import jp.alessandro.android.iab.logger.Logger; -import jp.alessandro.android.iab.response.PurchaseResponse; - -abstract class BaseProcessor { - - protected final BillingContext mContext; - private final String mItemType; - private final SparseArray mPurchaseFlows; - private final Logger mLogger; - private final Intent mServiceIntent; - private final Handler mWorkHandler; - private final Handler mMainHandler; - - private PurchaseHandler mPurchaseHandler; - - BaseProcessor(BillingContext context, String itemType, - PurchaseHandler purchaseHandler, Handler workHandler, Handler mainHandler) { - - mContext = context; - mItemType = itemType; - mPurchaseHandler = purchaseHandler; - mWorkHandler = workHandler; - mMainHandler = mainHandler; - mPurchaseFlows = new SparseArray<>(); - mLogger = context.getLogger(); - - mServiceIntent = new Intent(Constants.ACTION_BILLING_SERVICE_BIND); - mServiceIntent.setPackage(Constants.VENDING_PACKAGE); - } - - /** - * Purchase a subscription - * This will be executed from UI Thread - * - * @param activity activity calling this method - * @param requestCode - * @param oldItemIds a list of item ids to be updated - * @param itemId new subscription item id - * @param developerPayload optional argument to be sent back with the purchase information. It helps to identify the user - * @param handler callback called asynchronously - */ - public void startPurchase(final Activity activity, final int requestCode, - final List oldItemIds, final String itemId, - final String developerPayload, final StartActivityHandler handler) { - - executeInServiceOnMainThread(new ServiceBinder.Handler() { - @Override - public void onBind(IInAppBillingService service) { - try { - // Before launch the IAB activity, we check if subscriptions are supported. - checkIfBillingIsSupported(service); - PurchaseFlowLauncher launcher = createPurchaseFlowLauncher(requestCode); - mPurchaseFlows.append(requestCode, launcher); - launcher.launch(service, activity, requestCode, oldItemIds, itemId, developerPayload); - - postActivityStartedSuccess(handler); - } catch (BillingException e) { - postOnError(e, handler); - } - } - - @Override - public void onError() { - postBindServiceError(handler); - } - }); - } - - /** - * Get item details (SKU) - * See http://developer.android.com/google/play/billing/billing_integrate.html#QueryDetails - * - * @param itemIds list of SKU ids to be loaded - * @param handler callback called asynchronously - */ - public void getItemDetails(final ArrayList itemIds, final ItemDetailsHandler handler) { - executeInServiceOnWorkThread(new ServiceBinder.Handler() { - @Override - public void onBind(IInAppBillingService service) { - try { - ItemGetter getter = new ItemGetter(mContext); - ItemDetails details = getter.get(service, mItemType, itemIds); - postListSuccess(details, handler); - } catch (BillingException e) { - postOnError(e, handler); - } - } - - @Override - public void onError() { - postBindServiceError(handler); - } - }); - } - - /** - * Get the information about inventory of purchases made by a user from your app - * This method will get all the purchases even if there are more than 500 - * See http://developer.android.com/google/play/billing/billing_integrate.html#QueryPurchases - * - * @param handler callback called asynchronously - */ - public void getInventory(final InventoryHandler handler) { - executeInServiceOnWorkThread(new ServiceBinder.Handler() { - @Override - public void onBind(IInAppBillingService service) { - try { - PurchaseGetter getter = new PurchaseGetter(mContext); - Purchases purchases = getter.get(service, mItemType); - postInventorySuccess(purchases, handler); - } catch (BillingException e) { - postOnError(e, handler); - } - } - - @Override - public void onError() { - postBindServiceError(handler); - } - }); - } - - /** - * Checks the purchase response from Google - * The result will be sent through PurchaseHandler - * This method MUST be called from UI Thread - * - * @param requestCode - * @param resultCode - * @param data - * @return - */ - public boolean onActivityResult(int requestCode, int resultCode, Intent data) { - PurchaseFlowLauncher launcher = mPurchaseFlows.get(requestCode); - if (launcher == null) { - return false; - } - try { - Purchase purchase = launcher.handleResult(requestCode, resultCode, data); - postPurchaseSuccess(purchase); - } catch (BillingException e) { - postPurchaseError(e); - } finally { - mPurchaseFlows.delete(requestCode); - } - return true; - } - - /** - * Release the handlers - * By releasing it will not cancel the purchase process - * since the purchase process is not controlled by the app. - * Once you release it, you MUST to create a new instance - */ - public void release() { - mPurchaseHandler = null; - mPurchaseFlows.clear(); - } - - protected void executeInServiceOnWorkThread(final ServiceBinder.Handler serviceHandler) { - executeInService(serviceHandler, mWorkHandler); - } - - protected void executeInServiceOnMainThread(final ServiceBinder.Handler serviceHandler) { - executeInService(serviceHandler, mMainHandler); - } - - protected void postEventHandler(Runnable r) { - if (mMainHandler != null) { - mMainHandler.post(r); - } - } - - protected void postOnError(final BillingException e, final ErrorHandler handler) { - postEventHandler(new Runnable() { - @Override - public void run() { - if (handler != null) { - handler.onError(e); - } - } - }); - } - - protected void postBindServiceError(ErrorHandler handler) { - postOnError(new BillingException( - Constants.ERROR_BIND_SERVICE_FAILED_EXCEPTION, - Constants.ERROR_MSG_BIND_SERVICE_FAILED), handler); - } - - private PurchaseFlowLauncher createPurchaseFlowLauncher(int requestCode) throws BillingException { - PurchaseFlowLauncher launcher = mPurchaseFlows.get(requestCode); - if (launcher != null) { - String message = String.format(Locale.US, Constants.ERROR_MSG_PURCHASE_FLOW_ALREADY_EXISTS, requestCode); - throw new BillingException(Constants.ERROR_PURCHASE_FLOW_ALREADY_EXISTS, message); - } - return new PurchaseFlowLauncher(mContext, mItemType); - } - - private void executeInService(final ServiceBinder.Handler serviceHandler, Handler handler) { - handler.post(new Runnable() { - @Override - public void run() { - final ServiceBinder conn = new ServiceBinder(mContext.getContext(), mServiceIntent); - - conn.getServiceAsync(new ServiceBinder.Handler() { - @Override - public void onBind(IInAppBillingService service) { - try { - serviceHandler.onBind(service); - } finally { - conn.unbindService(); - } - } - - @Override - public void onError() { - serviceHandler.onError(); - } - }); - } - }); - } - - private void postPurchaseSuccess(final Purchase purchase) { - postEventHandler(new Runnable() { - @Override - public void run() { - if (mPurchaseHandler != null) { - mPurchaseHandler.call(new PurchaseResponse(purchase, null)); - } - } - }); - } - - private void postPurchaseError(final BillingException e) { - postEventHandler(new Runnable() { - @Override - public void run() { - if (mPurchaseHandler != null) { - mPurchaseHandler.call(new PurchaseResponse(null, e)); - } - } - }); - } - - private void postListSuccess(final ItemDetails itemDetails, final ItemDetailsHandler handler) { - postEventHandler(new Runnable() { - @Override - public void run() { - handler.onSuccess(itemDetails); - } - }); - } - - private void postInventorySuccess(final Purchases purchases, final InventoryHandler handler) { - postEventHandler(new Runnable() { - @Override - public void run() { - handler.onSuccess(purchases); - } - }); - } - - private void postActivityStartedSuccess(final StartActivityHandler handler) { - postEventHandler(new Runnable() { - @Override - public void run() { - handler.onSuccess(); - } - }); - } - - private void checkIfBillingIsSupported(IInAppBillingService service) throws BillingException { - if (isSupported(service)) { - return; - } - if (mItemType.equals(Constants.ITEM_TYPE_INAPP)) { - throw new BillingException(Constants.ERROR_PURCHASES_NOT_SUPPORTED, - Constants.ERROR_MSG_PURCHASES_NOT_SUPPORTED); - } - throw new BillingException(Constants.ERROR_SUBSCRIPTIONS_NOT_SUPPORTED, - Constants.ERROR_MSG_SUBSCRIPTIONS_NOT_SUPPORTED); - } - - private boolean isSupported(IInAppBillingService service) { - try { - int response = service.isBillingSupported(mContext.getApiVersion(), - mContext.getContext().getPackageName(), mItemType); - - if (response == Constants.BILLING_RESPONSE_RESULT_OK) { - mLogger.d(Logger.TAG, "Subscription is AVAILABLE."); - return true; - } - mLogger.w(Logger.TAG, - String.format(Locale.US, "Subscription is NOT AVAILABLE. Response: %d", response)); - } catch (RemoteException e) { - mLogger.e(Logger.TAG, - "RemoteException while checking if the subscription is available."); - } - return false; - } -} \ No newline at end of file diff --git a/library/src/main/java/jp/alessandro/android/iab/BillingContext.java b/library/src/main/java/jp/alessandro/android/iab/BillingContext.java index f27ce40..0d0c6d8 100644 --- a/library/src/main/java/jp/alessandro/android/iab/BillingContext.java +++ b/library/src/main/java/jp/alessandro/android/iab/BillingContext.java @@ -26,7 +26,7 @@ public class BillingContext { private final Context mContext; - private final String mSignatureBase64; + private final String mPublicKeyBase64; private final BillingApi mApiVersion; private final Logger mLogger; @@ -34,16 +34,16 @@ public class BillingContext { * Context that contains all information to execute the library * * @param context application context - * @param signatureBase64 rsa public key generated by Google Play + * @param publicKeyBase64 rsa public key generated by Google Play Developer Console * @param apiVersion google api version (The library supports version 3 & 5) * @param logger interface to print the library's log */ public BillingContext(Context context, - String signatureBase64, + String publicKeyBase64, BillingApi apiVersion, Logger logger) { mContext = context; - mSignatureBase64 = signatureBase64; + mPublicKeyBase64 = publicKeyBase64; mApiVersion = apiVersion; mLogger = logger == null ? new DiscardLogger() : logger; } @@ -52,8 +52,8 @@ Context getContext() { return mContext; } - String getSignatureBase64() { - return mSignatureBase64; + String getPublicKeyBase64() { + return mPublicKeyBase64; } int getApiVersion() { diff --git a/library/src/main/java/jp/alessandro/android/iab/BillingProcessor.java b/library/src/main/java/jp/alessandro/android/iab/BillingProcessor.java index 8aa2cf4..f4cb8f4 100644 --- a/library/src/main/java/jp/alessandro/android/iab/BillingProcessor.java +++ b/library/src/main/java/jp/alessandro/android/iab/BillingProcessor.java @@ -1,19 +1,19 @@ /* - * Copyright (C) 2016 Alessandro Yuichi Okimoto + * Copyright (C) 2016 Alessandro Yuichi Okimoto * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - * Contact email: alessandro@alessandro.jp + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * Contact email: alessandro@alessandro.jp */ package jp.alessandro.android.iab; @@ -23,64 +23,75 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; +import android.os.RemoteException; +import android.text.TextUtils; +import android.util.SparseArray; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import jp.alessandro.android.iab.handler.ConsumeItemHandler; +import jp.alessandro.android.iab.handler.ErrorHandler; import jp.alessandro.android.iab.handler.InventoryHandler; import jp.alessandro.android.iab.handler.ItemDetailsHandler; import jp.alessandro.android.iab.handler.PurchaseHandler; +import jp.alessandro.android.iab.handler.PurchasesHandler; import jp.alessandro.android.iab.handler.StartActivityHandler; - -/** - * Created by Alessandro Yuichi Okimoto on 2016/11/22. - */ +import jp.alessandro.android.iab.logger.Logger; +import jp.alessandro.android.iab.response.PurchaseResponse; public class BillingProcessor { - private SubscriptionProcessor mSubscriptionProcessor; - private ItemProcessor mItemProcessor; + protected static final String WORK_THREAD_NAME = "AndroidEasyCheckoutThread"; + + private final BillingContext mContext; + private final SparseArray mPurchaseFlows; + private final Logger mLogger; + private final Intent mServiceIntent; + + private PurchaseHandler mPurchaseHandler; private Handler mWorkHandler; private Handler mMainHandler; + private boolean mIsReleased; public BillingProcessor(BillingContext context, PurchaseHandler purchaseHandler) { - HandlerThread thread = new HandlerThread("AndroidIabThread"); - thread.start(); - // Handler to post all actions in the library - mWorkHandler = new Handler(thread.getLooper()); - // Handler to post all events in the library - mMainHandler = new Handler(Looper.getMainLooper()); - mSubscriptionProcessor = new SubscriptionProcessor(context, purchaseHandler, mWorkHandler, mMainHandler); - mItemProcessor = new ItemProcessor(context, purchaseHandler, mWorkHandler, mMainHandler); + mContext = context; + mPurchaseHandler = purchaseHandler; + mPurchaseFlows = new SparseArray<>(); + mLogger = context.getLogger(); + + mServiceIntent = new Intent(Constants.ACTION_BILLING_SERVICE_BIND); + mServiceIntent.setPackage(Constants.VENDING_PACKAGE); } /** - * Checks if the in-app billing service is available + * Check if nAppBillingService is supported on the device. * - * @param context application context - * @return true if it is available + * @param context + * @return true if it is supported */ - public static boolean isServiceAvailable(Context context) { + public synchronized static boolean isServiceAvailable(Context context) { PackageManager packageManager = context.getPackageManager(); Intent serviceIntent = new Intent(Constants.ACTION_BILLING_SERVICE_BIND); serviceIntent.setPackage(Constants.VENDING_PACKAGE); List list = packageManager.queryIntentServices(serviceIntent, 0); - return list != null && list.size() > 0; + return list != null && !list.isEmpty(); } /** - * Starts to purchase a consumable/non-consumable item or a subscription + * Purchase a subscription * This will be executed from UI Thread * * @param activity activity calling this method - * @param requestCode request code for the billing activity - * @param itemId product item id + * @param requestCode + * @param itemId new subscription item id * @param purchaseType IN_APP or SUBSCRIPTION * @param developerPayload optional argument to be sent back with the purchase information. It helps to identify the user * @param handler callback called asynchronously @@ -92,33 +103,65 @@ public void startPurchase(Activity activity, String developerPayload, StartActivityHandler handler) { synchronized (this) { - checkIfIsNotReleased(); - if (purchaseType == PurchaseType.SUBSCRIPTION) { - mSubscriptionProcessor.startPurchase(activity, requestCode, null, itemId, developerPayload, handler); - } else { - mItemProcessor.startPurchase(activity, requestCode, null, itemId, developerPayload, handler); - } + startPurchase(activity, requestCode, null, itemId, purchaseType, developerPayload, handler); } } /** + * Method deprecated, please use consumePurchase above instead * Consumes previously purchased item to be purchased again - * This will be executed from Work Thread * See http://developer.android.com/google/play/billing/billing_integrate.html#Consume * * @param itemId consumable item id * @param handler callback called asynchronously */ - public void consume(String itemId, ConsumeItemHandler handler) { + @Deprecated + public void consume(final String itemId, final ConsumeItemHandler handler) { + consumePurchase(itemId, handler); + } + + /** + * Consumes previously purchased item to be purchased again + * See http://developer.android.com/google/play/billing/billing_integrate.html#Consume + * + * @param itemId consumable item id + * @param handler callback called asynchronously + */ + public void consumePurchase(final String itemId, final ConsumeItemHandler handler) { synchronized (this) { checkIfIsNotReleased(); - mItemProcessor.consume(itemId, handler); + executeInServiceOnWorkThread(new ServiceBinder.Handler() { + @Override + public void onBind(BillingService service) { + try { + checkIfBillingIsSupported(PurchaseType.IN_APP, service); + + int response = service.consumePurchase(mContext.getApiVersion(), + mContext.getContext().getPackageName(), getToken(service, itemId)); + + if (response != Constants.BILLING_RESPONSE_RESULT_OK) { + throw new BillingException(response, Constants.ERROR_MSG_CONSUME); + } + + postConsumePurchaseSuccess(handler); + } catch (BillingException e) { + postOnError(e, handler); + } catch (RemoteException e) { + postOnError(new BillingException(Constants.ERROR_REMOTE_EXCEPTION, e.getMessage()), handler); + } + } + + @Override + public void onError(BillingException e) { + postBindServiceError(e, handler); + } + }); } } /** * Updates a subscription (Upgrade / Downgrade) - * This will be executed from UI Thread + * This method MUST be called from UI Thread * This can only be done on API version 5 * Even if you set up to use the API version 3 * It will automatically use API version 5 @@ -137,49 +180,134 @@ public void updateSubscription(Activity activity, String itemId, String developerPayload, StartActivityHandler handler) { + synchronized (this) { + if (oldItemIds == null || oldItemIds.isEmpty()) { + throw new IllegalArgumentException(Constants.ERROR_MSG_UPDATE_ARGUMENT_MISSING); + } + startPurchase(activity, requestCode, oldItemIds, itemId, PurchaseType.SUBSCRIPTION, developerPayload, handler); + } + } + + /** + * Get item details (SKU) + * See http://developer.android.com/google/play/billing/billing_integrate.html#QueryDetails + * + * @param purchaseType IN_APP or SUBSCRIPTION + * @param handler callback called asynchronously + */ + public void getItemDetails(final PurchaseType purchaseType, + final ArrayList itemIds, + final ItemDetailsHandler handler) { synchronized (this) { checkIfIsNotReleased(); - mSubscriptionProcessor.update(activity, requestCode, oldItemIds, itemId, developerPayload, handler); + executeInServiceOnWorkThread(new ServiceBinder.Handler() { + @Override + public void onBind(BillingService service) { + String type; + if (purchaseType == PurchaseType.SUBSCRIPTION) { + type = Constants.TYPE_SUBSCRIPTION; + } else { + type = Constants.TYPE_IN_APP; + } + try { + checkIfBillingIsSupported(purchaseType, service); + + ItemGetter getter = new ItemGetter(mContext); + ItemDetails details = getter.get(service, type, createBundleItemListFromArray(itemIds)); + + postListSuccess(details, handler); + } catch (BillingException e) { + postOnError(e, handler); + } + } + + @Override + public void onError(BillingException e) { + postBindServiceError(e, handler); + } + }); } } /** * Get the information about inventory of purchases made by a user from your app * This method will get all the purchases even if there are more than 500 - * This will be executed from Work Thread * See http://developer.android.com/google/play/billing/billing_integrate.html#QueryPurchases * * @param purchaseType IN_APP or SUBSCRIPTION * @param handler callback called asynchronously */ - public void getInventory(PurchaseType purchaseType, InventoryHandler handler) { + public void getPurchases(final PurchaseType purchaseType, final PurchasesHandler handler) { synchronized (this) { checkIfIsNotReleased(); - if (purchaseType == PurchaseType.SUBSCRIPTION) { - mSubscriptionProcessor.getInventory(handler); - } else { - mItemProcessor.getInventory(handler); - } + executeInServiceOnWorkThread(new ServiceBinder.Handler() { + @Override + public void onBind(BillingService service) { + String type; + if (purchaseType == PurchaseType.SUBSCRIPTION) { + type = Constants.TYPE_SUBSCRIPTION; + } else { + type = Constants.TYPE_IN_APP; + } + try { + checkIfBillingIsSupported(purchaseType, service); + + PurchaseGetter getter = new PurchaseGetter(mContext); + Purchases purchases = getter.get(service, type); + + postPurchasesSuccess(purchases, handler); + } catch (BillingException e) { + postOnError(e, handler); + } + } + + @Override + public void onError(BillingException e) { + postBindServiceError(e, handler); + } + }); } } /** - * Get item details (SKU) - * This will be executed from Work Thread - * See http://developer.android.com/google/play/billing/billing_integrate.html#QueryDetails + * Method deprecated, please use getPurchases above instead + *

+ * Get the information about inventory of purchases made by a user from your app + * This method will get all the purchases even if there are more than 500 + * See http://developer.android.com/google/play/billing/billing_integrate.html#QueryPurchases * - * @param purchaseType IN_APP or SUBSCRIPTION - * @param itemIds list of SKU ids to be loaded - * @param handler callback called asynchronously + * @param handler callback called asynchronously */ - public void getItemDetails(PurchaseType purchaseType, ArrayList itemIds, ItemDetailsHandler handler) { + @Deprecated + public void getInventory(final PurchaseType purchaseType, final InventoryHandler handler) { synchronized (this) { checkIfIsNotReleased(); - if (purchaseType == PurchaseType.SUBSCRIPTION) { - mSubscriptionProcessor.getItemDetails(itemIds, handler); - } else { - mItemProcessor.getItemDetails(itemIds, handler); - } + executeInServiceOnWorkThread(new ServiceBinder.Handler() { + @Override + public void onBind(BillingService service) { + String type; + if (purchaseType == PurchaseType.SUBSCRIPTION) { + type = Constants.TYPE_SUBSCRIPTION; + } else { + type = Constants.TYPE_IN_APP; + } + try { + checkIfBillingIsSupported(purchaseType, service); + + PurchaseGetter getter = new PurchaseGetter(mContext); + Purchases purchases = getter.get(service, type); + + postInventorySuccess(purchases, handler); + } catch (BillingException e) { + postOnError(e, handler); + } + } + + @Override + public void onError(BillingException e) { + postBindServiceError(e, handler); + } + }); } } @@ -191,17 +319,51 @@ public void getItemDetails(PurchaseType purchaseType, ArrayList itemIds, * @param requestCode * @param resultCode * @param data - * @return true if the result was processed in the library + * @return */ public boolean onActivityResult(int requestCode, int resultCode, Intent data) { synchronized (this) { - checkIfIsNotReleased(); - checkIsMainThread(); - if (mSubscriptionProcessor.onActivityResult(requestCode, resultCode, data) - || mItemProcessor.onActivityResult(requestCode, resultCode, data)) { - return true; + PurchaseFlowLauncher launcher = mPurchaseFlows.get(requestCode); + if (launcher == null) { + return false; + } + try { + checkIsMainThread(); + Purchase purchase = launcher.handleResult(requestCode, resultCode, data); + + postPurchaseSuccess(purchase); + } catch (BillingException e) { + postPurchaseError(e); + } finally { + mPurchaseFlows.delete(requestCode); + } + return true; + } + } + + /** + * Cancel the all purchase flows + * It will clear the pending purchase flows and ignore any event until a new request + *

+ * If you don't need the BillingProcessor any more, + * call directly {@link BillingProcessor#release()} instead + *

+ * By canceling it will not cancel the purchase process + * since the purchase process is not controlled by the app. + */ + public void cancel() { + synchronized (this) { + if (mIsReleased) { + return; + } + mPurchaseFlows.clear(); + + if (mMainHandler != null) { + mMainHandler.removeCallbacksAndMessages(null); + } + if (mWorkHandler != null) { + mWorkHandler.removeCallbacksAndMessages(null); } - return false; } } @@ -212,29 +374,306 @@ public boolean onActivityResult(int requestCode, int resultCode, Intent data) { * Once you release it, you MUST to create a new instance */ public void release() { - synchronized (this) { - SubscriptionProcessor subscriptionProcessor = mSubscriptionProcessor; - ItemProcessor itemProcessor = mItemProcessor; - Handler mainThread = mMainHandler; - Handler workHandler = mWorkHandler; + mIsReleased = true; + mPurchaseHandler = null; + mPurchaseFlows.clear(); - mSubscriptionProcessor = null; - mItemProcessor = null; - mMainHandler = null; - mWorkHandler = null; + Handler mainThread = mMainHandler; + Handler workHandler = mWorkHandler; + mMainHandler = null; + mWorkHandler = null; + + if (mainThread != null) { mainThread.removeCallbacksAndMessages(null); + } + if (workHandler != null) { workHandler.removeCallbacksAndMessages(null); workHandler.getLooper().quit(); + } + } - subscriptionProcessor.release(); - itemProcessor.release(); + /** + * Handler to post all events in the library + */ + protected Handler getMainHandler() { + if (mMainHandler == null) { + return mMainHandler = new Handler(Looper.getMainLooper()); } + return mMainHandler; + } + + /** + * Handler to post all actions in the library + */ + protected Handler getWorkHandler() { + if (mWorkHandler == null) { + HandlerThread thread = new HandlerThread(WORK_THREAD_NAME); + thread.start(); + return mWorkHandler = new Handler(thread.getLooper()); + } + return mWorkHandler; + } + + protected void checkIfBillingIsSupported(PurchaseType purchaseType, BillingService service) throws BillingException { + if (isSupported(purchaseType, service)) { + return; + } + if (purchaseType == PurchaseType.SUBSCRIPTION) { + throw new BillingException(Constants.ERROR_SUBSCRIPTIONS_NOT_SUPPORTED, + Constants.ERROR_MSG_SUBSCRIPTIONS_NOT_SUPPORTED); + } + throw new BillingException(Constants.ERROR_PURCHASES_NOT_SUPPORTED, + Constants.ERROR_MSG_PURCHASES_NOT_SUPPORTED); + } + + /** + * Check if the device supports InAppBilling + * + * @param service + * @return true if it is supported + */ + protected boolean isSupported(PurchaseType purchaseType, BillingService service) { + String type; + + if (purchaseType == PurchaseType.SUBSCRIPTION) { + type = Constants.TYPE_SUBSCRIPTION; + } else { + type = Constants.TYPE_IN_APP; + } + + try { + int response = service.isBillingSupported( + mContext.getApiVersion(), + mContext.getContext().getPackageName(), + type); + + if (response == Constants.BILLING_RESPONSE_RESULT_OK) { + mLogger.d(Logger.TAG, "Subscription is AVAILABLE."); + return true; + } + mLogger.w(Logger.TAG, + String.format(Locale.US, "Subscription is NOT AVAILABLE. Response: %d", response)); + } catch (RemoteException e) { + mLogger.e(Logger.TAG, e.getMessage(), e); + } + return false; + } + + /** + * Get the purchase token to be used in {@link BillingProcessor#consumePurchase(String, ConsumeItemHandler)} + */ + protected String getToken(BillingService service, String itemId) throws BillingException { + PurchaseGetter getter = createPurchaseGetter(); + Purchases purchases = getter.get(service, Constants.ITEM_TYPE_INAPP); + Purchase purchase = purchases.getByPurchaseId(itemId); + + if (purchase == null || TextUtils.isEmpty(purchase.getToken())) { + throw new BillingException(Constants.ERROR_PURCHASE_DATA, + Constants.ERROR_MSG_PURCHASE_OR_TOKEN_NULL); + } + return purchase.getToken(); + } + + protected PurchaseGetter createPurchaseGetter() { + return new PurchaseGetter(mContext); + } + + protected Bundle createBundleItemListFromArray(ArrayList itemIds) { + Bundle bundle = new Bundle(); + bundle.putStringArrayList(Constants.RESPONSE_ITEM_ID_LIST, itemIds); + return bundle; + } + + protected ServiceBinder createServiceBinder() { + return new ServiceBinder(mContext, mServiceIntent); + } + + private void startPurchase(final Activity activity, + final int requestCode, + final List oldItemIds, + final String itemId, + final PurchaseType purchaseType, + final String developerPayload, + final StartActivityHandler handler) { + + checkIfIsNotReleased(); + executeInServiceOnMainThread(new ServiceBinder.Handler() { + @Override + public void onBind(BillingService service) { + try { + // Before launch the IAB activity, we check if subscriptions are supported. + checkIfBillingIsSupported(purchaseType, service); + PurchaseFlowLauncher launcher = createPurchaseFlowLauncher(purchaseType, requestCode); + mPurchaseFlows.append(requestCode, launcher); + launcher.launch(service, activity, requestCode, oldItemIds, itemId, developerPayload); + + postActivityStartedSuccess(handler); + } catch (BillingException e) { + if (e.getErrorCode() != Constants.ERROR_PURCHASE_FLOW_ALREADY_EXISTS) { + mPurchaseFlows.delete(requestCode); + } + postOnError(e, handler); + } + } + + @Override + public void onError(BillingException e) { + postBindServiceError(e, handler); + } + }); + } + + private void executeInService(final ServiceBinder.Handler serviceHandler, Handler handler) { + handler.post(new Runnable() { + @Override + public void run() { + final ServiceBinder conn = createServiceBinder(); + + conn.getServiceAsync(new ServiceBinder.Handler() { + @Override + public void onBind(BillingService service) { + try { + serviceHandler.onBind(service); + } finally { + conn.unbindService(); + } + } + + @Override + public void onError(BillingException e) { + serviceHandler.onError(e); + } + }); + } + }); + } + + private PurchaseFlowLauncher createPurchaseFlowLauncher(PurchaseType purchaseType, int requestCode) throws BillingException { + PurchaseFlowLauncher launcher = mPurchaseFlows.get(requestCode); + String type; + + if (launcher != null) { + String message = String.format(Locale.US, Constants.ERROR_MSG_PURCHASE_FLOW_ALREADY_EXISTS, requestCode); + throw new BillingException(Constants.ERROR_PURCHASE_FLOW_ALREADY_EXISTS, message); + } + + if (purchaseType == PurchaseType.SUBSCRIPTION) { + type = Constants.TYPE_SUBSCRIPTION; + } else { + type = Constants.TYPE_IN_APP; + } + return new PurchaseFlowLauncher(mContext, type); + } + + private void executeInServiceOnWorkThread(final ServiceBinder.Handler serviceHandler) { + executeInService(serviceHandler, getWorkHandler()); + } + + private void executeInServiceOnMainThread(final ServiceBinder.Handler serviceHandler) { + executeInService(serviceHandler, getMainHandler()); + } + + private void postBindServiceError(BillingException exception, ErrorHandler handler) { + postOnError(exception, handler); + } + + private void postPurchaseSuccess(final Purchase purchase) { + postEventHandler(new Runnable() { + @Override + public void run() { + if (mPurchaseHandler != null) { + mPurchaseHandler.call(new PurchaseResponse(purchase, null)); + } + } + }); + } + + private void postPurchaseError(final BillingException e) { + postEventHandler(new Runnable() { + @Override + public void run() { + if (mPurchaseHandler != null) { + mPurchaseHandler.call(new PurchaseResponse(null, e)); + } + } + }); + } + + private void postListSuccess(final ItemDetails itemDetails, final ItemDetailsHandler handler) { + postEventHandler(new Runnable() { + @Override + public void run() { + if (handler != null) { + handler.onSuccess(itemDetails); + } + } + }); + } + + private void postPurchasesSuccess(final Purchases purchases, final PurchasesHandler handler) { + postEventHandler(new Runnable() { + @Override + public void run() { + if (handler != null) { + handler.onSuccess(purchases); + } + } + }); + } + + private void postConsumePurchaseSuccess(final ConsumeItemHandler handler) { + postEventHandler(new Runnable() { + @Override + public void run() { + if (handler != null) { + handler.onSuccess(); + } + } + }); + } + + @Deprecated + private void postInventorySuccess(final Purchases purchases, final InventoryHandler handler) { + postEventHandler(new Runnable() { + @Override + public void run() { + if (handler != null) { + handler.onSuccess(purchases); + } + } + }); + } + + private void postActivityStartedSuccess(final StartActivityHandler handler) { + postEventHandler(new Runnable() { + @Override + public void run() { + if (handler != null) { + handler.onSuccess(); + } + } + }); + } + + private void postOnError(final BillingException e, final ErrorHandler handler) { + postEventHandler(new Runnable() { + @Override + public void run() { + if (handler != null) { + handler.onError(e); + } + } + }); + } + + private void postEventHandler(Runnable r) { + getMainHandler().post(r); } private void checkIfIsNotReleased() { - if (mSubscriptionProcessor == null || mItemProcessor == null) { - throw new IllegalStateException("The library was released. Please generate a new instance of BillingProcessor."); + if (mIsReleased) { + throw new IllegalStateException(Constants.ERROR_MSG_LIBRARY_ALREADY_RELEASED); } } @@ -242,6 +681,6 @@ private void checkIsMainThread() { if (Looper.getMainLooper() == Looper.myLooper()) { return; } - throw new IllegalStateException("Must be called from UI Thread."); + throw new IllegalStateException(Constants.ERROR_MSG_METHOD_MUST_BE_CALLED_ON_UI_THREAD); } } \ No newline at end of file diff --git a/library/src/main/java/jp/alessandro/android/iab/BillingService.java b/library/src/main/java/jp/alessandro/android/iab/BillingService.java new file mode 100644 index 0000000..d102c85 --- /dev/null +++ b/library/src/main/java/jp/alessandro/android/iab/BillingService.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2016 Alessandro Yuichi Okimoto + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * Contact email: alessandro@alessandro.jp + */ + +package jp.alessandro.android.iab; + +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteException; + +import com.android.vending.billing.IInAppBillingService; + +import java.util.List; + +/** + * Created by Alessandro Yuichi Okimoto on 2017/02/19. + */ + +public class BillingService implements IInAppBillingService { + + private final IInAppBillingService mService; + + public BillingService(IInAppBillingService service) { + mService = service; + } + + @Override + public int isBillingSupported(int apiVersion, String packageName, String type) throws RemoteException { + return mService.isBillingSupported(apiVersion, packageName, type); + } + + @Override + public Bundle getSkuDetails(int apiVersion, String packageName, String type, Bundle skusBundle) throws RemoteException { + return mService.getSkuDetails(apiVersion, packageName, type, skusBundle); + } + + @Override + public Bundle getBuyIntent(int apiVersion, String packageName, String sku, String type, String developerPayload) throws RemoteException { + return mService.getBuyIntent(apiVersion, packageName, sku, type, developerPayload); + } + + @Override + public Bundle getPurchases(int apiVersion, String packageName, String type, String continuationToken) throws RemoteException { + return mService.getPurchases(apiVersion, packageName, type, continuationToken); + } + + @Override + public int consumePurchase(int apiVersion, String packageName, String purchaseToken) throws RemoteException { + return mService.consumePurchase(apiVersion, packageName, purchaseToken); + } + + @Override + public int stub(int apiVersion, String packageName, String type) throws RemoteException { + return mService.stub(apiVersion, packageName, type); + } + + @Override + public Bundle getBuyIntentToReplaceSkus(int apiVersion, + String packageName, + List oldSkus, + String newSku, + String type, + String developerPayload) throws RemoteException { + return mService.getBuyIntentToReplaceSkus(apiVersion, packageName, oldSkus, newSku, type, developerPayload); + } + + @Override + public IBinder asBinder() { + return mService.asBinder(); + } +} \ No newline at end of file diff --git a/library/src/main/java/jp/alessandro/android/iab/Constants.java b/library/src/main/java/jp/alessandro/android/iab/Constants.java index a4b128f..ccf5c0f 100644 --- a/library/src/main/java/jp/alessandro/android/iab/Constants.java +++ b/library/src/main/java/jp/alessandro/android/iab/Constants.java @@ -87,24 +87,38 @@ private Constants() { // ******************** BILLING ERROR MESSAGES ******************** // - public static final String ERROR_MSG_BAD_RESPONSE = "Failed to parse the purchase data."; + public static final String ERROR_MSG_BAD_RESPONSE = "Failed to parse the purchase data. Please check the log for more info."; public static final String ERROR_MSG_BIND_SERVICE_FAILED = "Failed to bind In-App Billing service. " + "Have you checked if this device supports In-App Billing? " + "If not, you can check if it is available calling isServiceAvailable. " + - "See the documentation for more information."; + "See the documentation for more information or the logs."; + + @SuppressWarnings("checkstyle:linelength") + public static final String ERROR_MSG_BIND_SERVICE_FAILED_NPE = "NullPointerException while trying to bind service. Please check the log for more info."; + @SuppressWarnings("checkstyle:linelength") + public static final String ERROR_MSG_BIND_SERVICE_FAILED_ILLEGAL_ARGUMENT = "IllegalArgumentException while trying to bind service. Please check the log for more info."; + @SuppressWarnings("checkstyle:linelength") + public static final String ERROR_MSG_BIND_SERVICE_FAILED_SERVICE_NULL = "onServiceConnected was called but InAppBillingService is null."; public static final String ERROR_MSG_CONSUME = "Error while trying to consume item."; public static final String ERROR_MSG_GET_PURCHASES = "Error while trying to get purchases."; - public static final String ERROR_MSG_GET_PURCHASES_SIGNATURE = "Purchase or Signature is null."; - public static final String ERROR_MSG_GET_PURCHASES_SIGNATURE_SIZE = "Purchase and Signature size are different."; + public static final String ERROR_MSG_GET_PURCHASES_DATA_LIST = "Purchase list is null."; + public static final String ERROR_MSG_GET_PURCHASES_DIFFERENT_SIZE = "Purchase and Signature have different sizes."; + public static final String ERROR_MSG_GET_PURCHASES_SIGNATURE_LIST = "Signature list is null."; + @SuppressWarnings("checkstyle:linelength") + public static final String ERROR_MSG_GET_PURCHASE_VERIFICATION_FAILED_WITH_PARAMS = "***FAILED*** Purchase signature verification failed. Not adding item. PurchaseData: %s, signature: %s."; + @SuppressWarnings("checkstyle:linelength") + public static final String ERROR_MSG_GET_PURCHASE_VERIFICATION_FAILED = "***FAILED*** Failed to verify if the purchase is valid or not. Please check the log for more info."; public static final String ERROR_MSG_GET_SKU_DETAILS = "Error while trying to get sku details."; - public static final String ERROR_MSG_PURCHASE_FLOW_ALREADY_EXISTS = "Purchase flow already exists. RequestCode: %d."; + public static final String ERROR_MSG_GET_SKU_DETAILS_RESPONSE_LIST_NULL = "Response item details list is null."; + public static final String ERROR_MSG_LIBRARY_ALREADY_RELEASED = "The library was released. Please generate a new instance of BillingProcessor."; public static final String ERROR_MSG_LOST_CONTEXT = "Context is null."; + public static final String ERROR_MSG_METHOD_MUST_BE_CALLED_ON_UI_THREAD = "Must be called from UI Thread."; public static final String ERROR_MSG_NULL_PURCHASE_DATA = "IAB returned null purchaseData or signature."; public static final String ERROR_MSG_PENDING_INTENT = "Pending intent is null. Probably a BUG."; - public static final String ERROR_MSG_PURCHASE_OR_TOKEN_NULL = "Purchase data or token is null."; - public static final String ERROR_MSG_PURCHASE_TOKEN = "Purchase token is null. Probably a BUG."; + public static final String ERROR_MSG_PURCHASE_FLOW_ALREADY_EXISTS = "Purchase flow already exists. RequestCode: %d."; public static final String ERROR_MSG_PURCHASES_NOT_SUPPORTED = "Purchases are not supported on this device."; + public static final String ERROR_MSG_PURCHASE_OR_TOKEN_NULL = "Purchase data or token is null."; public static final String ERROR_MSG_RESULT_NULL_INTENT = "IAB result returned a null intent data."; public static final String ERROR_MSG_RESULT_REQUEST_CODE_INVALID = "An invalid requestCode was given."; public static final String ERROR_MSG_RESULT_OK = "Problem while trying to purchase an item."; @@ -114,6 +128,49 @@ private Constants() { public static final String ERROR_MSG_UNABLE_TO_BUY = "Unable to buy the item."; public static final String ERROR_MSG_VERIFICATION_FAILED = "Signature verification has failed."; public static final String ERROR_MSG_UNEXPECTED_BUNDLE_RESPONSE = "***BUG*** Unexpected type for bundle response code."; - public static final String ERROR_MSG_UNEXPECTED_INTENT_RESPONSE = "***BUG*** Unexpected type for intent response code."; + public static final String ERROR_MSG_UNEXPECTED_BUNDLE_RESPONSE_NULL = "***BUG*** Bundle response is null."; public static final String ERROR_MSG_UPDATE_ARGUMENT_MISSING = "Argument oldItemList cannot be null or empty."; + + + // ******************** BILLING TESTS ******************** // + static final String TEST_ORDER_ID = "GPA.1234-5678-9012-34567"; + static final String TEST_PACKAGE_NAME = "jp.alessandro.android.iab"; + static final String TEST_PRODUCT_ID = "android.test.purchased"; + static final String TEST_PURCHASE_TIME = "1345678900000"; + static final String TEST_DEVELOPER_PAYLOAD = "optional_developer_payload"; + static final String TEST_PURCHASE_TOKEN = "opaque-token-up-to-1000-characters"; + @SuppressWarnings("checkstyle:linelength") + static final String TEST_PUBLIC_KEY_BASE_64 = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7SEtV7WT1vJKdS1fBgskYk+c8j6YUa6kz8NwLbD7EkKGh+0ocSmsde4BewrQDijHC0z6Cxs3s8Kks2JC75NTZUvRQRN5T19Po2owTXTrkT5+Zh2nt5/0lj7RnMyB6qYMeVebDh4oUmj4YkLdQ3QjOpLjGep1xjIunOvJrpMiNkQuRl3ENBbkwEbDKzSquXXMngjfkx2PyHfirbE2dDVXkG85G542KSBfOHF1AQpEO7hiRgz8b5JTuSe4oOdYc11WG4bNxnLpcUeh8xwE9txcipDrz6cUFfb6D3lL8zPIzyZxiwIr0+G0O7ise+vIMaP0JOA891eqruBVEI7WPCyT0QIDAQAB"; + static final String TEST_JSON_RECEIPT = "{" + + "\"orderId\":\"" + TEST_ORDER_ID + "\"," + + "\"packageName\":\"" + TEST_PACKAGE_NAME + "\"," + + "\"productId\":\"" + TEST_PRODUCT_ID + "_%d\"," + + "\"purchaseTime\":" + TEST_PURCHASE_TIME + "," + + "\"purchaseState\":0," + + "\"developerPayload\":\"" + TEST_DEVELOPER_PAYLOAD + "\"," + + "\"purchaseToken\":\"" + TEST_PURCHASE_TOKEN + "\"," + + "\"autoRenewing\":true}"; + + static final String TEST_JSON_RECEIPT_NO_TOKEN = "{" + + "\"orderId\":\"" + TEST_ORDER_ID + "\"," + + "\"packageName\":\"" + TEST_PACKAGE_NAME + "\"," + + "\"productId\":\"" + TEST_PRODUCT_ID + "_%d\"," + + "\"purchaseTime\":" + TEST_PURCHASE_TIME + "," + + "\"purchaseState\":0," + + "\"developerPayload\":\"" + TEST_DEVELOPER_PAYLOAD + "\"," + + "\"autoRenewing\":true}"; + + static final String TEST_JSON_BROKEN = "{\"productId\":\"\""; + + static final String SKU_DETAIL_JSON = "{" + + "\"productId\": \"" + TEST_PRODUCT_ID + "_%d\"," + + "\"type\": \"subs\"," + + "\"price\": \"Â¥1080\"," + + "\"price_amount_micros\": \"10800000\"," + + "\"price_currency_code\": \"JPY\"," + + "\"title\": \"Test Product\"," + + "\"description\": \"Fast and easy use Android In-App Billing\"}"; + + static final String TYPE_IN_APP = "inapp"; + static final String TYPE_SUBSCRIPTION = "subs"; } \ No newline at end of file diff --git a/library/src/main/java/jp/alessandro/android/iab/ItemGetter.java b/library/src/main/java/jp/alessandro/android/iab/ItemGetter.java index 447abf1..ad9e4e7 100644 --- a/library/src/main/java/jp/alessandro/android/iab/ItemGetter.java +++ b/library/src/main/java/jp/alessandro/android/iab/ItemGetter.java @@ -25,17 +25,20 @@ import org.json.JSONException; -import java.util.ArrayList; import java.util.List; +import jp.alessandro.android.iab.logger.Logger; + class ItemGetter { private final int mApiVersion; private final String mPackageName; + private final Logger mLogger; ItemGetter(BillingContext context) { mApiVersion = context.getApiVersion(); mPackageName = context.getContext().getPackageName(); + mLogger = context.getLogger(); } /** @@ -45,45 +48,48 @@ class ItemGetter { * where each string is a product ID for an purchasable item. * See https://developer.android.com/google/play/billing/billing_integrate.html#QueryDetails * - * @param service in-app billing service - * @param itemType "inapp" or "subs" - * @param itemIdList a list of the item ids that you want to request + * @param service in-app billing service + * @param itemType "inapp" or "subs" + * @param requestBundle a bundle that contains the list of item ids that you want to request * @return * @throws BillingException */ - public ItemDetails get(IInAppBillingService service, String itemType, - ArrayList itemIdList) throws BillingException { - Bundle bundle = new Bundle(); - bundle.putStringArrayList(Constants.RESPONSE_ITEM_ID_LIST, itemIdList); + public ItemDetails get(IInAppBillingService service, + String itemType, + Bundle requestBundle) throws BillingException { + try { - Bundle skuDetails = service.getSkuDetails(mApiVersion, mPackageName, itemType, bundle); + Bundle skuDetails = service.getSkuDetails(mApiVersion, mPackageName, itemType, requestBundle); return getItemsFromResponse(skuDetails); } catch (RemoteException e) { throw new BillingException(Constants.ERROR_REMOTE_EXCEPTION, e.getMessage()); } } - private ItemDetails getItemsFromResponse(Bundle skuDetails) throws BillingException { - int response = skuDetails.getInt(Constants.RESPONSE_CODE); + private ItemDetails getItemsFromResponse(Bundle bundle) throws BillingException { + int response = Util.getResponseCodeFromBundle(bundle, mLogger); if (response == Constants.BILLING_RESPONSE_RESULT_OK) { - return getDetailsList(skuDetails); + return getDetailsList(bundle); } else { throw new BillingException(response, Constants.ERROR_MSG_GET_SKU_DETAILS); } } - private ItemDetails getDetailsList(Bundle skuDetails) throws BillingException { - ItemDetails itemDetails = new ItemDetails(); - List detailsList = skuDetails.getStringArrayList(Constants.RESPONSE_DETAILS_LIST); + private ItemDetails getDetailsList(Bundle bundle) throws BillingException { + List detailsList = bundle.getStringArrayList(Constants.RESPONSE_DETAILS_LIST); if (detailsList == null) { - return itemDetails; + throw new BillingException( + Constants.ERROR_UNEXPECTED_TYPE, Constants.ERROR_MSG_GET_SKU_DETAILS_RESPONSE_LIST_NULL); } + ItemDetails itemDetails = new ItemDetails(); for (String response : detailsList) { try { Item product = Item.parseJson(response); itemDetails.put(product); } catch (JSONException e) { - throw new BillingException(Constants.ERROR_BAD_RESPONSE, e.getMessage()); + mLogger.e(Logger.TAG, e.getMessage()); + throw new BillingException( + Constants.ERROR_BAD_RESPONSE, Constants.ERROR_MSG_BAD_RESPONSE); } } return itemDetails; diff --git a/library/src/main/java/jp/alessandro/android/iab/ItemProcessor.java b/library/src/main/java/jp/alessandro/android/iab/ItemProcessor.java deleted file mode 100644 index 47754ac..0000000 --- a/library/src/main/java/jp/alessandro/android/iab/ItemProcessor.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (C) 2016 Alessandro Yuichi Okimoto - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - * Contact email: alessandro@alessandro.jp - */ - -package jp.alessandro.android.iab; - -import android.os.Handler; -import android.os.RemoteException; -import android.text.TextUtils; - -import com.android.vending.billing.IInAppBillingService; - -import jp.alessandro.android.iab.handler.ConsumeItemHandler; -import jp.alessandro.android.iab.handler.PurchaseHandler; - -class ItemProcessor extends BaseProcessor { - - public ItemProcessor(BillingContext context, PurchaseHandler purchaseHandler, - Handler workHandler, Handler mainHandler) { - - super(context, Constants.ITEM_TYPE_INAPP, purchaseHandler, workHandler, mainHandler); - } - - /** - * Consumes previously purchased item to be purchased again - * See http://developer.android.com/google/play/billing/billing_integrate.html#Consume - * - * @param itemId consumable item id - * @param handler callback called asynchronously - */ - public void consume(final String itemId, final ConsumeItemHandler handler) { - executeInServiceOnWorkThread(new ServiceBinder.Handler() { - @Override - public void onBind(IInAppBillingService service) { - try { - consume(service, getToken(service, itemId)); - postConsumeItemSuccess(handler); - } catch (BillingException e) { - postOnError(e, handler); - } - } - - @Override - public void onError() { - postBindServiceError(handler); - } - }); - } - - private String getToken(IInAppBillingService service, String itemId) throws BillingException { - PurchaseGetter getter = new PurchaseGetter(mContext); - Purchases purchases = getter.get(service, Constants.ITEM_TYPE_INAPP); - Purchase purchase = purchases.getByPurchaseId(itemId); - - if (purchase == null || TextUtils.isEmpty(purchase.getToken())) { - throw new BillingException(Constants.ERROR_PURCHASE_DATA, - Constants.ERROR_MSG_PURCHASE_OR_TOKEN_NULL); - } - return purchase.getToken(); - } - - private void consume(IInAppBillingService service, String purchaseToken) throws BillingException { - try { - int response = service.consumePurchase(mContext.getApiVersion(), - mContext.getContext().getPackageName(), purchaseToken); - - if (response != Constants.BILLING_RESPONSE_RESULT_OK) { - throw new BillingException(response, Constants.ERROR_MSG_CONSUME); - } - } catch (RemoteException e) { - throw new BillingException(Constants.ERROR_REMOTE_EXCEPTION, e.getMessage()); - } - } - - private void postConsumeItemSuccess(final ConsumeItemHandler handler) { - postEventHandler(new Runnable() { - @Override - public void run() { - if (handler != null) { - handler.onSuccess(); - } - } - }); - } -} \ No newline at end of file diff --git a/library/src/main/java/jp/alessandro/android/iab/PurchaseFlowLauncher.java b/library/src/main/java/jp/alessandro/android/iab/PurchaseFlowLauncher.java index f667703..39143bc 100644 --- a/library/src/main/java/jp/alessandro/android/iab/PurchaseFlowLauncher.java +++ b/library/src/main/java/jp/alessandro/android/iab/PurchaseFlowLauncher.java @@ -34,10 +34,10 @@ import jp.alessandro.android.iab.logger.Logger; -class PurchaseFlowLauncher { +public class PurchaseFlowLauncher { private final String mItemType; - private final String mSignatureBase64; + private final String mPublicKeyBase64; private final int mApiVersion; private final String mPackageName; private final Logger mLogger; @@ -45,7 +45,7 @@ class PurchaseFlowLauncher { PurchaseFlowLauncher(BillingContext context, String itemType) { mItemType = itemType; - mSignatureBase64 = context.getSignatureBase64(); + mPublicKeyBase64 = context.getPublicKeyBase64(); mApiVersion = context.getApiVersion(); mPackageName = context.getContext().getPackageName(); mLogger = context.getLogger(); @@ -83,7 +83,7 @@ private Bundle getBuyIntent(IInAppBillingService service, List oldItemId } private PendingIntent getPendingIntent(Activity activity, Bundle bundle) throws BillingException { - int response = getResponseCodeFromBundle(bundle); + int response = Util.getResponseCodeFromBundle(bundle, mLogger); if (response != Constants.BILLING_RESPONSE_RESULT_OK) { throw new BillingException(response, Constants.ERROR_MSG_UNABLE_TO_BUY); } @@ -102,9 +102,10 @@ private PendingIntent getPendingIntent(Activity activity, Bundle bundle) throws private void startBuyIntent(final Activity activity, final PendingIntent pendingIntent, int requestCode) throws BillingException { + + IntentSender sender = pendingIntent.getIntentSender(); try { - activity.startIntentSenderForResult( - pendingIntent.getIntentSender(), requestCode, new Intent(), 0, 0, 0); + activity.startIntentSenderForResult(sender, requestCode, new Intent(), 0, 0, 0); } catch (IntentSender.SendIntentException e) { throw new BillingException(Constants.ERROR_SEND_INTENT_FAILED, e.getMessage()); @@ -118,22 +119,18 @@ public Purchase handleResult(int requestCode, int resultCode, Intent data) throw throw new BillingException( Constants.ERROR_BAD_RESPONSE, Constants.ERROR_MSG_RESULT_REQUEST_CODE_INVALID); } - if (data == null) { - throw new BillingException( - Constants.ERROR_BAD_RESPONSE, Constants.ERROR_MSG_RESULT_NULL_INTENT); - } - int responseCode = getResponseCodeFromIntent(data); + int responseCode = Util.getResponseCodeFromIntent(data, mLogger); String purchaseData = data.getStringExtra(Constants.RESPONSE_INAPP_PURCHASE_DATA); String signature = data.getStringExtra(Constants.RESPONSE_INAPP_SIGNATURE); - return getPurchase(resultCode, responseCode, purchaseData, signature, data); + return getPurchase(resultCode, responseCode, purchaseData, signature); } - private Purchase getPurchase(int resultCode, int responseCode, String purchaseData, - String signature, Intent data) throws BillingException { + private Purchase getPurchase(int resultCode, int responseCode, + String purchaseData, String signature) throws BillingException { // Check the Billing response if (resultCode == Activity.RESULT_OK && responseCode == Constants.BILLING_RESPONSE_RESULT_OK) { - return getPurchaseFromIntent(purchaseData, signature, data); + return getPurchaseFromIntent(purchaseData, signature); } // Something happened while trying to purchase the item switch (resultCode) { @@ -144,78 +141,36 @@ private Purchase getPurchase(int resultCode, int responseCode, String purchaseDa throw new BillingException(responseCode, Constants.ERROR_MSG_RESULT_CANCELED); default: - throw new BillingException(resultCode, String.format(Locale.US, Constants.ERROR_MSG_RESULT_UNKNOWN, resultCode)); + throw new BillingException(resultCode, String.format( + Locale.US, Constants.ERROR_MSG_RESULT_UNKNOWN, resultCode)); } } - private Purchase getPurchaseFromIntent(String purchaseData, - String signature, - Intent data) throws BillingException { + private Purchase getPurchaseFromIntent(String purchaseData, String signature) throws BillingException { - printBillingResponse(purchaseData, signature, data); + printBillingResponse(purchaseData, signature); if (purchaseData == null || signature == null) { throw new BillingException(Constants.ERROR_PURCHASE_DATA, Constants.ERROR_MSG_NULL_PURCHASE_DATA); } - if (!Security.verifyPurchase(purchaseData, mLogger, mSignatureBase64, purchaseData, signature)) { + if (!Security.verifyPurchase(purchaseData, mLogger, mPublicKeyBase64, purchaseData, signature)) { throw new BillingException(Constants.ERROR_VERIFICATION_FAILED, Constants.ERROR_MSG_VERIFICATION_FAILED); } try { return Purchase.parseJson(purchaseData, signature); } catch (JSONException e) { + mLogger.e(Logger.TAG, e.getMessage(), e); throw new BillingException(Constants.ERROR_BAD_RESPONSE, Constants.ERROR_MSG_BAD_RESPONSE); } } - private void printBillingResponse(String purchaseData, String dataSignature, Intent data) { + private void printBillingResponse(String purchaseData, String dataSignature) { mLogger.i(Logger.TAG, "------------- BILLING RESPONSE start -------------"); mLogger.i(Logger.TAG, "Successful resultCode from purchase activity."); mLogger.i(Logger.TAG, String.format("Purchase data: %s", purchaseData)); mLogger.i(Logger.TAG, String.format("Data signature: %s", dataSignature)); - mLogger.i(Logger.TAG, String.format("Extras: %s", data.getExtras() != null ? data.getExtras().toString() : "")); mLogger.i(Logger.TAG, "------------- BILLING RESPONSE end -------------"); } - - /** - * Workaround to bug where sometimes response codes come as Long instead of Integer - */ - private int getResponseCodeFromIntent(Intent intent) throws BillingException { - Object obj = intent.getExtras().get(Constants.RESPONSE_CODE); - if (obj == null) { - mLogger.e(Logger.TAG, - "Intent with no response code, assuming there is no problem (known issue)."); - return Constants.BILLING_RESPONSE_RESULT_OK; - } - if (obj instanceof Integer) { - return ((Integer) obj).intValue(); - } - if (obj instanceof Long) { - return (int) ((Long) obj).longValue(); - } - mLogger.e(Logger.TAG, "Unexpected type for intent response code."); - throw new BillingException(Constants.ERROR_UNEXPECTED_TYPE, - Constants.ERROR_MSG_UNEXPECTED_INTENT_RESPONSE); - } - - /** - * Workaround to bug where sometimes response codes come as Long instead of Integer - */ - private int getResponseCodeFromBundle(Bundle bundle) throws BillingException { - Object obj = bundle.get(Constants.RESPONSE_CODE); - if (obj == null) { - mLogger.e(Logger.TAG, "Bundle with null response code, assuming there is no problem (known issue)."); - return Constants.BILLING_RESPONSE_RESULT_OK; - } - if (obj instanceof Integer) { - return ((Integer) obj).intValue(); - } - if (obj instanceof Long) { - return (int) ((Long) obj).longValue(); - } - mLogger.e(Logger.TAG, "Unexpected type for bundle response."); - throw new BillingException( - Constants.ERROR_UNEXPECTED_TYPE, Constants.ERROR_MSG_UNEXPECTED_BUNDLE_RESPONSE); - } } \ No newline at end of file diff --git a/library/src/main/java/jp/alessandro/android/iab/PurchaseGetter.java b/library/src/main/java/jp/alessandro/android/iab/PurchaseGetter.java index c443b5d..fd7bc88 100644 --- a/library/src/main/java/jp/alessandro/android/iab/PurchaseGetter.java +++ b/library/src/main/java/jp/alessandro/android/iab/PurchaseGetter.java @@ -26,19 +26,19 @@ import org.json.JSONException; -import java.util.ArrayList; +import java.util.List; import jp.alessandro.android.iab.logger.Logger; class PurchaseGetter { - private final String mSignatureBase64; + private final String mPublicKeyBase64; private final int mApiVersion; private final String mPackageName; private final Logger mLogger; PurchaseGetter(BillingContext context) { - mSignatureBase64 = context.getSignatureBase64(); + mPublicKeyBase64 = context.getPublicKeyBase64(); mApiVersion = context.getApiVersion(); mPackageName = context.getContext().getPackageName(); mLogger = context.getLogger(); @@ -61,13 +61,14 @@ public Purchases get(IInAppBillingService service, String itemType) throws Billi String continueToken = null; do { Bundle bundle = getPurchasesBundle(service, itemType, continueToken); - checkResponse(bundle, purchases); + checkResponseAndAddPurchases(bundle, purchases); continueToken = bundle.getString(Constants.RESPONSE_INAPP_CONTINUATION_TOKEN); } while (!TextUtils.isEmpty(continueToken)); return purchases; } - private Bundle getPurchasesBundle(IInAppBillingService service, String itemType, + private Bundle getPurchasesBundle(IInAppBillingService service, + String itemType, String continueToken) throws BillingException { try { return service.getPurchases(mApiVersion, mPackageName, itemType, continueToken); @@ -76,69 +77,90 @@ private Bundle getPurchasesBundle(IInAppBillingService service, String itemType, } } - private void checkResponse(Bundle data, Purchases purchasesList) throws BillingException { - int response = data.getInt(Constants.RESPONSE_CODE); - if (response == Constants.BILLING_RESPONSE_RESULT_OK) { - ArrayList purchaseList = - data.getStringArrayList(Constants.RESPONSE_INAPP_PURCHASE_LIST); + private void checkResponseAndAddPurchases(Bundle bundle, Purchases purchases) throws BillingException { + int response = Util.getResponseCodeFromBundle(bundle, mLogger); - ArrayList signatureList = - data.getStringArrayList(Constants.RESPONSE_INAPP_SIGNATURE_LIST); - - checkPurchaseList(purchaseList, signatureList, purchasesList); - } else { + if (response != Constants.BILLING_RESPONSE_RESULT_OK) { throw new BillingException(response, Constants.ERROR_MSG_GET_PURCHASES); } + + List purchaseList = extractPurchaseList(bundle); + List signatureList = extractSignatureList(bundle); + + if (purchaseList.size() != signatureList.size()) { + throw new BillingException( + Constants.ERROR_PURCHASE_DATA, Constants.ERROR_MSG_GET_PURCHASES_DIFFERENT_SIZE); + } + addAllPurchases(purchaseList, signatureList, purchases); } - private void checkPurchaseList(ArrayList purchaseList, ArrayList signatureList, - Purchases purchasesList) throws BillingException { - if ((purchaseList == null) || (signatureList == null)) { - throw new BillingException(Constants.ERROR_PURCHASE_DATA, - Constants.ERROR_MSG_GET_PURCHASES_SIGNATURE); + private List extractPurchaseList(Bundle bundle) throws BillingException { + List purchaseList = bundle.getStringArrayList(Constants.RESPONSE_INAPP_PURCHASE_LIST); + + if (purchaseList == null) { + throw new BillingException( + Constants.ERROR_PURCHASE_DATA, Constants.ERROR_MSG_GET_PURCHASES_DATA_LIST); } - if (purchaseList.size() != signatureList.size()) { - throw new BillingException(Constants.ERROR_PURCHASE_DATA, - Constants.ERROR_MSG_GET_PURCHASES_SIGNATURE_SIZE); + return purchaseList; + } + + private List extractSignatureList(Bundle bundle) throws BillingException { + List signatureList = bundle.getStringArrayList(Constants.RESPONSE_INAPP_SIGNATURE_LIST); + + if (signatureList == null) { + throw new BillingException( + Constants.ERROR_PURCHASE_DATA, Constants.ERROR_MSG_GET_PURCHASES_SIGNATURE_LIST); } - verifyAllPurchases(purchaseList, signatureList, purchasesList); + return signatureList; } - private void verifyAllPurchases(ArrayList purchaseList, - ArrayList signatureList, - Purchases purchasesList) throws BillingException { + private void addAllPurchases(List purchaseList, + List signatureList, + Purchases purchases) throws BillingException { + int errors = 0; for (int i = 0; i < purchaseList.size(); i++) { String purchaseData = purchaseList.get(i); String signature = signatureList.get(i); - verifyBeforeAddPurchase(purchasesList, purchaseData, signature); + if (!verifyBeforeAddPurchase(purchaseData, signature, purchases)) { + errors++; + } + } + if (errors > 0) { + throw new BillingException( + Constants.ERROR_PURCHASE_DATA, Constants.ERROR_MSG_GET_PURCHASE_VERIFICATION_FAILED); } } - private void verifyBeforeAddPurchase(Purchases purchases, String purchaseData, - String signature) throws BillingException { - if (!TextUtils.isEmpty(purchaseData)) { - if (Security.verifyPurchase(purchaseData, mLogger, mSignatureBase64, purchaseData, signature)) { - addPurchase(purchases, purchaseData, signature); - } else { - mLogger.w(Logger.TAG, String.format( - "Purchase not valid. PurchaseData: %s, signature: %s", purchaseData, signature)); - } + private boolean verifyBeforeAddPurchase(String purchaseData, + String signature, + Purchases purchases) throws BillingException { + + if (Security.verifyPurchase(purchaseData, mLogger, mPublicKeyBase64, purchaseData, signature)) { + addPurchase(purchaseData, signature, purchases); + return true; } + printPurchaseVerificationFailed(purchaseData, signature); + return false; } - private void addPurchase(Purchases purchases, - String purchaseData, - String signature) throws BillingException { + private void addPurchase(String purchaseData, + String signature, + Purchases purchases) throws BillingException { Purchase purchase; try { purchase = Purchase.parseJson(purchaseData, signature); } catch (JSONException e) { - throw new BillingException(Constants.ERROR_BAD_RESPONSE, e.getMessage()); - } - if (TextUtils.isEmpty(purchase.getToken())) { - throw new BillingException(Constants.ERROR_PURCHASE_DATA, - Constants.ERROR_MSG_PURCHASE_TOKEN); + mLogger.e(Logger.TAG, e.getMessage(), e); + throw new BillingException(Constants.ERROR_BAD_RESPONSE, Constants.ERROR_MSG_BAD_RESPONSE); } purchases.put(purchase); } + + private void printPurchaseVerificationFailed(String purchaseData, String dataSignature) { + mLogger.e(Logger.TAG, "------------- BILLING GET PURCHASES start -------------"); + mLogger.e(Logger.TAG, Constants.ERROR_MSG_GET_PURCHASE_VERIFICATION_FAILED_WITH_PARAMS); + mLogger.e(Logger.TAG, String.format("Purchase data: %s", purchaseData)); + mLogger.e(Logger.TAG, String.format("Data signature: %s", dataSignature)); + mLogger.e(Logger.TAG, "------------- BILLING GET PURCHASES end -------------"); + } } \ No newline at end of file diff --git a/library/src/main/java/jp/alessandro/android/iab/Security.java b/library/src/main/java/jp/alessandro/android/iab/Security.java index 9507a53..164298b 100644 --- a/library/src/main/java/jp/alessandro/android/iab/Security.java +++ b/library/src/main/java/jp/alessandro/android/iab/Security.java @@ -17,15 +17,20 @@ import android.text.TextUtils; import android.util.Base64; +import android.util.Log; +import java.io.UnsupportedEncodingException; import java.security.InvalidKeyException; import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; import java.security.PublicKey; import java.security.Signature; import java.security.SignatureException; import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; +import java.util.Locale; import jp.alessandro.android.iab.logger.Logger; @@ -42,6 +47,8 @@ public class Security { private static final String KEY_FACTORY_ALGORITHM = "RSA"; private static final String SIGNATURE_ALGORITHM = "SHA1withRSA"; + @SuppressWarnings("checkstyle:linelength") + private static final String PRIVATE_KEY_BASE_64_ENCODED = "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDtIS1XtZPW8kp1LV8GCyRiT5zyPphRrqTPw3AtsPsSQoaH7ShxKax17gF7CtAOKMcLTPoLGzezwqSzYkLvk1NlS9FBE3lPX0+jajBNdOuRPn5mHae3n/SWPtGczIHqpgx5V5sOHihSaPhiQt1DdCM6kuMZ6nXGMi6c68mukyI2RC5GXcQ0FuTARsMrNKq5dcyeCN+THY/Id+KtsTZ0NVeQbzkbnjYpIF84cXUBCkQ7uGJGDPxvklO5J7ig51hzXVYbhs3GculxR6HzHAT23FyKkOvPpxQV9voPeUvzM8jPJnGLAivT4bQ7uKx768gxo/Qk4Dz3V6qu4FUQjtY8LJPRAgMBAAECggEATUdYrZLhYVWI6nMk2qVa8Ccd8Nxxa31M/OCmeF2LFUJU8YtaeLaqG6y7EsxNTbAAXjBx9JikKJMwdb16LvWGYia5RUoBaNqY65q5rySBeM4zBzh25iLc5PIIAd+sHzqKKilgwNMXNPQ8rlk4HrmEmZwxIssEItlL05wMGDafGaux8OVBlLqRMIGAQjaKjGc66SgFxkiiiolUlQRcvm7szXC/wXi28f7JNImFXeH5FwhHB41fbHF7eHci2/9PRCTI6pawiiSVJqj3g0A7TNuYXSB9AtZdHX1iOr72N33P/MvWwnapGXkKDm6TX+my6XTQY0qZc1MtPlEuWKMUWsgweQKBgQD/DhNkBhaY8DpOflgksmJFumG2po8CK9eGQreUs/NoE1nKxItQAVLjohVd8+aoTuiG2IUCX9Pe5OYOAOjNQ4owvFx5KBty6lhGXaOOrRUbfRtn3PYTgDsc+n75AIkn6UyabaDEIY8EmyC8wr3PX/fEod5vf1J+mKSMLn13gj1KXwKBgQDuAhlMeYMXA0sJyUhwCKMa6dnBEoxKNjHDclLDfpPVf47ogA+P2MTvKnOn7EfwfLmiU/KqbYM+8KgJRyaofMyWvoIB873PI0G/l/d8DW3rMv1K8zPLrgknUpKDMt0rFzxlSm5tYFwvSTseOUZLPvEJLcYUKfuf2uWk82gdI8ovzwKBgFBYeclHlbTF8Egrys58lzKJ/SARpfk0IGe9+qDQczv05JNYiN5CHH9y3rJDFAUvHlbkPDo8P7z2dHYy2SNYRF8H50WPWd5AbmB0PQLECWMobQqx856/BWAilP8RqSM2fhgjssI2JBx6VbzAyBRckeuSZkTPYghZQ3SZbJLKJ06XAoGAJy6XRZy3dQFoyAqn7zGs0FBxNbS8/bagSKG4eFCNO9eNCj+S0EaKXSkq8xkV2sRdtxiE2YO/2Iu7zhM1jQVGlQZ11qZut/wA5e65omV/k/nH8x/Ihh53iU6xqgGkoWRo3+/57+2uH2a54cbiCJ8rBSzQ8B7dOrrJlXcwy6NJtMcCgYAdM7gR+aVFXsedq1QEXvpnggua70VPu56xHJ8GCh1zrDu9UubkZQ9bB74kNakzvhGBmLRs+Grp6wLIm66C4MgmlUbxDnOWQLkmHvBDVn9z60RE/MTxADLqlGWDkuUpSZHN1WSfKlRpj/VeLVpAREWYBSXqjWZA5sD/GKG8l6OTJg=="; private Security() { } @@ -54,7 +61,7 @@ private Security() { * * @param purchaseData the purchase data used for debug validation. * @param logger the logger to use for printing events - * @param base64PublicKey the base64-encoded public key to use for verifying. + * @param base64PublicKey rsa public key generated by Google Play Developer Console * @param signedData the signed JSON string (signed, not encrypted) * @param signature the signature for the data, signed with the private key */ @@ -97,7 +104,7 @@ private static boolean isTestingStaticResponse(String purchaseData) { * Base64-encoded public key. * * @param logger the logger to use for printing events - * @param encodedPublicKey Base64-encoded public key + * @param encodedPublicKey rsa public key generated by Google Play Developer Console * @throws IllegalArgumentException if encodedPublicKey is invalid */ private static PublicKey generatePublicKey(Logger logger, String encodedPublicKey) { @@ -108,10 +115,10 @@ private static PublicKey generatePublicKey(Logger logger, String encodedPublicKe } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } catch (InvalidKeySpecException e) { - logger.e(Logger.TAG, "Invalid key specification."); + logger.e(Logger.TAG, e.getMessage(), e); throw new IllegalArgumentException(e); } catch (IllegalArgumentException e) { - logger.e(Logger.TAG, "Base64 decoding failed."); + logger.e(Logger.TAG, e.getMessage(), e); throw e; } } @@ -121,32 +128,69 @@ private static PublicKey generatePublicKey(Logger logger, String encodedPublicKe * signature on the data. Returns true if the data is correctly signed. * * @param logger the logger to use for printing events - * @param publicKey public key associated with the developer account + * @param publicKey rsa public key generated by Google Play Developer Console * @param signedData signed data from server * @param signature server signature * @return true if the data and signature match */ private static boolean verify(Logger logger, PublicKey publicKey, String signedData, String signature) { - Signature sig; try { - sig = Signature.getInstance(SIGNATURE_ALGORITHM); + byte[] signatureBytes = Base64.decode(signature, Base64.DEFAULT); + Signature sig = Signature.getInstance(SIGNATURE_ALGORITHM); + sig.initVerify(publicKey); - sig.update(signedData.getBytes()); - if (!sig.verify(Base64.decode(signature, Base64.DEFAULT))) { + sig.update(signedData.getBytes("UTF-8")); + if (!sig.verify(signatureBytes)) { logger.e(Logger.TAG, "Signature verification failed."); return false; } return true; + } catch (UnsupportedEncodingException e) { + logger.e(Logger.TAG, e.getMessage(), e); } catch (NoSuchAlgorithmException e) { - logger.e(Logger.TAG, "NoSuchAlgorithmException."); + logger.e(Logger.TAG, e.getMessage(), e); } catch (InvalidKeyException e) { - logger.e(Logger.TAG, "Invalid key specification."); + logger.e(Logger.TAG, e.getMessage(), e); } catch (SignatureException e) { - logger.e(Logger.TAG, "Signature exception."); + logger.e(Logger.TAG, e.getMessage(), e); } catch (IllegalArgumentException e) { - logger.e(Logger.TAG, "Base64 decoding failed."); + logger.e(Logger.TAG, e.getMessage(), e); } return false; } + + /** + * Sign some data for testing + * + * @param signedData + * @return + */ + static String signData(String signedData) { + String baseEncodedSign = null; + try { + PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(Base64.decode(PRIVATE_KEY_BASE_64_ENCODED.getBytes("UTF-8"), Base64.DEFAULT)); + KeyFactory kf = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM); + PrivateKey privateKey = kf.generatePrivate(spec); + + Signature sig = Signature.getInstance(SIGNATURE_ALGORITHM); + sig.initSign(privateKey); + sig.update(signedData.getBytes("UTF-8")); + baseEncodedSign = Base64.encodeToString(sig.sign(), Base64.DEFAULT); + + Log.d(Logger.TAG, String.format(Locale.ENGLISH, "BaseEncodedSign: %s", baseEncodedSign)); + + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } catch (InvalidKeySpecException e) { + e.printStackTrace(); + } catch (InvalidKeyException e) { + e.printStackTrace(); + } catch (SignatureException e) { + e.printStackTrace(); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + return baseEncodedSign; + } } \ No newline at end of file diff --git a/library/src/main/java/jp/alessandro/android/iab/ServiceBinder.java b/library/src/main/java/jp/alessandro/android/iab/ServiceBinder.java index 3359f93..2c9a56f 100644 --- a/library/src/main/java/jp/alessandro/android/iab/ServiceBinder.java +++ b/library/src/main/java/jp/alessandro/android/iab/ServiceBinder.java @@ -25,24 +25,28 @@ import com.android.vending.billing.IInAppBillingService; +import jp.alessandro.android.iab.logger.Logger; + class ServiceBinder implements ServiceConnection { public interface Handler { - void onBind(IInAppBillingService service); + void onBind(BillingService service); - void onError(); + void onError(BillingException exception); } private final Context mContext; private final Intent mIntent; + private final Logger mLogger; private final android.os.Handler mEventHandler; private Handler mHandler; - public ServiceBinder(Context context, Intent intent) { - mContext = context.getApplicationContext(); + public ServiceBinder(BillingContext context, Intent intent) { + mContext = context.getContext(); mIntent = intent; + mLogger = context.getLogger(); mEventHandler = new android.os.Handler(); } @@ -70,25 +74,55 @@ public void onServiceDisconnected(ComponentName name) { setBinder(null); } - private void setBinder(android.os.IBinder binder) { + protected void setBinder(android.os.IBinder binder) { IInAppBillingService service = IInAppBillingService.Stub.asInterface(binder); Handler handler = mHandler; mHandler = null; - if (handler != null) { + if (handler == null) { + return; + } + if (service == null) { + BillingException e = new BillingException( + Constants.ERROR_BIND_SERVICE_FAILED_EXCEPTION, + Constants.ERROR_MSG_BIND_SERVICE_FAILED_SERVICE_NULL); + + postBinderError(e, handler); + } else { postBinder(service, handler); } } - private void bindService(Handler handler) { + protected void bindService(Handler handler) { if (mHandler != null) { return; } - boolean bound = mContext.bindService(mIntent, this, Context.BIND_AUTO_CREATE); - if (bound) { - mHandler = handler; - } else { - handler.onError(); + try { + boolean bound = mContext.bindService(mIntent, this, Context.BIND_AUTO_CREATE); + if (bound) { + mHandler = handler; + } else { + BillingException e = new BillingException( + Constants.ERROR_BIND_SERVICE_FAILED_EXCEPTION, + Constants.ERROR_MSG_BIND_SERVICE_FAILED); + postBinderError(e, handler); + } + } catch (NullPointerException e1) { + mLogger.e(Logger.TAG, e1.getMessage()); + + // Meizu M3s devices may throw a NPE + BillingException e = new BillingException( + Constants.ERROR_BIND_SERVICE_FAILED_EXCEPTION, + Constants.ERROR_MSG_BIND_SERVICE_FAILED_NPE); + postBinderError(e, handler); + } catch (IllegalArgumentException e2) { + mLogger.e(Logger.TAG, e2.getMessage()); + + // Some devices may throw IllegalArgumentException + BillingException e = new BillingException( + Constants.ERROR_BIND_SERVICE_FAILED_EXCEPTION, + Constants.ERROR_MSG_BIND_SERVICE_FAILED_ILLEGAL_ARGUMENT); + postBinderError(e, handler); } } @@ -97,7 +131,18 @@ private void postBinder(final IInAppBillingService service, final Handler handle @Override public void run() { if (handler != null) { - handler.onBind(service); + handler.onBind(new BillingService(service)); + } + } + }); + } + + private void postBinderError(final BillingException exception, final Handler handler) { + postEventHandler(new Runnable() { + @Override + public void run() { + if (handler != null) { + handler.onError(exception); } } }); diff --git a/library/src/main/java/jp/alessandro/android/iab/SubscriptionProcessor.java b/library/src/main/java/jp/alessandro/android/iab/SubscriptionProcessor.java deleted file mode 100644 index ae5ef69..0000000 --- a/library/src/main/java/jp/alessandro/android/iab/SubscriptionProcessor.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (C) 2016 Alessandro Yuichi Okimoto - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - * Contact email: alessandro@alessandro.jp - */ - -package jp.alessandro.android.iab; - -import android.app.Activity; -import android.os.Handler; - -import java.util.List; - -import jp.alessandro.android.iab.handler.PurchaseHandler; -import jp.alessandro.android.iab.handler.StartActivityHandler; - -class SubscriptionProcessor extends BaseProcessor { - - public SubscriptionProcessor(BillingContext context, PurchaseHandler purchaseHandler, - Handler workHandler, Handler mainHandler) { - - super(context, Constants.ITEM_TYPE_SUBSCRIPTION, purchaseHandler, workHandler, mainHandler); - } - - /** - * Updates a subscription (Upgrade / Downgrade) - * This method MUST be called from UI Thread - * This can only be done on API version 5 - * Even if you set up to use the API version 3 - * It will automatically use API version 5 - * IMPORTANT: In some devices it may not work - * - * @param activity activity calling this method - * @param requestCode - * @param oldItemIds a list of item ids to be updated - * @param itemId new subscription item id - * @param developerPayload optional argument to be sent back with the purchase information. It helps to identify the user - * @param handler callback called asynchronously - */ - public void update(Activity activity, - int requestCode, - List oldItemIds, - String itemId, - String developerPayload, - StartActivityHandler handler) { - if (oldItemIds == null || oldItemIds.isEmpty()) { - throw new IllegalArgumentException(Constants.ERROR_MSG_UPDATE_ARGUMENT_MISSING); - } - super.startPurchase(activity, requestCode, oldItemIds, itemId, developerPayload, handler); - } -} \ No newline at end of file diff --git a/library/src/main/java/jp/alessandro/android/iab/Util.java b/library/src/main/java/jp/alessandro/android/iab/Util.java new file mode 100644 index 0000000..411a129 --- /dev/null +++ b/library/src/main/java/jp/alessandro/android/iab/Util.java @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2016 Alessandro Yuichi Okimoto + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * Contact email: alessandro@alessandro.jp + */ + +package jp.alessandro.android.iab; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import jp.alessandro.android.iab.logger.Logger; + +/** + * Created by Alessandro Yuichi Okimoto on 2017/02/19. + */ + +class Util { + + private Util() { + } + + public static BillingContext newBillingContext(Context context) { + return new BillingContext(context, Constants.TEST_PUBLIC_KEY_BASE_64, BillingApi.VERSION_3, null); + } + + public static Intent newOkIntent() { + return newIntent(0, Constants.TEST_JSON_RECEIPT, Security.signData(Constants.TEST_JSON_RECEIPT)); + } + + public static Intent newIntent(int responseCode, String data, String signature) { + final Intent intent = new Intent(); + intent.putExtra(Constants.RESPONSE_CODE, responseCode); + intent.putExtra(Constants.RESPONSE_INAPP_PURCHASE_DATA, data); + intent.putExtra(Constants.RESPONSE_INAPP_SIGNATURE, signature); + return intent; + } + + public static ArrayList createInvalidSignatureRandomlyArray(List purchaseData) { + int size = purchaseData.size(); + int randomIndex = getRandomIndex(size); + ArrayList signatures = new ArrayList<>(); + for (int i = 0; i < size; i++) { + if (i == randomIndex) { + signatures.add("signature"); + } else { + signatures.add(Security.signData(purchaseData.get(i))); + } + } + return signatures; + } + + public static ArrayList createSignatureArray(List purchaseData) { + ArrayList signatures = new ArrayList<>(); + for (String data : purchaseData) { + signatures.add(Security.signData(data)); + } + return signatures; + } + + public static Bundle createPurchaseBundle(int responseCode, int startIndex, int size, String continuationString) { + ArrayList purchaseArray = Util.createPurchaseJsonArray(startIndex, size); + Bundle bundle = new Bundle(); + bundle.putInt(Constants.RESPONSE_CODE, responseCode); + bundle.putStringArrayList(Constants.RESPONSE_INAPP_PURCHASE_LIST, purchaseArray); + bundle.putStringArrayList(Constants.RESPONSE_INAPP_SIGNATURE_LIST, createSignatureArray(purchaseArray)); + bundle.putString(Constants.RESPONSE_INAPP_CONTINUATION_TOKEN, continuationString); + + return bundle; + } + + public static Bundle createPurchaseWithNoTokenBundle(int responseCode, int startIndex, int size, String continuationString) { + ArrayList purchaseArray = Util.createPurchaseWithNoTokenJsonArray(startIndex, size); + Bundle bundle = new Bundle(); + bundle.putInt(Constants.RESPONSE_CODE, responseCode); + bundle.putStringArrayList(Constants.RESPONSE_INAPP_PURCHASE_LIST, purchaseArray); + bundle.putStringArrayList(Constants.RESPONSE_INAPP_SIGNATURE_LIST, createSignatureArray(purchaseArray)); + bundle.putString(Constants.RESPONSE_INAPP_CONTINUATION_TOKEN, continuationString); + + return bundle; + } + + public static ArrayList createPurchaseJsonArray(int startIndex, int size) { + ArrayList purchases = new ArrayList<>(); + for (int i = startIndex; i < (size + startIndex); i++) { + String json = String.format(Locale.ENGLISH, Constants.TEST_JSON_RECEIPT, i); + purchases.add(json); + } + return purchases; + } + + public static ArrayList createPurchaseWithNoTokenJsonArray(int startIndex, int size) { + ArrayList purchases = new ArrayList<>(); + for (int i = startIndex; i < (size + startIndex); i++) { + String json = String.format(Locale.ENGLISH, Constants.TEST_JSON_RECEIPT_NO_TOKEN, i); + purchases.add(json); + } + return purchases; + } + + public static ArrayList createPurchaseJsonBrokenArray() { + ArrayList data = new ArrayList<>(); + data.add(Constants.TEST_JSON_BROKEN); + data.add(String.format(Locale.ENGLISH, Constants.TEST_JSON_RECEIPT, 0)); + return data; + } + + public static ArrayList createSkuItemDetailsJsonArray(int size) { + ArrayList purchases = new ArrayList<>(); + for (int i = 0; i < size; i++) { + String json = String.format(Locale.ENGLISH, Constants.SKU_DETAIL_JSON, i); + purchases.add(json); + } + return purchases; + } + + public static ArrayList createSkuDetailsJsonBrokenArray() { + ArrayList data = new ArrayList<>(); + data.add(String.format(Locale.ENGLISH, Constants.SKU_DETAIL_JSON, 0)); + data.add(Constants.TEST_JSON_BROKEN); + data.add(String.format(Locale.ENGLISH, Constants.SKU_DETAIL_JSON, 2)); + return data; + } + + public static int getRandomIndex(int size) { + return (int) (Math.random() * size); + } + + public static int getResponseCodeFromBundle(Bundle bundle, Logger logger) throws BillingException { + if (bundle == null) { + logger.e(Logger.TAG, Constants.ERROR_MSG_UNEXPECTED_BUNDLE_RESPONSE_NULL); + throw new BillingException( + Constants.ERROR_UNEXPECTED_TYPE, Constants.ERROR_MSG_UNEXPECTED_BUNDLE_RESPONSE_NULL); + } + return Util.getResponseCode(bundle.get(Constants.RESPONSE_CODE), logger); + } + + public static int getResponseCodeFromIntent(Intent intent, Logger logger) throws BillingException { + if (intent == null) { + throw new BillingException( + Constants.ERROR_UNEXPECTED_TYPE, Constants.ERROR_MSG_RESULT_NULL_INTENT); + } + return Util.getResponseCode(intent.getExtras().get(Constants.RESPONSE_CODE), logger); + } + + /** + * Workaround to bug where sometimes response codes come as Long instead of Integer + */ + private static int getResponseCode(Object obj, Logger logger) throws BillingException { + if (obj == null) { + logger.e(Logger.TAG, + "Intent with no response code, assuming there is no problem (known issue)."); + return Constants.BILLING_RESPONSE_RESULT_OK; + } + if (obj instanceof Integer) { + return ((Integer) obj).intValue(); + } + if (obj instanceof Long) { + return (int) ((Long) obj).longValue(); + } + logger.e(Logger.TAG, "Unexpected type for intent response code."); + throw new BillingException( + Constants.ERROR_UNEXPECTED_TYPE, Constants.ERROR_MSG_UNEXPECTED_BUNDLE_RESPONSE); + } +} \ No newline at end of file diff --git a/library/src/main/java/jp/alessandro/android/iab/handler/PurchasesHandler.java b/library/src/main/java/jp/alessandro/android/iab/handler/PurchasesHandler.java new file mode 100644 index 0000000..0c8c68e --- /dev/null +++ b/library/src/main/java/jp/alessandro/android/iab/handler/PurchasesHandler.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2016 Alessandro Yuichi Okimoto + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * Contact email: alessandro@alessandro.jp + */ + +package jp.alessandro.android.iab.handler; + +import jp.alessandro.android.iab.Purchases; + +/** + * Created by Alessandro Yuichi Okimoto on 2017/02/19. + */ + +public interface PurchasesHandler extends ErrorHandler { + + void onSuccess(Purchases purchases); +} \ No newline at end of file diff --git a/library/src/main/java/jp/alessandro/android/iab/logger/DiscardLogger.java b/library/src/main/java/jp/alessandro/android/iab/logger/DiscardLogger.java index a65be40..3f6e34d 100644 --- a/library/src/main/java/jp/alessandro/android/iab/logger/DiscardLogger.java +++ b/library/src/main/java/jp/alessandro/android/iab/logger/DiscardLogger.java @@ -30,6 +30,11 @@ public void e(String tag, String msg) { } + @Override + public void e(String tag, String msg, Exception e) { + + } + @Override public void i(String tag, String msg) { diff --git a/library/src/main/java/jp/alessandro/android/iab/logger/Logger.java b/library/src/main/java/jp/alessandro/android/iab/logger/Logger.java index 11fbea5..c155281 100644 --- a/library/src/main/java/jp/alessandro/android/iab/logger/Logger.java +++ b/library/src/main/java/jp/alessandro/android/iab/logger/Logger.java @@ -26,6 +26,8 @@ public interface Logger { void e(String tag, String msg); + void e(String tag, String msg, Exception e); + void i(String tag, String msg); void v(String tag, String msg); diff --git a/library/src/main/java/jp/alessandro/android/iab/logger/SystemLogger.java b/library/src/main/java/jp/alessandro/android/iab/logger/SystemLogger.java index 0cdfb51..8dd15b3 100644 --- a/library/src/main/java/jp/alessandro/android/iab/logger/SystemLogger.java +++ b/library/src/main/java/jp/alessandro/android/iab/logger/SystemLogger.java @@ -32,6 +32,11 @@ public void e(String tag, String msg) { Log.e(tag, msg); } + @Override + public void e(String tag, String msg, Exception e) { + Log.e(tag, msg, e); + } + @Override public void i(String tag, String msg) { Log.i(tag, msg); From 32ed1c7293ce118a0ad874f05f49f33572c82ddb Mon Sep 17 00:00:00 2001 From: Alessandro Yuichi Okimoto Date: Mon, 20 Feb 2017 15:23:31 +0900 Subject: [PATCH 2/4] Updating gradle files implementing jacoco --- build.gradle | 7 ++--- extension-rxjava/build.gradle | 14 ++++----- extension-rxjava/gradle.properties | 1 - gradle.properties | 5 +++- gradle/bintray_push.gradle | 27 ++++++++++++++++- gradle/dependencies.gradle | 32 ++++++++++++++------ gradle/jacoco.gradle | 37 ++++++++++++++++++++++++ gradle/wrapper/gradle-wrapper.properties | 4 +-- library/build.gradle | 18 +++++++++--- library/gradle.properties | 1 - 10 files changed, 114 insertions(+), 32 deletions(-) create mode 100644 gradle/jacoco.gradle diff --git a/build.gradle b/build.gradle index 8e8e6a7..599108b 100644 --- a/build.gradle +++ b/build.gradle @@ -6,18 +6,17 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.dicedmelon.gradle:jacoco-android:0.1.1' - classpath 'com.android.tools.build:gradle:2.2.2' + classpath 'com.android.tools.build:gradle:2.2.3' classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5' - classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7' + classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7.3' } } allprojects { repositories { jcenter() - maven { url 'https://dl.bintray.com/alessanrojp/maven/' } + mavenCentral() } } diff --git a/extension-rxjava/build.gradle b/extension-rxjava/build.gradle index aa02124..3dd9a1d 100644 --- a/extension-rxjava/build.gradle +++ b/extension-rxjava/build.gradle @@ -17,8 +17,8 @@ */ apply plugin: 'com.android.library' -apply plugin: 'jacoco-android' +apply from: rootProject.file('gradle/jacoco.gradle') apply from: rootProject.file('gradle/checkstyle.gradle') android { @@ -28,7 +28,8 @@ android { defaultConfig { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - testInstrumentationRunner rootProject.ext.testInstrumentationRunner +// testInstrumentationRunner rootProject.ext.testInstrumentationRunner +// multiDexEnabled true } buildTypes { @@ -58,13 +59,8 @@ android { dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile project(':library') - compile dep.rxjava - compile dep.rxandroid - - // Android Testing - testCompile dep.junit - androidTestCompile dep.testRunner - androidTestCompile dep.testRules + compile deps.rxjava + compile deps.rxandroid } android.libraryVariants.all { variant -> diff --git a/extension-rxjava/gradle.properties b/extension-rxjava/gradle.properties index 076e28f..2bc3a17 100644 --- a/extension-rxjava/gradle.properties +++ b/extension-rxjava/gradle.properties @@ -15,7 +15,6 @@ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Contact email: alessandro@alessandro.jp # -VERSION_NAME=v1.0.0 POM_ARTIFACT_ID=easy-checkout-extension-rxjava POM_NAME=RxJava Extension POM_DESCRIPTION=A RxJava extension for Easy Checkout Library diff --git a/gradle.properties b/gradle.properties index 06eac42..caa83ee 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,11 +1,14 @@ +VERSION_NAME=v1.0.1 GROUP=jp.alessandro.android POM_DEVELOPER_ID=alessandrojp POM_DEVELOPER_NAME=Alessandro Yuichi Okimoto POM_DEVELOPER_EMAIL=alessandro@alessandro.jp +POM_ISSUE_URL=https://github.com/alessandrojp/easy-checkout/issues POM_LICENCE_NAME=The Apache Software License, Version 2.0 POM_LICENCE_DIST=repo POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt POM_SCM_CONNECTION=https://github.com/alessandrojp/easy-checkout.git POM_SCM_DEV_CONNECTION=https://github.com/alessandrojp/easy-checkout.git POM_SCM_URL=https://github.com/alessandrojp/easy-checkout/ -POM_URL=https://github.com/alessandrojp/easy-checkout/ \ No newline at end of file +POM_URL=https://github.com/alessandrojp/easy-checkout/ +org.gradle.jvmargs=-Xmx8192M \ No newline at end of file diff --git a/gradle/bintray_push.gradle b/gradle/bintray_push.gradle index ac7b085..6aebbb4 100644 --- a/gradle/bintray_push.gradle +++ b/gradle/bintray_push.gradle @@ -16,7 +16,6 @@ * Contact email: alessandro@alessandro.jp */ -apply plugin: 'maven' apply plugin: 'maven-publish' apply plugin: 'com.jfrog.bintray' @@ -33,6 +32,32 @@ publishing { artifact(androidJavadocsJar) artifact source: file("${project.buildDir}/outputs/aar/${project.name}-release.aar") artifact source: file("${project.buildDir}/libs/${project.name}-${project.version}.jar") + + pom.withXml { + Node root = asNode() + root.appendNode('name', POM_ARTIFACT_ID) + root.appendNode('description', POM_DESCRIPTION) + root.appendNode('url', POM_URL) + + def issues = root.appendNode('issueManagement') + issues.appendNode('system', 'github') + issues.appendNode('url', POM_ISSUE_URL) + + def scm = root.appendNode('scm') + scm.appendNode('url', POM_SCM_URL) + scm.appendNode('connection', POM_SCM_CONNECTION) + scm.appendNode('developerConnection', POM_SCM_DEV_CONNECTION) + + def license = root.appendNode('licenses').appendNode('license') + license.appendNode('name', POM_LICENCE_NAME) + license.appendNode('url', POM_LICENCE_URL) + license.appendNode('distribution', POM_LICENCE_DIST) + + def developer = root.appendNode('developers').appendNode('developer') + developer.appendNode('id', POM_DEVELOPER_ID) + developer.appendNode('name', POM_DEVELOPER_NAME) + developer.appendNode('email', POM_DEVELOPER_EMAIL) + } } } } diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle index eca68e5..2885800 100644 --- a/gradle/dependencies.gradle +++ b/gradle/dependencies.gradle @@ -1,14 +1,14 @@ /* * Copyright (C) 2016 Alessandro Yuichi Okimoto * - * Licensed under the Apache License, Version 2.0 (the "License"); + * Licensed under the Apache License, Version 2.0 (the 'License'); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, + * distributed under the License is distributed on an 'AS IS' BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. @@ -17,22 +17,36 @@ */ ext { + supportLibs = '25.1.0' compileSdkVersion = 25 buildToolsVersion = '25.0.1' minSdkVersion = 9 targetSdkVersion = 25 sourceCompatibilityVersion = JavaVersion.VERSION_1_8 targetCompatibilityVersion = JavaVersion.VERSION_1_8 - testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner" + testInstrumentationRunner = 'android.support.test.runner.AndroidJUnitRunner' + + deps = [ + + // Android support libraries + googlePlayServices: 'com.google.android.gms:play-services:10.0.1', + supportAnnotations: "com.android.support:support-annotations:${supportLibs}", - dep = [ // Others - rxjava : "io.reactivex:rxjava:1.2.3", - rxandroid : "io.reactivex:rxandroid:1.2.1", + rxjava : 'io.reactivex:rxjava:1.2.3', + rxandroid : 'io.reactivex:rxandroid:1.2.1', // Tests - junit : 'junit:junit:4.12', - testRunner: 'com.android.support.test:runner:0.5', - testRules : 'com.android.support.test:rules:0.5' + junit : 'junit:junit:4.12', + testRunner : 'com.android.support.test:runner:0.5', + testRules : 'com.android.support.test:rules:0.5', + mockito : 'org.mockito:mockito-core:2.5.5', + mockitoAndroid : 'org.mockito:mockito-android:2.7.0', + mockitoCore : 'org.mockito:mockito-core:2.7.0', + dexmaker : 'com.linkedin.dexmaker:dexmaker:2.2.0', + dexmakerMockito : 'com.linkedin.dexmaker:dexmaker-mockito:2.2.0', + assertj : 'org.assertj:assertj-core:1.7.1', + assertj3 : 'org.assertj:assertj-core:3.6.2', + robolectric : 'org.robolectric:robolectric:3.2.2', ] } \ No newline at end of file diff --git a/gradle/jacoco.gradle b/gradle/jacoco.gradle new file mode 100644 index 0000000..9d99660 --- /dev/null +++ b/gradle/jacoco.gradle @@ -0,0 +1,37 @@ +// https://github.com/nomisRev/AndroidGradleJacoco +apply plugin: 'jacoco' + +jacoco { + toolVersion = "0.7.8" +} + +android { + testOptions { + unitTests.all { + jacoco { + includeNoLocationClasses = true + } + } + } +} + +task jacocoTestReport(type: JacocoReport) { + group = "Reporting" + description = "Generate Jacoco coverage reports" + + jacocoClasspath = project.configurations['androidJacocoAnt'] + + def fileFilter = ['**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*', '**/*Test*.*', 'android/**/*.*', '**/*_.*'] + def debugTree = fileTree(dir: "${project.buildDir}/intermediates/classes/debug", excludes: fileFilter) + def mainSrc = ['${project.projectDir}/src/main/java'] + + sourceDirectories = files([mainSrc]) + additionalSourceDirs = files([mainSrc]) + classDirectories = files([debugTree]) + executionData = fileTree(dir: project.projectDir, includes: ['**/*.exec', '**/*.ec']) + + reports { + xml.enabled = true + html.enabled = true + } +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 88a907e..d59c016 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Sat Jun 11 15:36:11 JST 2016 +#Mon Dec 19 14:24:53 JST 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-3.1-bin.zip \ No newline at end of file +distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip diff --git a/library/build.gradle b/library/build.gradle index 9905ff0..9caefbe 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -16,9 +16,9 @@ * Contact email: alessandro@alessandro.jp */ apply plugin: 'com.android.library' -apply plugin: 'jacoco-android' apply from: rootProject.file('gradle/checkstyle.gradle') +apply from: rootProject.file('gradle/jacoco.gradle') android { compileSdkVersion rootProject.ext.compileSdkVersion @@ -28,6 +28,7 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion testInstrumentationRunner rootProject.ext.testInstrumentationRunner + multiDexEnabled true } buildTypes { @@ -52,15 +53,24 @@ android { } } } + + packagingOptions { +// exclude 'mockito-extensions/org.mockito.plugins.MockMaker' +// exclude 'mockito-extensions/org.mockito.plugins.StackTraceCleanerProvider' + } } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) // Android Testing - testCompile dep.junit - androidTestCompile dep.testRunner - androidTestCompile dep.testRules + testCompile deps.supportAnnotations + testCompile deps.mockito + testCompile deps.junit + testCompile deps.assertj3 + testCompile deps.robolectric + testCompile deps.testRunner + testCompile deps.testRules } android.libraryVariants.all { variant -> diff --git a/library/gradle.properties b/library/gradle.properties index ca17e02..19b5d61 100644 --- a/library/gradle.properties +++ b/library/gradle.properties @@ -15,7 +15,6 @@ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Contact email: alessandro@alessandro.jp # -VERSION_NAME=v1.0.0 POM_ARTIFACT_ID=easy-checkout POM_DESCRIPTION=Fast and easy checkout library (Android In-App Billing) for Android apps with RxJava support. POM_NAME=Easy Checkout Library (Android In-App Billing version 3 and 5) From 250302abcdd0464971551a68e053fb21d4b470ca Mon Sep 17 00:00:00 2001 From: Alessandro Yuichi Okimoto Date: Mon, 20 Feb 2017 15:59:57 +0900 Subject: [PATCH 3/4] Adding tests --- checkstyle.xml | 2 +- .../android/iab/ItemParcelableTest.java | 65 -- .../android/iab/PurchaseParcelableTest.java | 68 -- .../android/iab/BillingProcessor.java | 2 +- .../android/iab/BillingProcessorTest.java | 747 ++++++++++++++++++ .../jp/alessandro/android/iab/CancelTest.java | 143 ++++ .../android/iab/ConsumePurchaseTest.java | 435 ++++++++++ .../android/iab/ItemGetterTest.java | 289 +++++++ .../android/iab/ItemParcelableTest.java | 69 ++ .../android/iab/OnActivityResultTest.java | 291 +++++++ .../android/iab/PurchaseFlowLaunchTest.java | 328 ++++++++ .../iab/PurchaseFlowOnActivityResultTest.java | 316 ++++++++ .../android/iab/PurchaseGetterTest.java | 388 +++++++++ .../android/iab/PurchaseParcelableTest.java | 69 ++ .../alessandro/android/iab/ReleaseTest.java | 123 +++ .../alessandro/android/iab/ServiceTest.java | 174 ++++ .../android/iab/StartActivityTest.java | 429 ++++++++++ rsa/openssl_command.txt | 8 + rsa/private_key.pem | 27 + rsa/private_key.pk8 | Bin 0 -> 1216 bytes rsa/public_key.der | Bin 0 -> 294 bytes rsa/receipt.json | 1 + 22 files changed, 3839 insertions(+), 135 deletions(-) delete mode 100644 library/src/androidTest/java/jp/alessandro/android/iab/ItemParcelableTest.java delete mode 100644 library/src/androidTest/java/jp/alessandro/android/iab/PurchaseParcelableTest.java create mode 100644 library/src/test/java/jp/alessandro/android/iab/BillingProcessorTest.java create mode 100644 library/src/test/java/jp/alessandro/android/iab/CancelTest.java create mode 100644 library/src/test/java/jp/alessandro/android/iab/ConsumePurchaseTest.java create mode 100644 library/src/test/java/jp/alessandro/android/iab/ItemGetterTest.java create mode 100644 library/src/test/java/jp/alessandro/android/iab/ItemParcelableTest.java create mode 100644 library/src/test/java/jp/alessandro/android/iab/OnActivityResultTest.java create mode 100644 library/src/test/java/jp/alessandro/android/iab/PurchaseFlowLaunchTest.java create mode 100644 library/src/test/java/jp/alessandro/android/iab/PurchaseFlowOnActivityResultTest.java create mode 100644 library/src/test/java/jp/alessandro/android/iab/PurchaseGetterTest.java create mode 100644 library/src/test/java/jp/alessandro/android/iab/PurchaseParcelableTest.java create mode 100644 library/src/test/java/jp/alessandro/android/iab/ReleaseTest.java create mode 100644 library/src/test/java/jp/alessandro/android/iab/ServiceTest.java create mode 100644 library/src/test/java/jp/alessandro/android/iab/StartActivityTest.java create mode 100644 rsa/openssl_command.txt create mode 100644 rsa/private_key.pem create mode 100644 rsa/private_key.pk8 create mode 100644 rsa/public_key.der create mode 100644 rsa/receipt.json diff --git a/checkstyle.xml b/checkstyle.xml index fff2e21..7901458 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -58,7 +58,7 @@ - + diff --git a/library/src/androidTest/java/jp/alessandro/android/iab/ItemParcelableTest.java b/library/src/androidTest/java/jp/alessandro/android/iab/ItemParcelableTest.java deleted file mode 100644 index 3a14ae5..0000000 --- a/library/src/androidTest/java/jp/alessandro/android/iab/ItemParcelableTest.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (C) 2016 Alessandro Yuichi Okimoto - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - * Contact email: alessandro@alessandro.jp - */ - -package jp.alessandro.android.iab; - -import android.os.Parcel; -import android.support.test.runner.AndroidJUnit4; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; - -/** - * Created by Alessandro Yuichi Okimoto on 2016/07/23. - */ -@RunWith(AndroidJUnit4.class) -public class ItemParcelableTest { - - private final String mSkuDetailsJson = "{" + - "\"productId\": \"premium\"," + - "\"type\": \"subs\"," + - "\"price\": \"Â¥960\"," + - "\"price_amount_micros\": \"9600000\"," + - "\"price_currency_code\": \"JPY\"," + - "\"title\": \"Test Product\"," + - "\"description\": \"Fast and easy use Android In-App Billing\"}"; - - @Test - public void testParcelable() throws Exception { - Item item = Item.parseJson(mSkuDetailsJson); - - assertNotNull(item); - - Parcel parcel = Parcel.obtain(); - item.writeToParcel(parcel, 0); - parcel.setDataPosition(0); - Item fromParcel = Item.CREATOR.createFromParcel(parcel); - - assertEquals(item.getOriginalJson(), fromParcel.getOriginalJson()); - assertEquals(item.getSku(), fromParcel.getSku()); - assertEquals(item.getType(), fromParcel.getType()); - assertEquals(item.getTitle(), fromParcel.getTitle()); - assertEquals(item.getDescription(), fromParcel.getDescription()); - assertEquals(item.getCurrency(), fromParcel.getCurrency()); - assertEquals(item.getPrice(), fromParcel.getPrice()); - assertEquals(item.getPriceMicros(), fromParcel.getPriceMicros()); - } -} \ No newline at end of file diff --git a/library/src/androidTest/java/jp/alessandro/android/iab/PurchaseParcelableTest.java b/library/src/androidTest/java/jp/alessandro/android/iab/PurchaseParcelableTest.java deleted file mode 100644 index 2129827..0000000 --- a/library/src/androidTest/java/jp/alessandro/android/iab/PurchaseParcelableTest.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (C) 2016 Alessandro Yuichi Okimoto - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - * Contact email: alessandro@alessandro.jp - */ - -package jp.alessandro.android.iab; - -import android.os.Parcel; -import android.support.test.runner.AndroidJUnit4; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; - -/** - * Created by Alessandro Yuichi Okimoto on 2016/07/23. - */ -@RunWith(AndroidJUnit4.class) -public class PurchaseParcelableTest { - - private final String mPurchaseDataJson = "{" + - "\"orderId\": \"GPA.1111-2222-3333-44444\"," + - "\"packageName\": \"jp.alessandro.android.iab\"," + - "\"productId\": \"premium\"," + - "\"purchaseTime\": 1469232000," + - "\"purchaseState\": 0," + - "\"developerPayload\": \"developer_payload\"," + - "\"purchaseToken\": \"token\"," + - "\"autoRenewing\": true}"; - - @Test - public void testParcelable() throws Exception { - Purchase purchase = Purchase.parseJson(mPurchaseDataJson, "signature"); - - assertNotNull(purchase); - - Parcel parcel = Parcel.obtain(); - purchase.writeToParcel(parcel, 0); - parcel.setDataPosition(0); - Purchase fromParcel = Purchase.CREATOR.createFromParcel(parcel); - - assertEquals(purchase.getOriginalJson(), fromParcel.getOriginalJson()); - assertEquals(purchase.getOrderId(), fromParcel.getOrderId()); - assertEquals(purchase.getPackageName(), fromParcel.getPackageName()); - assertEquals(purchase.getSku(), fromParcel.getSku()); - assertEquals(purchase.getPurchaseTime(), fromParcel.getPurchaseTime()); - assertEquals(purchase.getPurchaseState(), fromParcel.getPurchaseState()); - assertEquals(purchase.getDeveloperPayload(), fromParcel.getDeveloperPayload()); - assertEquals(purchase.getToken(), fromParcel.getToken()); - assertEquals(purchase.isAutoRenewing(), fromParcel.isAutoRenewing()); - assertEquals(purchase.getSignature(), fromParcel.getSignature()); - } -} \ No newline at end of file diff --git a/library/src/main/java/jp/alessandro/android/iab/BillingProcessor.java b/library/src/main/java/jp/alessandro/android/iab/BillingProcessor.java index f4cb8f4..86db2cb 100644 --- a/library/src/main/java/jp/alessandro/android/iab/BillingProcessor.java +++ b/library/src/main/java/jp/alessandro/android/iab/BillingProcessor.java @@ -48,6 +48,7 @@ public class BillingProcessor { protected static final String WORK_THREAD_NAME = "AndroidEasyCheckoutThread"; + protected Handler mWorkHandler; private final BillingContext mContext; private final SparseArray mPurchaseFlows; @@ -55,7 +56,6 @@ public class BillingProcessor { private final Intent mServiceIntent; private PurchaseHandler mPurchaseHandler; - private Handler mWorkHandler; private Handler mMainHandler; private boolean mIsReleased; diff --git a/library/src/test/java/jp/alessandro/android/iab/BillingProcessorTest.java b/library/src/test/java/jp/alessandro/android/iab/BillingProcessorTest.java new file mode 100644 index 0000000..f650204 --- /dev/null +++ b/library/src/test/java/jp/alessandro/android/iab/BillingProcessorTest.java @@ -0,0 +1,747 @@ +/* + * Copyright (C) 2016 Alessandro Yuichi Okimoto + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * Contact email: alessandro@alessandro.jp + */ + +package jp.alessandro.android.iab; + +import android.app.Activity; +import android.app.PendingIntent; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.RemoteException; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.mockito.stubbing.Answer; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import jp.alessandro.android.iab.handler.ConsumeItemHandler; +import jp.alessandro.android.iab.handler.InventoryHandler; +import jp.alessandro.android.iab.handler.ItemDetailsHandler; +import jp.alessandro.android.iab.handler.PurchasesHandler; +import jp.alessandro.android.iab.handler.StartActivityHandler; + +import static org.assertj.core.api.Java6Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.robolectric.Shadows.shadowOf; + +/** + * Created by Alessandro Yuichi Okimoto on 2017/02/19. + */ + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE, constants = BuildConfig.class) +public class BillingProcessorTest { + + private Handler mWorkHandler; + private BillingProcessor mProcessor; + + private final BillingContext mContext = Util.newBillingContext(RuntimeEnvironment.application); + + @Rule + public MockitoRule mMockitoRule = MockitoJUnit.rule(); + + @Mock + BillingService mService; + @Mock + ServiceBinder mServiceBinder; + @Mock + Activity mActivity; + + @Before + public void setUp() { + HandlerThread thread = new HandlerThread(BillingProcessor.WORK_THREAD_NAME); + thread.start(); + // Handler to post all actions in the library + mWorkHandler = new Handler(thread.getLooper()); + + mProcessor = spy(new BillingProcessor(mContext, null)); + } + + @Test + public void isServiceAvailable() { + assertThat(mProcessor.isServiceAvailable(mContext.getContext())).isFalse(); + } + + @Test + public void onActivityResult() throws InterruptedException, RemoteException { + final CountDownLatch latch = new CountDownLatch(1); + final int requestCode = 1001; + PendingIntent pendingIntent = PendingIntent.getActivity(mContext.getContext(), 1, new Intent(), 0); + Bundle bundle = new Bundle(); + bundle.putLong(Constants.RESPONSE_CODE, 0L); + bundle.putParcelable(Constants.RESPONSE_BUY_INTENT, pendingIntent); + PurchaseType type = PurchaseType.IN_APP; + + setUpStartPurchase(bundle, type); + mProcessor.startPurchase( + mActivity, + requestCode, + Constants.TEST_PRODUCT_ID, + type, + Constants.TEST_DEVELOPER_PAYLOAD, + new StartActivityHandler() { + @Override + public void onSuccess() { + assertThat(mProcessor.onActivityResult(requestCode, -1, Util.newOkIntent())).isTrue(); + latch.countDown(); + } + + @Override + public void onError(BillingException e) { + throw new IllegalStateException(); + } + }); + latch.await(15, TimeUnit.SECONDS); + } + + @Test + public void onActivityResultError() { + assertThat(mProcessor.onActivityResult(0, 0, null)).isFalse(); + } + + @Test + public void startPurchaseInApp() throws InterruptedException, RemoteException { + final CountDownLatch latch = new CountDownLatch(1); + int requestCode = 1001; + PendingIntent pendingIntent = PendingIntent.getActivity(mContext.getContext(), 1, new Intent(), 0); + Bundle bundle = new Bundle(); + bundle.putLong(Constants.RESPONSE_CODE, 0L); + bundle.putParcelable(Constants.RESPONSE_BUY_INTENT, pendingIntent); + PurchaseType type = PurchaseType.IN_APP; + + startActivity(latch, bundle, requestCode, type); + + latch.await(15, TimeUnit.SECONDS); + } + + @Test + public void startPurchaseInAppError() throws InterruptedException, RemoteException { + final CountDownLatch latch = new CountDownLatch(1); + int requestCode = 1001; + Bundle bundle = new Bundle(); + bundle.putLong(Constants.RESPONSE_CODE, 0L); + PurchaseType type = PurchaseType.IN_APP; + + startActivityError(latch, bundle, requestCode, type); + + latch.await(15, TimeUnit.SECONDS); + } + + @Test + public void updateSubscription() throws InterruptedException, RemoteException { + final CountDownLatch latch = new CountDownLatch(1); + int requestCode = 1001; + PendingIntent pendingIntent = PendingIntent.getActivity(mContext.getContext(), 1, new Intent(), 0); + List oldItemIds = new ArrayList<>(); + oldItemIds.add(Constants.TEST_PRODUCT_ID); + Bundle bundle = new Bundle(); + bundle.putLong(Constants.RESPONSE_CODE, 0L); + bundle.putParcelable(Constants.RESPONSE_BUY_INTENT, pendingIntent); + + setUpUpdateSubscriptionPurchase(bundle, oldItemIds); + mProcessor.updateSubscription( + mActivity, + requestCode, + oldItemIds, + Constants.TEST_PRODUCT_ID, + Constants.TEST_DEVELOPER_PAYLOAD, + new StartActivityHandler() { + @Override + public void onSuccess() { + latch.countDown(); + } + + @Override + public void onError(BillingException e) { + throw new IllegalStateException(); + } + }); + latch.await(15, TimeUnit.SECONDS); + } + + @Test + public void updateSubscriptionError() throws InterruptedException, RemoteException { + final CountDownLatch latch = new CountDownLatch(1); + int requestCode = 1001; + List oldItemIds = new ArrayList<>(); + oldItemIds.add(Constants.TEST_PRODUCT_ID); + Bundle bundle = new Bundle(); + bundle.putLong(Constants.RESPONSE_CODE, 0L); + + setUpUpdateSubscriptionPurchase(bundle, oldItemIds); + mProcessor.updateSubscription( + mActivity, + requestCode, + oldItemIds, + Constants.TEST_PRODUCT_ID, + Constants.TEST_DEVELOPER_PAYLOAD, + new StartActivityHandler() { + @Override + public void onSuccess() { + throw new IllegalStateException(); + } + + @Override + public void onError(BillingException e) { + assertThat(e.getErrorCode()).isEqualTo(Constants.ERROR_PENDING_INTENT); + assertThat(e.getMessage()).isEqualTo(Constants.ERROR_MSG_PENDING_INTENT); + latch.countDown(); + } + }); + latch.await(15, TimeUnit.SECONDS); + } + + @Test + public void updateSubscriptionListEmpty() { + List oldItemIds = new ArrayList<>(); + try { + mProcessor.updateSubscription(null, 0, oldItemIds, null, null, null); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage()).isEqualTo(Constants.ERROR_MSG_UPDATE_ARGUMENT_MISSING); + } + } + + @Test + public void updateSubscriptionListNull() { + try { + mProcessor.updateSubscription(null, 0, null, null, null, null); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage()).isEqualTo(Constants.ERROR_MSG_UPDATE_ARGUMENT_MISSING); + } + } + + @Test + public void startPurchaseSubscription() throws InterruptedException, RemoteException { + final CountDownLatch latch = new CountDownLatch(1); + int requestCode = 1001; + PendingIntent pendingIntent = PendingIntent.getActivity(mContext.getContext(), 1, new Intent(), 0); + Bundle bundle = new Bundle(); + bundle.putLong(Constants.RESPONSE_CODE, 0L); + bundle.putParcelable(Constants.RESPONSE_BUY_INTENT, pendingIntent); + PurchaseType type = PurchaseType.SUBSCRIPTION; + + startActivity(latch, bundle, requestCode, type); + + latch.await(15, TimeUnit.SECONDS); + } + + @Test + public void startPurchaseSubscriptionError() throws InterruptedException, RemoteException { + final CountDownLatch latch = new CountDownLatch(1); + int requestCode = 1001; + Bundle bundle = new Bundle(); + bundle.putLong(Constants.RESPONSE_CODE, 0L); + PurchaseType type = PurchaseType.SUBSCRIPTION; + + startActivityError(latch, bundle, requestCode, type); + + latch.await(15, TimeUnit.SECONDS); + } + + @Test + public void releaseAndGetPurchases() { + mProcessor.release(); + try { + mProcessor.getPurchases(PurchaseType.IN_APP, null); + } catch (IllegalStateException e) { + assertThat(e.getMessage()).isEqualTo(Constants.ERROR_MSG_LIBRARY_ALREADY_RELEASED); + } + } + + @Test + public void getPurchasesAndRelease() throws InterruptedException, RemoteException { + final CountDownLatch latch = new CountDownLatch(1); + final int size = 10; + Bundle responseBundle = Util.createPurchaseBundle(0, 0, size, null); + + doReturn(responseBundle).when(mService).getPurchases( + mContext.getApiVersion(), mContext.getContext().getPackageName(), Constants.ITEM_TYPE_SUBSCRIPTION, null); + + setUpProcessor(PurchaseType.SUBSCRIPTION); + mProcessor.getPurchases(PurchaseType.SUBSCRIPTION, new PurchasesHandler() { + @Override + public void onSuccess(Purchases purchases) { + mProcessor.release(); + try { + mProcessor.getPurchases(PurchaseType.SUBSCRIPTION, null); + } catch (IllegalStateException e) { + assertThat(e.getMessage()).isEqualTo(Constants.ERROR_MSG_LIBRARY_ALREADY_RELEASED); + } finally { + latch.countDown(); + } + } + + @Override + public void onError(BillingException e) { + throw new IllegalStateException(); + } + }); + shadowOf(mWorkHandler.getLooper()).getScheduler().advanceToNextPostedRunnable(); + + latch.await(15, TimeUnit.SECONDS); + } + + @Test + public void getInAppPurchases() throws InterruptedException, RemoteException { + getPurchases(PurchaseType.IN_APP); + } + + @Test + public void getSubscriptionPurchases() throws InterruptedException, RemoteException { + getPurchases(PurchaseType.SUBSCRIPTION); + } + + private void getPurchases(PurchaseType type) throws InterruptedException, RemoteException { + final CountDownLatch latch = new CountDownLatch(1); + final int size = 10; + Bundle responseBundle = Util.createPurchaseBundle(0, 0, size, null); + + doReturn(responseBundle).when(mService).getPurchases( + mContext.getApiVersion(), + mContext.getContext().getPackageName(), + type == PurchaseType.SUBSCRIPTION ? Constants.ITEM_TYPE_SUBSCRIPTION : Constants.TYPE_IN_APP, + null); + + setUpProcessor(type); + getPurchases(type, latch, size); + + latch.await(15, TimeUnit.SECONDS); + } + + @Test + public void getInAppPurchasesError() throws InterruptedException, RemoteException { + getPurchasesError(PurchaseType.IN_APP); + } + + @Test + public void getSubscriptionPurchasesError() throws InterruptedException, RemoteException { + getPurchasesError(PurchaseType.SUBSCRIPTION); + } + + private void getPurchasesError(PurchaseType type) throws InterruptedException, RemoteException { + final CountDownLatch latch = new CountDownLatch(1); + + Bundle responseBundle = new Bundle(); + responseBundle.putInt(Constants.RESPONSE_CODE, 0); + + doReturn(responseBundle).when(mService).getPurchases( + mContext.getApiVersion(), + mContext.getContext().getPackageName(), + type == PurchaseType.SUBSCRIPTION ? Constants.ITEM_TYPE_SUBSCRIPTION : Constants.TYPE_IN_APP, + null); + + setUpProcessor(type); + getPurchasesError(type, latch); + + latch.await(15, TimeUnit.SECONDS); + } + + @Test + @Deprecated + public void getInventorySubscription() throws InterruptedException, RemoteException { + getInventory(PurchaseType.SUBSCRIPTION); + } + + @Test + @Deprecated + public void getInventoryInApp() throws InterruptedException, RemoteException { + getInventory(PurchaseType.IN_APP); + } + + @Test + public void getInAppItemDetails() throws InterruptedException, RemoteException { + getItemDetails(PurchaseType.IN_APP); + } + + @Test + public void getSubscriptionItemDetails() throws InterruptedException, RemoteException { + getItemDetails(PurchaseType.SUBSCRIPTION); + } + + private void getItemDetails(PurchaseType purchaseType) throws InterruptedException, RemoteException { + final CountDownLatch latch = new CountDownLatch(1); + + ArrayList itemIds = new ArrayList<>(); + Bundle requestBundle = new Bundle(); + requestBundle.putStringArrayList(Constants.RESPONSE_ITEM_ID_LIST, itemIds); + + final int size = 10; + ArrayList items = Util.createSkuItemDetailsJsonArray(size); + Bundle responseBundle = new Bundle(); + responseBundle.putLong(Constants.RESPONSE_CODE, 0L); + responseBundle.putStringArrayList(Constants.RESPONSE_DETAILS_LIST, items); + + doReturn(requestBundle).when(mProcessor).createBundleItemListFromArray(itemIds); + doReturn(responseBundle).when(mService).getSkuDetails( + mContext.getApiVersion(), + mContext.getContext().getPackageName(), + purchaseType == PurchaseType.IN_APP ? Constants.ITEM_TYPE_INAPP : Constants.ITEM_TYPE_SUBSCRIPTION, + requestBundle + ); + setUpProcessor(purchaseType); + getItemDetails(purchaseType, latch, itemIds, size); + + latch.await(15, TimeUnit.SECONDS); + } + + @Test + public void getInAppItemDetailsError() throws InterruptedException, RemoteException { + getItemDetailsError(PurchaseType.IN_APP); + } + + @Test + public void getSubscriptionItemDetailsError() throws InterruptedException, RemoteException { + getItemDetailsError(PurchaseType.IN_APP); + } + + private void getItemDetailsError(PurchaseType purchaseType) throws InterruptedException, RemoteException { + final CountDownLatch latch = new CountDownLatch(1); + + ArrayList itemIds = new ArrayList<>(); + Bundle requestBundle = new Bundle(); + requestBundle.putStringArrayList(Constants.RESPONSE_ITEM_ID_LIST, itemIds); + + Bundle responseBundle = new Bundle(); + responseBundle.putLong(Constants.RESPONSE_CODE, 0L); + + doReturn(requestBundle).when(mProcessor).createBundleItemListFromArray(itemIds); + doReturn(responseBundle).when(mService).getSkuDetails( + mContext.getApiVersion(), + mContext.getContext().getPackageName(), + purchaseType == PurchaseType.IN_APP ? Constants.ITEM_TYPE_INAPP : Constants.ITEM_TYPE_SUBSCRIPTION, + requestBundle + ); + setUpProcessor(PurchaseType.IN_APP); + getItemDetailsError(PurchaseType.IN_APP, latch, itemIds); + + latch.await(15, TimeUnit.SECONDS); + } + + @Test + public void consume() throws InterruptedException, RemoteException, BillingException { + final CountDownLatch latch = new CountDownLatch(1); + final String purchaseToken = "purchase_token"; + + Bundle responseBundle = new Bundle(); + responseBundle.putInt(Constants.RESPONSE_CODE, 0); + + doReturn(0).when(mService).consumePurchase( + mContext.getApiVersion(), mContext.getContext().getPackageName(), purchaseToken); + + doReturn(purchaseToken).when(mProcessor).getToken(mService, Constants.TEST_PRODUCT_ID); + + setUpProcessor(PurchaseType.IN_APP); + mProcessor.consume(Constants.TEST_PRODUCT_ID, new ConsumeItemHandler() { + @Override + public void onSuccess() { + try { + verify(mService).consumePurchase( + mContext.getApiVersion(), mContext.getContext().getPackageName(), purchaseToken); + } catch (RemoteException e2) { + } finally { + verifyNoMoreInteractions(mService); + latch.countDown(); + } + } + + @Override + public void onError(BillingException e) { + throw new IllegalStateException(); + } + }); + shadowOf(mWorkHandler.getLooper()).getScheduler().advanceToNextPostedRunnable(); + + latch.await(15, TimeUnit.SECONDS); + } + + @Test + public void consumePurchase() throws InterruptedException, RemoteException, BillingException { + final CountDownLatch latch = new CountDownLatch(1); + final String purchaseToken = "purchase_token"; + + Bundle responseBundle = new Bundle(); + responseBundle.putInt(Constants.RESPONSE_CODE, 0); + + doReturn(0).when(mService).consumePurchase( + mContext.getApiVersion(), mContext.getContext().getPackageName(), purchaseToken); + + doReturn(purchaseToken).when(mProcessor).getToken(mService, Constants.TEST_PRODUCT_ID); + + setUpProcessor(PurchaseType.IN_APP); + mProcessor.consumePurchase(Constants.TEST_PRODUCT_ID, new ConsumeItemHandler() { + @Override + public void onSuccess() { + try { + verify(mService).consumePurchase( + mContext.getApiVersion(), mContext.getContext().getPackageName(), purchaseToken); + } catch (RemoteException e2) { + } finally { + verifyNoMoreInteractions(mService); + latch.countDown(); + } + } + + @Override + public void onError(BillingException e) { + throw new IllegalStateException(); + } + }); + shadowOf(mWorkHandler.getLooper()).getScheduler().advanceToNextPostedRunnable(); + + latch.await(15, TimeUnit.SECONDS); + } + + @Test + public void getWorkHandler() { + Handler handler = mProcessor.getWorkHandler(); + assertThat(handler.getLooper().getThread().getName()).isEqualTo(BillingProcessor.WORK_THREAD_NAME); + } + + @Test + public void createServiceBinder() { + ServiceBinder binder = mProcessor.createServiceBinder(); + assertThat(binder).isNotNull(); + } + + private void startActivity(final CountDownLatch latch, + Bundle bundle, + int requestCode, + PurchaseType type) throws RemoteException { + setUpStartPurchase(bundle, type); + mProcessor.startPurchase( + mActivity, + requestCode, + Constants.TEST_PRODUCT_ID, + type, + Constants.TEST_DEVELOPER_PAYLOAD, + new StartActivityHandler() { + @Override + public void onSuccess() { + latch.countDown(); + } + + @Override + public void onError(BillingException e) { + throw new IllegalStateException(); + } + }); + } + + private void startActivityError(final CountDownLatch latch, + Bundle bundle, + int requestCode, + PurchaseType type) throws RemoteException { + setUpStartPurchase(bundle, type); + mProcessor.startPurchase( + mActivity, + requestCode, + Constants.TEST_PRODUCT_ID, + type, + Constants.TEST_DEVELOPER_PAYLOAD, + new StartActivityHandler() { + @Override + public void onSuccess() { + throw new IllegalStateException(); + } + + @Override + public void onError(BillingException e) { + assertThat(e.getErrorCode()).isEqualTo(Constants.ERROR_PENDING_INTENT); + assertThat(e.getMessage()).isEqualTo(Constants.ERROR_MSG_PENDING_INTENT); + latch.countDown(); + } + }); + } + + private void setUpStartPurchase(Bundle bundle, PurchaseType type) throws RemoteException { + setUpProcessor(type); + Mockito.when(mService.getBuyIntent( + mContext.getApiVersion(), + mContext.getContext().getPackageName(), + Constants.TEST_PRODUCT_ID, + type == PurchaseType.SUBSCRIPTION ? Constants.TYPE_SUBSCRIPTION : Constants.TYPE_IN_APP, + Constants.TEST_DEVELOPER_PAYLOAD + )).thenReturn(bundle); + } + + private void setUpUpdateSubscriptionPurchase(Bundle bundle, List oldItems) throws RemoteException { + setUpProcessor(PurchaseType.SUBSCRIPTION); + Mockito.when(mService.getBuyIntentToReplaceSkus( + BillingApi.VERSION_5.getValue(), + mContext.getContext().getPackageName(), + oldItems, + Constants.TEST_PRODUCT_ID, + Constants.TYPE_SUBSCRIPTION, + Constants.TEST_DEVELOPER_PAYLOAD + )).thenReturn(bundle); + } + + private void getPurchases(final PurchaseType type, final CountDownLatch latch, final int size) { + mProcessor.getPurchases(type, new PurchasesHandler() { + @Override + public void onSuccess(Purchases purchases) { + assertThat(purchases).isNotNull(); + assertThat(purchases.getSize()).isEqualTo(size); + assertThat(purchases.getAll()).isNotNull(); + + List purchaseList = purchases.getAll(); + for (Purchase p : purchaseList) { + assertThat(purchases.hasItemId(p.getSku())).isTrue(); + assertThat(purchases.getByPurchaseId(p.getSku())).isNotNull(); + } + mProcessor.release(); + latch.countDown(); + } + + @Override + public void onError(BillingException e) { + throw new IllegalStateException(); + } + }); + shadowOf(mWorkHandler.getLooper()).getScheduler().advanceToNextPostedRunnable(); + } + + private void getPurchasesError(PurchaseType type, final CountDownLatch latch) { + mProcessor.getPurchases(type, new PurchasesHandler() { + @Override + public void onSuccess(Purchases purchases) { + throw new IllegalStateException(); + } + + @Override + public void onError(BillingException e) { + assertThat(e).isNotNull(); + assertThat(e.getErrorCode()).isEqualTo(Constants.ERROR_PURCHASE_DATA); + assertThat(e.getMessage()).isEqualTo(Constants.ERROR_MSG_GET_PURCHASES_DATA_LIST); + mProcessor.release(); + latch.countDown(); + } + }); + shadowOf(mWorkHandler.getLooper()).getScheduler().advanceToNextPostedRunnable(); + } + + private void getItemDetails(PurchaseType type, final CountDownLatch latch, final ArrayList itemIds, final int size) { + mProcessor.getItemDetails(type, itemIds, new ItemDetailsHandler() { + @Override + public void onSuccess(ItemDetails itemDetails) { + assertThat(itemDetails.getSize()).isEqualTo(size); + assertThat(itemDetails.getAll()).isNotNull(); + + List purchaseList = itemDetails.getAll(); + for (Item item : purchaseList) { + assertThat(itemDetails.hasItemId(item.getSku())).isTrue(); + assertThat(itemDetails.getByItemId(item.getSku())).isNotNull(); + } + mProcessor.release(); + latch.countDown(); + } + + @Override + public void onError(BillingException e) { + throw new IllegalStateException(); + } + }); + shadowOf(mWorkHandler.getLooper()).getScheduler().advanceToNextPostedRunnable(); + } + + private void getItemDetailsError(PurchaseType type, final CountDownLatch latch, final ArrayList itemIds) { + mProcessor.getItemDetails(type, itemIds, new ItemDetailsHandler() { + @Override + public void onSuccess(ItemDetails itemDetails) { + throw new IllegalStateException(); + } + + @Override + public void onError(BillingException e) { + assertThat(e).isNotNull(); + assertThat(e.getErrorCode()).isEqualTo(Constants.ERROR_UNEXPECTED_TYPE); + assertThat(e.getMessage()).isEqualTo(Constants.ERROR_MSG_GET_SKU_DETAILS_RESPONSE_LIST_NULL); + mProcessor.release(); + latch.countDown(); + } + }); + shadowOf(mWorkHandler.getLooper()).getScheduler().advanceToNextPostedRunnable(); + } + + private void getInventory(final PurchaseType type) throws InterruptedException, RemoteException { + final CountDownLatch latch = new CountDownLatch(1); + final int size = 10; + Bundle responseBundle = Util.createPurchaseBundle(0, 0, size, null); + String purchaseType = type == PurchaseType.SUBSCRIPTION ? Constants.ITEM_TYPE_SUBSCRIPTION : Constants.ITEM_TYPE_INAPP; + doReturn(responseBundle).when(mService).getPurchases( + mContext.getApiVersion(), mContext.getContext().getPackageName(), purchaseType, null); + + setUpProcessor(type); + mProcessor.getInventory(type, new InventoryHandler() { + @Override + public void onSuccess(Purchases purchases) { + assertThat(purchases).isNotNull(); + assertThat(purchases.getSize()).isEqualTo(size); + assertThat(purchases.getAll()).isNotNull(); + + List purchaseList = purchases.getAll(); + for (Purchase p : purchaseList) { + assertThat(purchases.hasItemId(p.getSku())).isTrue(); + assertThat(purchases.getByPurchaseId(p.getSku())).isNotNull(); + } + latch.countDown(); + } + + @Override + public void onError(BillingException e) { + throw new IllegalStateException(); + } + }); + shadowOf(mWorkHandler.getLooper()).getScheduler().advanceToNextPostedRunnable(); + } + + private void setUpProcessor(PurchaseType purchaseType) { + doReturn(mWorkHandler).when(mProcessor).getWorkHandler(); + doReturn(true).when(mProcessor).isSupported(purchaseType, mService); + doReturn(mServiceBinder).when(mProcessor).createServiceBinder(); + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + ServiceBinder.Handler handler = invocation.getArgument(0); + handler.onBind(mService); + return null; + } + }).when(mServiceBinder).getServiceAsync(any(ServiceBinder.Handler.class)); + } +} \ No newline at end of file diff --git a/library/src/test/java/jp/alessandro/android/iab/CancelTest.java b/library/src/test/java/jp/alessandro/android/iab/CancelTest.java new file mode 100644 index 0000000..3472b5b --- /dev/null +++ b/library/src/test/java/jp/alessandro/android/iab/CancelTest.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2016 Alessandro Yuichi Okimoto + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * Contact email: alessandro@alessandro.jp + */ + +package jp.alessandro.android.iab; + +import android.os.Bundle; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.RemoteException; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.mockito.stubbing.Answer; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.Shadows; +import org.robolectric.annotation.Config; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import jp.alessandro.android.iab.handler.PurchasesHandler; + +import static org.assertj.core.api.Java6Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; + +/** + * Created by Alessandro Yuichi Okimoto on 2017/02/19. + */ + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE, constants = BuildConfig.class) +public class CancelTest { + + @Rule + public MockitoRule mMockitoRule = MockitoJUnit.rule(); + + @Mock + BillingService mService; + @Mock + ServiceBinder mServiceBinder; + + private final BillingContext mContext = Util.newBillingContext(RuntimeEnvironment.application); + + private BillingProcessor mProcessor; + private Handler mWorkHandler; + + @Before + public void setUp() { + HandlerThread thread = new HandlerThread("AndroidIabThread"); + thread.start(); + // Handler to post all actions in the library + mWorkHandler = new Handler(thread.getLooper()); + + mProcessor = spy(new BillingProcessor(mContext, null)); + mProcessor.mWorkHandler = mWorkHandler; + + doReturn(mWorkHandler).when(mProcessor).getWorkHandler(); + doReturn(true).when(mProcessor).isSupported(PurchaseType.IN_APP, mService); + doReturn(mServiceBinder).when(mProcessor).createServiceBinder(); + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + ServiceBinder.Handler handler = invocation.getArgument(0); + handler.onBind(mService); + return null; + } + }).when(mServiceBinder).getServiceAsync(any(ServiceBinder.Handler.class)); + } + + @Test + public void getPurchasesAndCancel() throws InterruptedException, RemoteException { + CountDownLatch latch = new CountDownLatch(1); + Bundle responseBundle = Util.createPurchaseBundle(0, 0, 10, null); + + doReturn(responseBundle).when(mService).getPurchases( + mContext.getApiVersion(), mContext.getContext().getPackageName(), Constants.ITEM_TYPE_INAPP, null); + + getPurchasesAndCancel(latch, new AtomicInteger(10)); + latch.await(15, TimeUnit.SECONDS); + } + + @Test + public void cancelButAlreadyReleased() throws InterruptedException, RemoteException { + mProcessor.release(); + mProcessor.cancel(); + try { + mProcessor.getPurchases(PurchaseType.IN_APP, null); + } catch (IllegalStateException e) { + assertThat(e.getMessage()).isEqualTo(Constants.ERROR_MSG_LIBRARY_ALREADY_RELEASED); + } + } + + private void getPurchasesAndCancel(final CountDownLatch latch, final AtomicInteger times) throws InterruptedException { + mProcessor.getPurchases(PurchaseType.IN_APP, new PurchasesHandler() { + @Override + public void onSuccess(Purchases purchases) { + if (times.getAndDecrement() < 1) { + assertThat(purchases.getAll()).isNotEmpty(); + latch.countDown(); + return; + } + mProcessor.cancel(); + try { + getPurchasesAndCancel(latch, times); + } catch (InterruptedException e) { + throw new IllegalStateException(e); + } + } + + @Override + public void onError(BillingException e) { + throw new IllegalStateException(); + } + }); + Shadows.shadowOf(mWorkHandler.getLooper()).getScheduler().advanceToNextPostedRunnable(); + } +} \ No newline at end of file diff --git a/library/src/test/java/jp/alessandro/android/iab/ConsumePurchaseTest.java b/library/src/test/java/jp/alessandro/android/iab/ConsumePurchaseTest.java new file mode 100644 index 0000000..4c13660 --- /dev/null +++ b/library/src/test/java/jp/alessandro/android/iab/ConsumePurchaseTest.java @@ -0,0 +1,435 @@ +/* + * Copyright (C) 2016 Alessandro Yuichi Okimoto + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * Contact email: alessandro@alessandro.jp + */ + +package jp.alessandro.android.iab; + +import android.os.Bundle; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.RemoteException; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.mockito.stubbing.Answer; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.Shadows; +import org.robolectric.annotation.Config; + +import java.util.Locale; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import jp.alessandro.android.iab.handler.ConsumeItemHandler; + +import static org.assertj.core.api.Java6Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +/** + * Created by Alessandro Yuichi Okimoto on 2017/02/19. + */ + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE, constants = BuildConfig.class) +public class ConsumePurchaseTest { + + private Handler mWorkHandler; + private BillingProcessor mProcessor; + + private final BillingContext mContext = Util.newBillingContext(RuntimeEnvironment.application); + + @Rule + public MockitoRule mMockitoRule = MockitoJUnit.rule(); + + @Mock + BillingService mService; + @Mock + ServiceBinder mServiceBinder; + + @Before + public void setUp() { + HandlerThread thread = new HandlerThread("AndroidIabThread"); + thread.start(); + // Handler to post all actions in the library + mWorkHandler = new Handler(thread.getLooper()); + + mProcessor = spy(new BillingProcessor(mContext, null)); + + doReturn(mWorkHandler).when(mProcessor).getWorkHandler(); + } + + @Test + public void consumePurchaseSuccess() throws InterruptedException, RemoteException, BillingException { + final CountDownLatch latch = new CountDownLatch(1); + final int responseCode = 0; + PurchaseGetter getter = spy(new PurchaseGetter(mContext)); + Bundle responseBundle = Util.createPurchaseBundle(0, 0, 10, null); + + doReturn(responseCode).when(mService).consumePurchase( + mContext.getApiVersion(), mContext.getContext().getPackageName(), Constants.TEST_PURCHASE_TOKEN); + + doReturn(true).when(mProcessor).isSupported(PurchaseType.IN_APP, mService); + doReturn(getter).when(mProcessor).createPurchaseGetter(); + doReturn(responseBundle).when(mService).getPurchases( + mContext.getApiVersion(), mContext.getContext().getPackageName(), Constants.ITEM_TYPE_INAPP, null); + + doReturn(mServiceBinder).when(mProcessor).createServiceBinder(); + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + ServiceBinder.Handler handler = invocation.getArgument(0); + handler.onBind(mService); + return null; + } + }).when(mServiceBinder).getServiceAsync(any(ServiceBinder.Handler.class)); + + mProcessor.consume(String.format(Locale.US, "%s_%d", Constants.TEST_PRODUCT_ID, 0), new ConsumeItemHandler() { + @Override + public void onSuccess() { + latch.countDown(); + } + + @Override + public void onError(BillingException e) { + throw new IllegalStateException(); + } + }); + Shadows.shadowOf(mWorkHandler.getLooper()).getScheduler().advanceToNextPostedRunnable(); + + latch.await(15, TimeUnit.SECONDS); + } + + @Test + @SuppressWarnings("checkstyle:methodlength") + public void consumeError() throws InterruptedException, RemoteException, BillingException { + final CountDownLatch latch = new CountDownLatch(1); + final int responseCode = 3; + + doReturn(responseCode).when(mService).consumePurchase( + mContext.getApiVersion(), mContext.getContext().getPackageName(), Constants.TEST_PURCHASE_TOKEN); + + doReturn(true).when(mProcessor).isSupported(PurchaseType.IN_APP, mService); + doReturn(mServiceBinder).when(mProcessor).createServiceBinder(); + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + ServiceBinder.Handler handler = invocation.getArgument(0); + handler.onBind(mService); + return null; + } + }).when(mServiceBinder).getServiceAsync(any(ServiceBinder.Handler.class)); + doReturn(Constants.TEST_PURCHASE_TOKEN).when(mProcessor).getToken(mService, Constants.TEST_PRODUCT_ID); + + mProcessor.consume(Constants.TEST_PRODUCT_ID, new ConsumeItemHandler() { + @Override + public void onSuccess() { + throw new IllegalStateException(); + } + + @Override + public void onError(BillingException e) { + assertThat(e.getErrorCode()).isEqualTo(responseCode); + assertThat(e.getMessage()).isEqualTo(Constants.ERROR_MSG_CONSUME); + try { + verify(mService).consumePurchase( + mContext.getApiVersion(), mContext.getContext().getPackageName(), Constants.TEST_PURCHASE_TOKEN); + } catch (RemoteException e2) { + } + verifyNoMoreInteractions(mService); + latch.countDown(); + } + }); + Shadows.shadowOf(mWorkHandler.getLooper()).getScheduler().advanceToNextPostedRunnable(); + + latch.await(15, TimeUnit.SECONDS); + } + + @Test + public void consumeWithResponseCodeNull() throws InterruptedException, RemoteException, BillingException { + final CountDownLatch latch = new CountDownLatch(1); + final int responseCode = 3; + + doReturn(responseCode).when(mService).consumePurchase( + mContext.getApiVersion(), mContext.getContext().getPackageName(), Constants.TEST_PURCHASE_TOKEN); + + doReturn(true).when(mProcessor).isSupported(PurchaseType.IN_APP, mService); + doReturn(mServiceBinder).when(mProcessor).createServiceBinder(); + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + ServiceBinder.Handler handler = invocation.getArgument(0); + handler.onBind(mService); + return null; + } + }).when(mServiceBinder).getServiceAsync(any(ServiceBinder.Handler.class)); + + mProcessor.consume(Constants.TEST_PRODUCT_ID, new ConsumeItemHandler() { + @Override + public void onSuccess() { + throw new IllegalStateException(); + } + + @Override + public void onError(BillingException e) { + assertThat(e.getErrorCode()).isEqualTo(Constants.ERROR_UNEXPECTED_TYPE); + assertThat(e.getMessage()).isEqualTo(Constants.ERROR_MSG_UNEXPECTED_BUNDLE_RESPONSE_NULL); + try { + verify(mService, never()).consumePurchase( + mContext.getApiVersion(), mContext.getContext().getPackageName(), Constants.TEST_PURCHASE_TOKEN); + } catch (RemoteException e2) { + } + latch.countDown(); + } + }); + Shadows.shadowOf(mWorkHandler.getLooper()).getScheduler().advanceToNextPostedRunnable(); + + latch.await(15, TimeUnit.SECONDS); + } + + @Test + @SuppressWarnings("checkstyle:methodlength") + public void consumePurchaseNull() throws InterruptedException, RemoteException, BillingException { + final CountDownLatch latch = new CountDownLatch(1); + final int responseCode = 3; + PurchaseGetter getter = spy(new PurchaseGetter(mContext)); + Bundle responseBundle = Util.createPurchaseBundle(0, 0, 10, null); + + doReturn(responseCode).when(mService).consumePurchase( + mContext.getApiVersion(), mContext.getContext().getPackageName(), Constants.TEST_PURCHASE_TOKEN); + + doReturn(true).when(mProcessor).isSupported(PurchaseType.IN_APP, mService); + doReturn(mServiceBinder).when(mProcessor).createServiceBinder(); + doReturn(getter).when(mProcessor).createPurchaseGetter(); + doReturn(responseBundle).when(mService).getPurchases( + mContext.getApiVersion(), mContext.getContext().getPackageName(), Constants.ITEM_TYPE_INAPP, null); + + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + ServiceBinder.Handler handler = invocation.getArgument(0); + handler.onBind(mService); + return null; + } + }).when(mServiceBinder).getServiceAsync(any(ServiceBinder.Handler.class)); + + mProcessor.consume(Constants.TEST_PRODUCT_ID, new ConsumeItemHandler() { + @Override + public void onSuccess() { + throw new IllegalStateException(); + } + + @Override + public void onError(BillingException e) { + assertThat(e.getErrorCode()).isEqualTo(Constants.ERROR_PURCHASE_DATA); + assertThat(e.getMessage()).isEqualTo(Constants.ERROR_MSG_PURCHASE_OR_TOKEN_NULL); + try { + verify(mService, never()).consumePurchase( + mContext.getApiVersion(), mContext.getContext().getPackageName(), Constants.TEST_PURCHASE_TOKEN); + } catch (RemoteException e2) { + } + latch.countDown(); + } + }); + Shadows.shadowOf(mWorkHandler.getLooper()).getScheduler().advanceToNextPostedRunnable(); + + latch.await(15, TimeUnit.SECONDS); + } + + @Test + @SuppressWarnings("checkstyle:methodlength") + public void consumePurchaseTokenNull() throws InterruptedException, RemoteException, BillingException { + final CountDownLatch latch = new CountDownLatch(1); + PurchaseGetter getter = spy(new PurchaseGetter(mContext)); + Bundle responseBundle = Util.createPurchaseWithNoTokenBundle(0, 0, 10, null); + + doReturn(true).when(mProcessor).isSupported(PurchaseType.IN_APP, mService); + doReturn(mServiceBinder).when(mProcessor).createServiceBinder(); + doReturn(getter).when(mProcessor).createPurchaseGetter(); + doReturn(responseBundle).when(mService).getPurchases( + mContext.getApiVersion(), mContext.getContext().getPackageName(), Constants.ITEM_TYPE_INAPP, null); + + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + ServiceBinder.Handler handler = invocation.getArgument(0); + handler.onBind(mService); + return null; + } + }).when(mServiceBinder).getServiceAsync(any(ServiceBinder.Handler.class)); + + mProcessor.consume(String.format(Locale.US, "%s_%d", Constants.TEST_PRODUCT_ID, 0), new ConsumeItemHandler() { + @Override + public void onSuccess() { + throw new IllegalStateException(); + } + + @Override + public void onError(BillingException e) { + assertThat(e.getErrorCode()).isEqualTo(Constants.ERROR_PURCHASE_DATA); + assertThat(e.getMessage()).isEqualTo(Constants.ERROR_MSG_PURCHASE_OR_TOKEN_NULL); + try { + verify(mService, never()).consumePurchase( + mContext.getApiVersion(), mContext.getContext().getPackageName(), Constants.TEST_PURCHASE_TOKEN); + } catch (RemoteException e2) { + } + latch.countDown(); + } + }); + Shadows.shadowOf(mWorkHandler.getLooper()).getScheduler().advanceToNextPostedRunnable(); + + latch.await(15, TimeUnit.SECONDS); + } + + @Test + @SuppressWarnings("checkstyle:methodlength") + public void remoteException() throws InterruptedException, RemoteException, BillingException { + final CountDownLatch latch = new CountDownLatch(1); + PurchaseGetter getter = spy(new PurchaseGetter(mContext)); + Bundle responseBundle = Util.createPurchaseBundle(0, 0, 10, null); + + doThrow(RemoteException.class).when(mService).consumePurchase( + mContext.getApiVersion(), mContext.getContext().getPackageName(), Constants.TEST_PURCHASE_TOKEN); + + doReturn(true).when(mProcessor).isSupported(PurchaseType.IN_APP, mService); + doReturn(mServiceBinder).when(mProcessor).createServiceBinder(); + doReturn(getter).when(mProcessor).createPurchaseGetter(); + doReturn(responseBundle).when(mService).getPurchases( + mContext.getApiVersion(), mContext.getContext().getPackageName(), Constants.ITEM_TYPE_INAPP, null); + + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + ServiceBinder.Handler handler = invocation.getArgument(0); + handler.onBind(mService); + return null; + } + }).when(mServiceBinder).getServiceAsync(any(ServiceBinder.Handler.class)); + + mProcessor.consume(String.format(Locale.US, "%s_%d", Constants.TEST_PRODUCT_ID, 0), new ConsumeItemHandler() { + @Override + public void onSuccess() { + throw new IllegalStateException(); + } + + @Override + public void onError(BillingException e) { + assertThat(e.getErrorCode()).isEqualTo(Constants.ERROR_REMOTE_EXCEPTION); + try { + verify(mService).consumePurchase( + mContext.getApiVersion(), mContext.getContext().getPackageName(), Constants.TEST_PURCHASE_TOKEN); + } catch (RemoteException e2) { + } + latch.countDown(); + } + }); + Shadows.shadowOf(mWorkHandler.getLooper()).getScheduler().advanceToNextPostedRunnable(); + + latch.await(15, TimeUnit.SECONDS); + } + + @Test + public void bindServiceError() throws InterruptedException, RemoteException, BillingException { + final CountDownLatch latch = new CountDownLatch(1); + + doReturn(mServiceBinder).when(mProcessor).createServiceBinder(); + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + ServiceBinder.Handler handler = invocation.getArgument(0); + handler.onError(new BillingException( + Constants.ERROR_BIND_SERVICE_FAILED_EXCEPTION, + Constants.ERROR_MSG_BIND_SERVICE_FAILED) + ); + return null; + } + }).when(mServiceBinder).getServiceAsync(any(ServiceBinder.Handler.class)); + + mProcessor.consume(null, new ConsumeItemHandler() { + @Override + public void onSuccess() { + throw new IllegalStateException(); + } + + @Override + public void onError(BillingException e) { + assertThat(e.getErrorCode()).isEqualTo(Constants.ERROR_BIND_SERVICE_FAILED_EXCEPTION); + assertThat(e.getMessage()).isEqualTo(Constants.ERROR_MSG_BIND_SERVICE_FAILED); + try { + verify(mService, never()).consumePurchase( + mContext.getApiVersion(), mContext.getContext().getPackageName(), Constants.TEST_PURCHASE_TOKEN); + } catch (RemoteException e2) { + } + latch.countDown(); + } + }); + Shadows.shadowOf(mWorkHandler.getLooper()).getScheduler().advanceToNextPostedRunnable(); + + latch.await(15, TimeUnit.SECONDS); + } + + @Test + public void billingNotSupport() throws InterruptedException, RemoteException, BillingException { + final CountDownLatch latch = new CountDownLatch(1); + + doReturn(false).when(mProcessor).isSupported(PurchaseType.IN_APP, mService); + doReturn(mServiceBinder).when(mProcessor).createServiceBinder(); + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + ServiceBinder.Handler handler = invocation.getArgument(0); + handler.onBind(mService); + return null; + } + }).when(mServiceBinder).getServiceAsync(any(ServiceBinder.Handler.class)); + + mProcessor.consume(null, new ConsumeItemHandler() { + @Override + public void onSuccess() { + throw new IllegalStateException(); + } + + @Override + public void onError(BillingException e) { + assertThat(e.getErrorCode()).isEqualTo(Constants.ERROR_PURCHASES_NOT_SUPPORTED); + assertThat(e.getMessage()).isEqualTo(Constants.ERROR_MSG_PURCHASES_NOT_SUPPORTED); + try { + verify(mService, never()).consumePurchase( + mContext.getApiVersion(), mContext.getContext().getPackageName(), Constants.TEST_PURCHASE_TOKEN); + } catch (RemoteException e2) { + } + latch.countDown(); + } + }); + Shadows.shadowOf(mWorkHandler.getLooper()).getScheduler().advanceToNextPostedRunnable(); + + latch.await(15, TimeUnit.SECONDS); + } +} \ No newline at end of file diff --git a/library/src/test/java/jp/alessandro/android/iab/ItemGetterTest.java b/library/src/test/java/jp/alessandro/android/iab/ItemGetterTest.java new file mode 100644 index 0000000..d05d655 --- /dev/null +++ b/library/src/test/java/jp/alessandro/android/iab/ItemGetterTest.java @@ -0,0 +1,289 @@ +/* + * Copyright (C) 2016 Alessandro Yuichi Okimoto + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * Contact email: alessandro@alessandro.jp + */ + +package jp.alessandro.android.iab; + +import android.os.Bundle; +import android.os.RemoteException; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Java6Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +/** + * Created by Alessandro Yuichi Okimoto on 2017/02/19. + */ + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class ItemGetterTest { + + @Rule + public MockitoRule mMockitoRule = MockitoJUnit.rule(); + + @Mock + BillingService mService; + + private final BillingContext mBillingContext = Util.newBillingContext(RuntimeEnvironment.application); + + private ItemGetter mGetter; + + @Before + public void setUp() { + mGetter = new ItemGetter(mBillingContext); + } + + @Test + public void remoteException() throws RemoteException { + Bundle requestBundle = new Bundle(); + + Mockito.when(mService.getSkuDetails( + mBillingContext.getApiVersion(), + mBillingContext.getContext().getPackageName(), + Constants.TYPE_IN_APP, + requestBundle + )).thenThrow(RemoteException.class); + + ItemDetails itemDetails = null; + try { + itemDetails = mGetter.get(mService, Constants.TYPE_IN_APP, requestBundle); + } catch (BillingException e) { + assertThat(e.getErrorCode()).isEqualTo(Constants.ERROR_REMOTE_EXCEPTION); + } finally { + assertThat(itemDetails).isNull(); + } + } + + @Test + public void getItemDetails() throws RemoteException, BillingException { + ArrayList itemIds = new ArrayList<>(); + Bundle requestBundle = new Bundle(); + requestBundle.putStringArrayList(Constants.RESPONSE_ITEM_ID_LIST, itemIds); + + int size = 10; + ArrayList items = Util.createSkuItemDetailsJsonArray(size); + Bundle responseBundle = new Bundle(); + responseBundle.putLong(Constants.RESPONSE_CODE, 0L); + responseBundle.putStringArrayList(Constants.RESPONSE_DETAILS_LIST, items); + + Mockito.when(mService.getSkuDetails( + mBillingContext.getApiVersion(), + mBillingContext.getContext().getPackageName(), + Constants.TYPE_IN_APP, + requestBundle + )).thenReturn(responseBundle); + + ItemDetails itemDetails = null; + try { + itemDetails = mGetter.get(mService, Constants.TYPE_IN_APP, requestBundle); + } finally { + assertThat(itemDetails).isNotNull(); + assertThat(itemDetails.getSize()).isEqualTo(size); + assertThat(itemDetails.getAll()).isNotNull(); + + List purchaseList = itemDetails.getAll(); + for (Item p : purchaseList) { + assertThat(itemDetails.hasItemId(p.getSku())).isTrue(); + assertThat(itemDetails.getByItemId(p.getSku())).isNotNull(); + } + } + } + + @Test + public void getItemDetailsJsonBroken() throws RemoteException, BillingException { + ArrayList itemIds = new ArrayList<>(); + Bundle requestBundle = new Bundle(); + requestBundle.putStringArrayList(Constants.RESPONSE_ITEM_ID_LIST, itemIds); + + ArrayList items = Util.createSkuDetailsJsonBrokenArray(); + Bundle responseBundle = new Bundle(); + responseBundle.putLong(Constants.RESPONSE_CODE, 0L); + responseBundle.putStringArrayList(Constants.RESPONSE_DETAILS_LIST, items); + + getItemDetails( + requestBundle, + responseBundle, + Constants.ERROR_BAD_RESPONSE, + Constants.ERROR_MSG_BAD_RESPONSE + ); + } + + @Test + public void getItemDetailsWithEmptyArray() throws RemoteException { + ArrayList itemIds = new ArrayList<>(); + Bundle requestBundle = new Bundle(); + requestBundle.putStringArrayList(Constants.RESPONSE_ITEM_ID_LIST, itemIds); + + Bundle responseBundle = new Bundle(); + responseBundle.putLong(Constants.RESPONSE_CODE, 0L); + + getItemDetails( + requestBundle, + responseBundle, + Constants.ERROR_UNEXPECTED_TYPE, + Constants.ERROR_MSG_GET_SKU_DETAILS_RESPONSE_LIST_NULL + ); + } + + @Test + public void getItemDetailsWithArrayNull() throws RemoteException { + Bundle requestBundle = new Bundle(); + + Bundle responseBundle = new Bundle(); + responseBundle.putLong(Constants.RESPONSE_CODE, 0L); + + getItemDetails( + requestBundle, + responseBundle, + Constants.ERROR_UNEXPECTED_TYPE, + Constants.ERROR_MSG_GET_SKU_DETAILS_RESPONSE_LIST_NULL + ); + } + + @Test + public void responseBundleResponseNull() throws RemoteException { + try { + mGetter.get(mService, Constants.TYPE_IN_APP, null); + } catch (BillingException e) { + assertThat(e.getErrorCode()).isEqualTo(Constants.ERROR_UNEXPECTED_TYPE); + assertThat(e.getMessage()).isEqualTo(Constants.ERROR_MSG_UNEXPECTED_BUNDLE_RESPONSE_NULL); + } finally { + verify(mService).getSkuDetails( + mBillingContext.getApiVersion(), + mBillingContext.getContext().getPackageName(), + Constants.TYPE_IN_APP, + null + ); + verifyNoMoreInteractions(mService); + } + } + + @Test + public void getWithLongResponseCode() throws RemoteException { + Bundle requestBundle = new Bundle(); + Bundle responseBundle = new Bundle(); + responseBundle.putLong(Constants.RESPONSE_CODE, 0L); + + getItemDetails( + requestBundle, + responseBundle, + Constants.ERROR_UNEXPECTED_TYPE, + Constants.ERROR_MSG_GET_SKU_DETAILS_RESPONSE_LIST_NULL + ); + } + + @Test + public void getWithDifferentResponseCode() throws RemoteException { + Bundle requestBundle = new Bundle(); + Bundle responseBundle = new Bundle(); + responseBundle.putInt(Constants.RESPONSE_CODE, 3); + + getItemDetails( + requestBundle, + responseBundle, + 3, + Constants.ERROR_MSG_GET_SKU_DETAILS + ); + } + + @Test + public void getWithIntegerResponseCode() throws RemoteException { + Bundle requestBundle = new Bundle(); + Bundle responseBundle = new Bundle(); + responseBundle.putInt(Constants.RESPONSE_CODE, 0); + + getItemDetails( + requestBundle, + responseBundle, + Constants.ERROR_UNEXPECTED_TYPE, + Constants.ERROR_MSG_GET_SKU_DETAILS_RESPONSE_LIST_NULL + ); + } + + @Test + public void getWithNoResponseCode() throws RemoteException { + Bundle requestBundle = new Bundle(); + Bundle responseBundle = new Bundle(); + + getItemDetails( + responseBundle, + requestBundle, + Constants.ERROR_UNEXPECTED_TYPE, + Constants.ERROR_MSG_GET_SKU_DETAILS_RESPONSE_LIST_NULL + ); + } + + @Test + public void stringResponseCode() throws InterruptedException, RemoteException { + Bundle requestBundle = new Bundle(); + Bundle responseBundle = new Bundle(); + responseBundle.putString(Constants.RESPONSE_CODE, "0"); + + getItemDetails( + requestBundle, + responseBundle, + Constants.ERROR_UNEXPECTED_TYPE, + Constants.ERROR_MSG_UNEXPECTED_BUNDLE_RESPONSE + ); + } + + private void getItemDetails(Bundle requestBundle, + Bundle responseBundle, + int errorCode, + String errorMessage) throws RemoteException { + + Mockito.when(mService.getSkuDetails( + mBillingContext.getApiVersion(), + mBillingContext.getContext().getPackageName(), + Constants.TYPE_IN_APP, + requestBundle + )).thenReturn(responseBundle); + + ItemDetails itemDetails = null; + try { + itemDetails = mGetter.get(mService, Constants.TYPE_IN_APP, requestBundle); + } catch (BillingException e) { + assertThat(e.getErrorCode()).isEqualTo(errorCode); + assertThat(e.getMessage()).isEqualTo(errorMessage); + } finally { + assertThat(itemDetails).isNull(); + verify(mService).getSkuDetails( + mBillingContext.getApiVersion(), + mBillingContext.getContext().getPackageName(), + Constants.TYPE_IN_APP, + requestBundle + ); + verifyNoMoreInteractions(mService); + } + } +} \ No newline at end of file diff --git a/library/src/test/java/jp/alessandro/android/iab/ItemParcelableTest.java b/library/src/test/java/jp/alessandro/android/iab/ItemParcelableTest.java new file mode 100644 index 0000000..a0f6e1a --- /dev/null +++ b/library/src/test/java/jp/alessandro/android/iab/ItemParcelableTest.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2016 Alessandro Yuichi Okimoto + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * Contact email: alessandro@alessandro.jp + */ + +package jp.alessandro.android.iab; + +import android.os.Parcel; + +import org.json.JSONException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.util.Locale; + +import static org.assertj.core.api.Java6Assertions.assertThat; + +/** + * Created by Alessandro Yuichi Okimoto on 2016/07/23. + */ + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE, constants = BuildConfig.class) +public class ItemParcelableTest { + + @Test + public void writeToParcel() throws JSONException { + Item item = Item.parseJson(String.format(Locale.ENGLISH, Constants.SKU_DETAIL_JSON, 0)); + + // Obtain a Parcel object and write the parcelable object to it + Parcel parcel = Parcel.obtain(); + item.writeToParcel(parcel, item.describeContents()); + + // After you're done with writing, you need to reset the parcel for reading + parcel.setDataPosition(0); + + Item fromParcel = Item.CREATOR.createFromParcel(parcel); + + assertThat(item.getOriginalJson()).isEqualTo(fromParcel.getOriginalJson()); + assertThat(item.getSku()).isEqualTo(fromParcel.getSku()); + assertThat(item.getType()).isEqualTo(fromParcel.getType()); + assertThat(item.getTitle()).isEqualTo(fromParcel.getTitle()); + assertThat(item.getDescription()).isEqualTo(fromParcel.getDescription()); + assertThat(item.getCurrency()).isEqualTo(fromParcel.getCurrency()); + assertThat(item.getPrice()).isEqualTo(fromParcel.getPrice()); + assertThat(item.getPriceMicros()).isEqualTo(fromParcel.getPriceMicros()); + } + + @Test + public void newArray() { + Item[] items = Item.CREATOR.newArray(10); + assertThat(items.length).isEqualTo(10); + } +} \ No newline at end of file diff --git a/library/src/test/java/jp/alessandro/android/iab/OnActivityResultTest.java b/library/src/test/java/jp/alessandro/android/iab/OnActivityResultTest.java new file mode 100644 index 0000000..9c09925 --- /dev/null +++ b/library/src/test/java/jp/alessandro/android/iab/OnActivityResultTest.java @@ -0,0 +1,291 @@ +/* + * Copyright (C) 2016 Alessandro Yuichi Okimoto + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * Contact email: alessandro@alessandro.jp + */ + +package jp.alessandro.android.iab; + +import android.app.Activity; +import android.app.PendingIntent; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.RemoteException; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.mockito.stubbing.Answer; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import jp.alessandro.android.iab.handler.PurchaseHandler; +import jp.alessandro.android.iab.handler.StartActivityHandler; +import jp.alessandro.android.iab.response.PurchaseResponse; + +import static org.assertj.core.api.Java6Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +/** + * Created by Alessandro Yuichi Okimoto on 2017/02/19. + */ + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE, constants = BuildConfig.class) +public class OnActivityResultTest { + + private Handler mWorkHandler; + private BillingProcessor mProcessor; + + private final BillingContext mContext = Util.newBillingContext(RuntimeEnvironment.application); + + @Rule + public MockitoRule mMockitoRule = MockitoJUnit.rule(); + + @Mock + BillingService mService; + @Mock + ServiceBinder mServiceBinder; + @Mock + Activity mActivity; + + @Before + public void setUp() { + HandlerThread thread = new HandlerThread("AndroidIabThread"); + thread.start(); + // Handler to post all actions in the library + mWorkHandler = new Handler(thread.getLooper()); + } + + @Test + public void onActivityResultInAppSuccess() throws InterruptedException, RemoteException { + onActivityResultSuccess(PurchaseType.IN_APP); + } + + @Test + public void onActivityResultSubscriptionSuccess() throws InterruptedException, RemoteException { + onActivityResultSuccess(PurchaseType.SUBSCRIPTION); + } + + @Test + public void onActivityResultInAppSignatureVerificationFailed() throws InterruptedException, RemoteException { + onActivityResultSignatureVerificationFailed(PurchaseType.IN_APP); + } + + @Test + public void onActivityResultSubscriptionSignatureVerificationFailed() throws InterruptedException, RemoteException { + onActivityResultSignatureVerificationFailed(PurchaseType.SUBSCRIPTION); + } + + @Test + public void onActivityResultInAppDifferentRequestCode() throws InterruptedException, RemoteException { + onActivityResultDifferentRequestCode(PurchaseType.IN_APP); + } + + @Test + public void onActivityResultSubscriptionDifferentRequestCode() throws InterruptedException, RemoteException { + onActivityResultDifferentRequestCode(PurchaseType.SUBSCRIPTION); + } + + @Test + public void onActivityResultInAppDifferentThread() throws InterruptedException, RemoteException { + onActivityResultDifferentThread(PurchaseType.IN_APP); + } + + @Test + public void onActivityResultSubscriptionDifferentThread() throws InterruptedException, RemoteException { + onActivityResultDifferentThread(PurchaseType.SUBSCRIPTION); + } + + private void onActivityResultSuccess(PurchaseType type) throws InterruptedException, RemoteException { + final CountDownLatch latch = new CountDownLatch(1); + final int requestCode = 1001; + + setUpStartPurchase(latch, type, true); + mProcessor.startPurchase(mActivity, + requestCode, + Constants.TEST_PRODUCT_ID, + type, + Constants.TEST_DEVELOPER_PAYLOAD, + new StartActivityHandler() { + @Override + public void onSuccess() { + assertThat(mProcessor.onActivityResult(requestCode, -1, Util.newOkIntent())).isTrue(); + } + + @Override + public void onError(BillingException e) { + throw new IllegalStateException(); + } + }); + latch.await(15, TimeUnit.SECONDS); + } + + private void onActivityResultSignatureVerificationFailed(PurchaseType type) throws InterruptedException, RemoteException { + final CountDownLatch latch = new CountDownLatch(1); + final int requestCode = 1001; + + setUpStartPurchase(latch, type, false); + mProcessor.startPurchase(mActivity, + requestCode, + Constants.TEST_PRODUCT_ID, + type, + Constants.TEST_DEVELOPER_PAYLOAD, + new StartActivityHandler() { + @Override + public void onSuccess() { + Intent intent = Util.newIntent(0, Constants.TEST_JSON_RECEIPT, ""); + assertThat(mProcessor.onActivityResult(requestCode, -1, intent)).isTrue(); + } + + @Override + public void onError(BillingException e) { + throw new IllegalStateException(); + } + }); + latch.await(15, TimeUnit.SECONDS); + } + + private void onActivityResultDifferentRequestCode(PurchaseType type) throws InterruptedException, RemoteException { + final CountDownLatch latch = new CountDownLatch(1); + final int requestCode = 1001; + + setUpStartPurchase(latch, type, false); + mProcessor.startPurchase(mActivity, + requestCode, + Constants.TEST_PRODUCT_ID, + type, + Constants.TEST_DEVELOPER_PAYLOAD, + new StartActivityHandler() { + @Override + public void onSuccess() { + assertThat(mProcessor.onActivityResult(1002, -1, Util.newOkIntent())).isFalse(); + latch.countDown(); + } + + @Override + public void onError(BillingException e) { + throw new IllegalStateException(); + } + }); + latch.await(15, TimeUnit.SECONDS); + } + + private void onActivityResultDifferentThread(PurchaseType type) throws InterruptedException, RemoteException { + final CountDownLatch latch = new CountDownLatch(1); + final int requestCode = 1001; + + setUpStartPurchase(latch, type, false); + mProcessor.startPurchase(mActivity, + requestCode, + Constants.TEST_PRODUCT_ID, + type, + Constants.TEST_DEVELOPER_PAYLOAD, + new StartActivityHandler() { + @Override + public void onSuccess() { + executeOnDifferentThread(latch, requestCode); + } + + @Override + public void onError(BillingException e) { + throw new IllegalStateException(); + } + }); + latch.await(15, TimeUnit.SECONDS); + } + + private void setUpStartPurchase(final CountDownLatch latch, + final PurchaseType type, + final boolean checkSuccess) throws RemoteException { + + PendingIntent pendingIntent = PendingIntent.getActivity(mContext.getContext(), 1, new Intent(), 0); + Bundle bundle = new Bundle(); + bundle.putLong(Constants.RESPONSE_CODE, 0L); + bundle.putParcelable(Constants.RESPONSE_BUY_INTENT, pendingIntent); + + PurchaseHandler handler = new PurchaseHandler() { + @Override + public void call(PurchaseResponse response) { + if (checkSuccess) { + assertThat(response.isSuccess()).isTrue(); + assertThat(response.getPurchase()).isNotNull(); + } else { + assertThat(response.getException().getErrorCode()).isEqualTo(Constants.ERROR_VERIFICATION_FAILED); + assertThat(response.getException().getMessage()).isEqualTo(Constants.ERROR_MSG_VERIFICATION_FAILED); + } + latch.countDown(); + } + }; + mProcessor = spy(new BillingProcessor(mContext, handler)); + + setUpProcessor(bundle, type); + + doReturn(mWorkHandler).when(mProcessor).getWorkHandler(); + } + + private void setUpProcessor(Bundle bundle, PurchaseType type) throws RemoteException { + when(mService.getBuyIntent( + mContext.getApiVersion(), + mContext.getContext().getPackageName(), + Constants.TEST_PRODUCT_ID, + type == PurchaseType.SUBSCRIPTION ? Constants.TYPE_SUBSCRIPTION : Constants.TYPE_IN_APP, + Constants.TEST_DEVELOPER_PAYLOAD + )).thenReturn(bundle); + + doReturn(true).when(mProcessor).isSupported(type, mService); + doReturn(mServiceBinder).when(mProcessor).createServiceBinder(); + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + ServiceBinder.Handler handler = invocation.getArgument(0); + handler.onBind(mService); + return null; + } + }).when(mServiceBinder).getServiceAsync(any(ServiceBinder.Handler.class)); + } + + private void executeOnDifferentThread(final CountDownLatch latch, final int requestCode) { + Thread th = new Thread(new Runnable() { + @Override + public void run() { + try { + Intent intent = Util.newIntent(0, Constants.TEST_JSON_RECEIPT, ""); + mProcessor.onActivityResult(requestCode, -1, intent); + } catch (IllegalStateException e) { + assertThat(e.getMessage()).isEqualTo(Constants.ERROR_MSG_METHOD_MUST_BE_CALLED_ON_UI_THREAD); + } finally { + latch.countDown(); + } + } + }); + th.start(); + } +} \ No newline at end of file diff --git a/library/src/test/java/jp/alessandro/android/iab/PurchaseFlowLaunchTest.java b/library/src/test/java/jp/alessandro/android/iab/PurchaseFlowLaunchTest.java new file mode 100644 index 0000000..b4d2ccd --- /dev/null +++ b/library/src/test/java/jp/alessandro/android/iab/PurchaseFlowLaunchTest.java @@ -0,0 +1,328 @@ +/* + * Copyright (C) 2016 Alessandro Yuichi Okimoto + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * Contact email: alessandro@alessandro.jp + */ + +package jp.alessandro.android.iab; + +import android.app.Activity; +import android.app.PendingIntent; +import android.content.Intent; +import android.os.Bundle; +import android.os.RemoteException; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Java6Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +/** + * Created by Alessandro Yuichi Okimoto on 2017/02/19. + */ + +@RunWith(RobolectricTestRunner.class) +public class PurchaseFlowLaunchTest { + + private static final String TYPE_IN_APP = "inapp"; + private static final String TYPE_SUBSCRIPTION = "subs"; + + private final BillingContext mBillingContext = Util.newBillingContext(RuntimeEnvironment.application); + + @Rule + public MockitoRule mMockitoRule = MockitoJUnit.rule(); + + @Mock + BillingService mService; + + @Mock + Activity mActivity; + + @Test + public void startIntentSenderForResultError() throws RemoteException, BillingException { + PurchaseFlowLauncher launcher = new PurchaseFlowLauncher(mBillingContext, TYPE_IN_APP); + int requestCode = 1001; + PendingIntent pendingIntent = PendingIntent.getActivity(mBillingContext.getContext(), 1, new Intent(), 0); + pendingIntent.cancel(); + Bundle bundle = new Bundle(); + bundle.putLong(Constants.RESPONSE_CODE, 0L); + bundle.putParcelable(Constants.RESPONSE_BUY_INTENT, pendingIntent); + + startIntentSender(bundle, launcher, requestCode, Constants.ERROR_SEND_INTENT_FAILED); + } + + @Test + public void startIntentSenderForResult() throws RemoteException, BillingException { + PurchaseFlowLauncher launcher = new PurchaseFlowLauncher(mBillingContext, TYPE_IN_APP); + int requestCode = 1001; + PendingIntent pendingIntent = PendingIntent.getActivity(mBillingContext.getContext(), 1, new Intent(), 0); + Bundle bundle = new Bundle(); + bundle.putLong(Constants.RESPONSE_CODE, 0L); + bundle.putParcelable(Constants.RESPONSE_BUY_INTENT, pendingIntent); + + startIntentSender(bundle, launcher, requestCode, -1099); + } + + private void startIntentSender(Bundle bundle, + PurchaseFlowLauncher launcher, + int requestCode, + int errorCode) throws RemoteException { + + Mockito.when(mService.getBuyIntent( + mBillingContext.getApiVersion(), + mBillingContext.getContext().getPackageName(), + "", + TYPE_IN_APP, + "" + )).thenReturn(bundle); + + try { + launcher.launch(mService, mActivity, requestCode, null, "", ""); + } catch (BillingException e) { + assertThat(e.getErrorCode()).isEqualTo(errorCode); + } finally { + verify(mService).getBuyIntent( + mBillingContext.getApiVersion(), + mBillingContext.getContext().getPackageName(), + "", + TYPE_IN_APP, + "" + ); + verifyNoMoreInteractions(mService); + } + } + + @Test + public void bundleResponseNull() throws RemoteException { + PurchaseFlowLauncher launcher = new PurchaseFlowLauncher(mBillingContext, TYPE_IN_APP); + int requestCode = 1001; + + try { + launcher.launch(mService, mActivity, requestCode, null, "", ""); + } catch (BillingException e) { + assertThat(e.getErrorCode()).isEqualTo(Constants.ERROR_UNEXPECTED_TYPE); + assertThat(e.getMessage()).isEqualTo(Constants.ERROR_MSG_UNEXPECTED_BUNDLE_RESPONSE_NULL); + } finally { + verify(mService).getBuyIntent( + mBillingContext.getApiVersion(), + mBillingContext.getContext().getPackageName(), + "", + TYPE_IN_APP, + "" + ); + verifyNoMoreInteractions(mService); + } + } + + @Test + public void remoteExceptionOnLaunch() throws RemoteException { + PurchaseFlowLauncher launcher = new PurchaseFlowLauncher(mBillingContext, TYPE_IN_APP); + int requestCode = 1001; + + Mockito.when(mService.getBuyIntent( + mBillingContext.getApiVersion(), + mBillingContext.getContext().getPackageName(), + "", + TYPE_IN_APP, + "" + )).thenThrow(RemoteException.class); + + try { + launcher.launch(mService, mActivity, requestCode, null, "", ""); + } catch (BillingException e) { + assertThat(e.getErrorCode()).isEqualTo(Constants.ERROR_REMOTE_EXCEPTION); + } finally { + verify(mService).getBuyIntent( + mBillingContext.getApiVersion(), + mBillingContext.getContext().getPackageName(), + "", + TYPE_IN_APP, + "" + ); + verifyNoMoreInteractions(mService); + } + } + + @Test + public void pendingIntentNullUpdateSubscription() throws RemoteException { + PurchaseFlowLauncher launcher = new PurchaseFlowLauncher(mBillingContext, TYPE_SUBSCRIPTION); + int requestCode = 1001; + Bundle bundle = new Bundle(); + bundle.putLong(Constants.RESPONSE_CODE, 0L); + List oldSkus = new ArrayList<>(); + oldSkus.add("test"); + + Mockito.when(mService.getBuyIntentToReplaceSkus( + BillingApi.VERSION_5.getValue(), + mBillingContext.getContext().getPackageName(), + oldSkus, + "", + TYPE_SUBSCRIPTION, + "" + )).thenReturn(bundle); + + try { + launcher.launch(mService, mActivity, requestCode, oldSkus, "", ""); + } catch (BillingException e) { + assertThat(e.getErrorCode()).isEqualTo(Constants.ERROR_PENDING_INTENT); + assertThat(e.getMessage()).isEqualTo(Constants.ERROR_MSG_PENDING_INTENT); + } finally { + verify(mService).getBuyIntentToReplaceSkus( + BillingApi.VERSION_5.getValue(), + mBillingContext.getContext().getPackageName(), + oldSkus, + "", + TYPE_SUBSCRIPTION, + "" + ); + verifyNoMoreInteractions(mService); + } + } + + @Test + public void pendingIntentNullWithLongResponseCode() throws RemoteException { + PurchaseFlowLauncher launcher = new PurchaseFlowLauncher(mBillingContext, TYPE_IN_APP); + int requestCode = 1001; + Bundle bundle = new Bundle(); + bundle.putLong(Constants.RESPONSE_CODE, 0L); + + noPendingIntent(mActivity, + launcher, + bundle, + requestCode, + Constants.ERROR_PENDING_INTENT, + Constants.ERROR_MSG_PENDING_INTENT); + } + + @Test + public void pendingIntentNullWithDifferentResponseCode() throws RemoteException { + PurchaseFlowLauncher launcher = new PurchaseFlowLauncher(mBillingContext, TYPE_IN_APP); + int requestCode = 1001; + int responseCode = -100; + Bundle bundle = new Bundle(); + bundle.putLong(Constants.RESPONSE_CODE, responseCode); + + noPendingIntent(mActivity, + launcher, + bundle, + requestCode, + responseCode, + Constants.ERROR_MSG_UNABLE_TO_BUY); + } + + @Test + public void pendingIntentNullWithIntegerResponseCode() + throws RemoteException, BillingException { + + PurchaseFlowLauncher launcher = new PurchaseFlowLauncher(mBillingContext, TYPE_IN_APP); + int requestCode = 1001; + Bundle bundle = new Bundle(); + bundle.putInt(Constants.RESPONSE_CODE, 0); + + noPendingIntent(mActivity, + launcher, + bundle, + requestCode, + Constants.ERROR_PENDING_INTENT, + Constants.ERROR_MSG_PENDING_INTENT); + } + + @Test + public void noResponseCode() throws RemoteException { + PurchaseFlowLauncher launcher = new PurchaseFlowLauncher(mBillingContext, TYPE_IN_APP); + Bundle bundle = new Bundle(); + int requestCode = 1001; + + noPendingIntent(mActivity, + launcher, + bundle, + requestCode, + Constants.ERROR_PENDING_INTENT, + Constants.ERROR_MSG_PENDING_INTENT); + } + + @Test + public void stringResponseCode() throws RemoteException { + PurchaseFlowLauncher launcher = new PurchaseFlowLauncher(mBillingContext, TYPE_IN_APP); + int requestCode = 1001; + Bundle bundle = new Bundle(); + bundle.putString(Constants.RESPONSE_CODE, "0"); + + noPendingIntent(mActivity, + launcher, + bundle, + requestCode, + Constants.ERROR_UNEXPECTED_TYPE, + Constants.ERROR_MSG_UNEXPECTED_BUNDLE_RESPONSE); + } + + @Test + public void lostContext() throws RemoteException { + PurchaseFlowLauncher launcher = new PurchaseFlowLauncher(mBillingContext, TYPE_IN_APP); + int requestCode = 1001; + Bundle bundle = new Bundle(); + bundle.putInt(Constants.RESPONSE_CODE, 0); + + noPendingIntent(null, + launcher, + bundle, + requestCode, + Constants.ERROR_LOST_CONTEXT, + Constants.ERROR_MSG_LOST_CONTEXT); + } + + private void noPendingIntent(Activity activity, + PurchaseFlowLauncher launcher, + Bundle bundle, + int requestCode, + int errorCode, + String errorMessage) throws RemoteException { + Mockito.when(mService.getBuyIntent( + mBillingContext.getApiVersion(), + mBillingContext.getContext().getPackageName(), + "", + TYPE_IN_APP, + "" + )).thenReturn(bundle); + + try { + launcher.launch(mService, activity, requestCode, null, "", ""); + } catch (BillingException e) { + assertThat(e.getErrorCode()).isEqualTo(errorCode); + assertThat(e.getMessage()).isEqualTo(errorMessage); + } finally { + verify(mService).getBuyIntent( + mBillingContext.getApiVersion(), + mBillingContext.getContext().getPackageName(), + "", + TYPE_IN_APP, + "" + ); + verifyNoMoreInteractions(mService); + } + } +} \ No newline at end of file diff --git a/library/src/test/java/jp/alessandro/android/iab/PurchaseFlowOnActivityResultTest.java b/library/src/test/java/jp/alessandro/android/iab/PurchaseFlowOnActivityResultTest.java new file mode 100644 index 0000000..165774a --- /dev/null +++ b/library/src/test/java/jp/alessandro/android/iab/PurchaseFlowOnActivityResultTest.java @@ -0,0 +1,316 @@ +/* + * Copyright (C) 2016 Alessandro Yuichi Okimoto + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * Contact email: alessandro@alessandro.jp + */ + +package jp.alessandro.android.iab; + +import android.app.Activity; +import android.content.Intent; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +import java.util.Locale; + +import static org.assertj.core.api.Java6Assertions.assertThat; + +/** + * Created by Alessandro Yuichi Okimoto on 2017/02/19. + */ + +@RunWith(RobolectricTestRunner.class) +public class PurchaseFlowOnActivityResultTest { + + static Intent newIntent(String data, String signature) { + final Intent intent = new Intent(); + intent.putExtra(Constants.RESPONSE_INAPP_PURCHASE_DATA, data); + intent.putExtra(Constants.RESPONSE_INAPP_SIGNATURE, signature); + return intent; + } + + @Mock + BillingService mService; + + private final BillingContext mBillingContext = Util.newBillingContext(RuntimeEnvironment.application); + + @Test + public void purchaseJsonDataBroken() { + PurchaseFlowLauncher launcher = new PurchaseFlowLauncher(mBillingContext, Constants.TYPE_IN_APP); + int requestCode = 0; + int resultCode = Activity.RESULT_OK; + Intent intent = PurchaseFlowOnActivityResultTest.newIntent( + Constants.TEST_JSON_BROKEN, Security.signData(Constants.TEST_JSON_BROKEN)); + + intent.putExtra(Constants.RESPONSE_CODE, 0); + + checkIntent(launcher, + requestCode, + resultCode, + intent, + Constants.ERROR_BAD_RESPONSE, + Constants.ERROR_MSG_BAD_RESPONSE); + } + + @Test + public void differentRequestCode() { + PurchaseFlowLauncher launcher = new PurchaseFlowLauncher(mBillingContext, Constants.TYPE_IN_APP); + int requestCode = 1; + int resultCode = Activity.RESULT_OK; + + checkIntent(launcher, + requestCode, + resultCode, + null, + Constants.ERROR_BAD_RESPONSE, + Constants.ERROR_MSG_RESULT_REQUEST_CODE_INVALID); + } + + @Test + public void unknownResultCode() { + PurchaseFlowLauncher launcher = new PurchaseFlowLauncher(mBillingContext, Constants.TYPE_IN_APP); + int requestCode = 0; + int resultCode = 3; + Intent intent = PurchaseFlowOnActivityResultTest.newIntent( + Constants.TEST_JSON_RECEIPT, Security.signData(Constants.TEST_JSON_RECEIPT)); + + intent.putExtra(Constants.RESPONSE_CODE, 0); + + checkIntent(launcher, + requestCode, + resultCode, + intent, + 3, + String.format(Locale.US, Constants.ERROR_MSG_RESULT_UNKNOWN, resultCode)); + } + + @Test + public void cancelResultCode() { + PurchaseFlowLauncher launcher = new PurchaseFlowLauncher(mBillingContext, Constants.TYPE_IN_APP); + int requestCode = 0; + int resultCode = Activity.RESULT_CANCELED; + Intent intent = PurchaseFlowOnActivityResultTest.newIntent(null, null); + intent.putExtra(Constants.RESPONSE_CODE, 0); + + checkIntent(launcher, + requestCode, + resultCode, + intent, + Activity.RESULT_CANCELED, + Constants.ERROR_MSG_RESULT_CANCELED); + } + + @Test + public void signatureEmpty() { + PurchaseFlowLauncher launcher = new PurchaseFlowLauncher(mBillingContext, Constants.TYPE_IN_APP); + int requestCode = 0; + int resultCode = Activity.RESULT_OK; + Intent intent = PurchaseFlowOnActivityResultTest.newIntent(Constants.TEST_JSON_RECEIPT, ""); + intent.putExtra(Constants.RESPONSE_CODE, 0); + + checkIntent(launcher, + requestCode, + resultCode, + intent, + Constants.ERROR_VERIFICATION_FAILED, + Constants.ERROR_MSG_VERIFICATION_FAILED); + } + + @Test + public void signatureNull() { + PurchaseFlowLauncher launcher = new PurchaseFlowLauncher(mBillingContext, Constants.TYPE_IN_APP); + int requestCode = 0; + int resultCode = Activity.RESULT_OK; + Intent intent = PurchaseFlowOnActivityResultTest.newIntent(Constants.TEST_JSON_RECEIPT, null); + intent.putExtra(Constants.RESPONSE_CODE, 0); + + checkIntent(launcher, + requestCode, + resultCode, + intent, + Constants.ERROR_PURCHASE_DATA, + Constants.ERROR_MSG_NULL_PURCHASE_DATA); + } + + @Test + public void purchaseDataEmpty() { + PurchaseFlowLauncher launcher = new PurchaseFlowLauncher(mBillingContext, Constants.TYPE_IN_APP); + int requestCode = 0; + int resultCode = Activity.RESULT_OK; + Intent intent = PurchaseFlowOnActivityResultTest.newIntent("", Security.signData(Constants.TEST_JSON_RECEIPT)); + intent.putExtra(Constants.RESPONSE_CODE, 0); + + checkIntent(launcher, + requestCode, + resultCode, + intent, + Constants.ERROR_VERIFICATION_FAILED, + Constants.ERROR_MSG_VERIFICATION_FAILED); + } + + @Test + public void purchaseAndSignatureEmpty() { + PurchaseFlowLauncher launcher = new PurchaseFlowLauncher(mBillingContext, Constants.TYPE_IN_APP); + int requestCode = 0; + int resultCode = Activity.RESULT_OK; + Intent intent = PurchaseFlowOnActivityResultTest.newIntent("", ""); + intent.putExtra(Constants.RESPONSE_CODE, 0); + + checkIntent(launcher, + requestCode, + resultCode, + intent, + Constants.ERROR_VERIFICATION_FAILED, + Constants.ERROR_MSG_VERIFICATION_FAILED); + } + + @Test + public void purchaseAndSignatureNull() { + PurchaseFlowLauncher launcher = new PurchaseFlowLauncher(mBillingContext, Constants.TYPE_IN_APP); + int requestCode = 0; + int resultCode = Activity.RESULT_OK; + Intent intent = PurchaseFlowOnActivityResultTest.newIntent(null, null); + intent.putExtra(Constants.RESPONSE_CODE, 0); + + checkIntent(launcher, + requestCode, + resultCode, + intent, + Constants.ERROR_PURCHASE_DATA, + Constants.ERROR_MSG_NULL_PURCHASE_DATA); + } + + @Test + public void purchaseDataNull() { + PurchaseFlowLauncher launcher = new PurchaseFlowLauncher(mBillingContext, Constants.TYPE_IN_APP); + int requestCode = 0; + int resultCode = Activity.RESULT_OK; + Intent intent = PurchaseFlowOnActivityResultTest.newIntent(null, Security.signData(Constants.TEST_JSON_RECEIPT)); + intent.putExtra(Constants.RESPONSE_CODE, 0); + + checkIntent(launcher, + requestCode, + resultCode, + intent, + Constants.ERROR_PURCHASE_DATA, + Constants.ERROR_MSG_NULL_PURCHASE_DATA); + } + + @Test + public void intentResponseNull() { + PurchaseFlowLauncher launcher = new PurchaseFlowLauncher(mBillingContext, Constants.TYPE_IN_APP); + int requestCode = 0; + int resultCode = Activity.RESULT_OK; + + checkIntent(launcher, + requestCode, + resultCode, + null, + Constants.ERROR_UNEXPECTED_TYPE, + Constants.ERROR_MSG_RESULT_NULL_INTENT); + } + + @Test + public void intentWithLongResponseCode() { + PurchaseFlowLauncher launcher = new PurchaseFlowLauncher(mBillingContext, Constants.TYPE_IN_APP); + int requestCode = 0; + int resultCode = Activity.RESULT_OK; + Intent intent = PurchaseFlowOnActivityResultTest.newIntent( + Constants.TEST_JSON_RECEIPT, Security.signData(Constants.TEST_JSON_RECEIPT)); + + intent.putExtra(Constants.RESPONSE_CODE, 0L); + + checkIntent(launcher, requestCode, resultCode, intent, -1, null); + } + + @Test + public void intentWithDifferentResponseCode() { + PurchaseFlowLauncher launcher = new PurchaseFlowLauncher(mBillingContext, Constants.TYPE_IN_APP); + int requestCode = 0; + int resultCode = Activity.RESULT_OK; + Intent intent = PurchaseFlowOnActivityResultTest.newIntent( + Constants.TEST_JSON_RECEIPT, Security.signData(Constants.TEST_JSON_RECEIPT)); + + intent.putExtra(Constants.RESPONSE_CODE, -1001); + + checkIntent(launcher, requestCode, resultCode, intent, -1001, Constants.ERROR_MSG_RESULT_OK); + } + + @Test + public void intentWithIntegerResponseCode() { + PurchaseFlowLauncher launcher = new PurchaseFlowLauncher(mBillingContext, Constants.TYPE_IN_APP); + int requestCode = 0; + int resultCode = Activity.RESULT_OK; + Intent intent = PurchaseFlowOnActivityResultTest.newIntent( + Constants.TEST_JSON_RECEIPT, Security.signData(Constants.TEST_JSON_RECEIPT)); + + intent.putExtra(Constants.RESPONSE_CODE, 0); + + checkIntent(launcher, requestCode, resultCode, intent, -1, null); + } + + @Test + public void intentWithStringResponseCode() { + PurchaseFlowLauncher launcher = new PurchaseFlowLauncher(mBillingContext, Constants.TYPE_IN_APP); + int requestCode = 0; + int resultCode = Activity.RESULT_OK; + Intent intent = PurchaseFlowOnActivityResultTest.newIntent( + Constants.TEST_JSON_RECEIPT, Security.signData(Constants.TEST_JSON_RECEIPT)); + + intent.putExtra(Constants.RESPONSE_CODE, "0"); + + checkIntent(launcher, + requestCode, + resultCode, + intent, + Constants.ERROR_UNEXPECTED_TYPE, + Constants.ERROR_MSG_UNEXPECTED_BUNDLE_RESPONSE); + } + + @Test + public void intentWithNoResponseCode() { + PurchaseFlowLauncher launcher = new PurchaseFlowLauncher(mBillingContext, Constants.TYPE_IN_APP); + int requestCode = 0; + int resultCode = Activity.RESULT_OK; + Intent intent = PurchaseFlowOnActivityResultTest.newIntent( + Constants.TEST_JSON_RECEIPT, Security.signData(Constants.TEST_JSON_RECEIPT)); + + checkIntent(launcher, requestCode, resultCode, intent, -1, null); + } + + private void checkIntent(PurchaseFlowLauncher launcher, + int requestCode, + int resultCode, + Intent intent, + int errorCode, + String errorMessage) { + + Purchase purchase = null; + try { + purchase = launcher.handleResult(requestCode, resultCode, intent); + } catch (BillingException e) { + assertThat(e.getErrorCode()).isEqualTo(errorCode); + assertThat(e.getMessage()).isEqualTo(errorMessage); + } finally { + if (errorCode == -1) { + assertThat(purchase).isNotNull(); + } + } + } +} \ No newline at end of file diff --git a/library/src/test/java/jp/alessandro/android/iab/PurchaseGetterTest.java b/library/src/test/java/jp/alessandro/android/iab/PurchaseGetterTest.java new file mode 100644 index 0000000..98e1e2c --- /dev/null +++ b/library/src/test/java/jp/alessandro/android/iab/PurchaseGetterTest.java @@ -0,0 +1,388 @@ +/* + * Copyright (C) 2016 Alessandro Yuichi Okimoto + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * Contact email: alessandro@alessandro.jp + */ + +package jp.alessandro.android.iab; + +import android.os.Bundle; +import android.os.RemoteException; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Java6Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +/** + * Created by Alessandro Yuichi Okimoto on 2017/02/19. + */ + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class PurchaseGetterTest { + + @Rule + public MockitoRule mMockitoRule = MockitoJUnit.rule(); + + @Mock + BillingService mService; + + private final BillingContext mBillingContext = Util.newBillingContext(RuntimeEnvironment.application); + + private PurchaseGetter mGetter; + + @Before + public void setUp() { + mGetter = new PurchaseGetter(mBillingContext); + } + + @Test + public void remoteException() throws RemoteException { + Mockito.when(mService.getPurchases( + mBillingContext.getApiVersion(), + mBillingContext.getContext().getPackageName(), + Constants.TYPE_IN_APP, + null + )).thenThrow(RemoteException.class); + + Purchases purchases = null; + try { + purchases = mGetter.get(mService, Constants.TYPE_IN_APP); + } catch (BillingException e) { + assertThat(e.getErrorCode()).isEqualTo(Constants.ERROR_REMOTE_EXCEPTION); + } finally { + assertThat(purchases).isNull(); + } + } + + @Test + public void getWithDifferentSizes() throws RemoteException { + Bundle bundle = new Bundle(); + bundle.putLong(Constants.RESPONSE_CODE, 0L); + bundle.putStringArrayList(Constants.RESPONSE_INAPP_PURCHASE_LIST, Util.createPurchaseJsonArray(0, 5)); + bundle.putStringArrayList(Constants.RESPONSE_INAPP_SIGNATURE_LIST, new ArrayList()); + + getPurchases(bundle, Constants.ERROR_PURCHASE_DATA, Constants.ERROR_MSG_GET_PURCHASES_DIFFERENT_SIZE); + + Mockito.when(mService.getPurchases( + mBillingContext.getApiVersion(), + mBillingContext.getContext().getPackageName(), + Constants.TYPE_IN_APP, + null + )).thenReturn(bundle); + } + + @Test + public void getWithPurchasesAndSignaturesEmpty() throws RemoteException { + Bundle bundle = Util.createPurchaseBundle(0, 0, 0, null); + + Mockito.when(mService.getPurchases( + mBillingContext.getApiVersion(), + mBillingContext.getContext().getPackageName(), + Constants.TYPE_IN_APP, + null + )).thenReturn(bundle); + + Purchases purchases = null; + try { + purchases = mGetter.get(mService, Constants.TYPE_IN_APP); + } catch (BillingException e) { + + } finally { + assertThat(purchases).isNotNull(); + assertThat(purchases.getSize()).isZero(); + } + } + + @Test + public void getWithPurchasesAndSignaturesNull() throws RemoteException { + Bundle bundle = new Bundle(); + bundle.putLong(Constants.RESPONSE_CODE, 0L); + + Mockito.when(mService.getPurchases( + mBillingContext.getApiVersion(), + mBillingContext.getContext().getPackageName(), + Constants.TYPE_IN_APP, + null + )).thenReturn(bundle); + + Purchases purchases = null; + try { + purchases = mGetter.get(mService, Constants.TYPE_IN_APP); + } catch (BillingException e) { + assertThat(e.getErrorCode()).isEqualTo(Constants.ERROR_PURCHASE_DATA); + assertThat(e.getMessage()).isEqualTo(Constants.ERROR_MSG_GET_PURCHASES_DATA_LIST); + } finally { + assertThat(purchases).isNull(); + } + } + + @Test + public void getWithPurchasesNull() throws RemoteException { + Bundle bundle = new Bundle(); + bundle.putLong(Constants.RESPONSE_CODE, 0L); + bundle.putStringArrayList(Constants.RESPONSE_INAPP_SIGNATURE_LIST, new ArrayList()); + + Mockito.when(mService.getPurchases( + mBillingContext.getApiVersion(), + mBillingContext.getContext().getPackageName(), + Constants.TYPE_IN_APP, + null + )).thenReturn(bundle); + + Purchases purchases = null; + try { + purchases = mGetter.get(mService, Constants.TYPE_IN_APP); + } catch (BillingException e) { + assertThat(e.getErrorCode()).isEqualTo(Constants.ERROR_PURCHASE_DATA); + assertThat(e.getMessage()).isEqualTo(Constants.ERROR_MSG_GET_PURCHASES_DATA_LIST); + } finally { + assertThat(purchases).isNull(); + } + } + + @Test + public void getWithSignaturesNull() throws RemoteException { + Bundle bundle = new Bundle(); + bundle.putLong(Constants.RESPONSE_CODE, 0L); + bundle.putStringArrayList(Constants.RESPONSE_INAPP_PURCHASE_LIST, new ArrayList()); + + Mockito.when(mService.getPurchases( + mBillingContext.getApiVersion(), + mBillingContext.getContext().getPackageName(), + Constants.TYPE_IN_APP, + null + )).thenReturn(bundle); + + Purchases purchases = null; + try { + purchases = mGetter.get(mService, Constants.TYPE_IN_APP); + } catch (BillingException e) { + assertThat(e.getErrorCode()).isEqualTo(Constants.ERROR_PURCHASE_DATA); + assertThat(e.getMessage()).isEqualTo(Constants.ERROR_MSG_GET_PURCHASES_SIGNATURE_LIST); + } finally { + assertThat(purchases).isNull(); + } + } + + @Test + public void bundleResponseNull() throws RemoteException { + try { + mGetter.get(mService, Constants.TYPE_IN_APP); + } catch (BillingException e) { + assertThat(e.getErrorCode()).isEqualTo(Constants.ERROR_UNEXPECTED_TYPE); + assertThat(e.getMessage()).isEqualTo(Constants.ERROR_MSG_UNEXPECTED_BUNDLE_RESPONSE_NULL); + } finally { + verify(mService).getPurchases( + mBillingContext.getApiVersion(), + mBillingContext.getContext().getPackageName(), + Constants.TYPE_IN_APP, + null + ); + verifyNoMoreInteractions(mService); + } + } + + @Test + public void getWithValidSignatures() throws RemoteException, BillingException { + int size = 10; + Bundle bundle = Util.createPurchaseBundle(0, 0, size, null); + + Mockito.when(mService.getPurchases( + mBillingContext.getApiVersion(), + mBillingContext.getContext().getPackageName(), + Constants.TYPE_IN_APP, + null + )).thenReturn(bundle); + + Purchases purchases = null; + try { + purchases = mGetter.get(mService, Constants.TYPE_IN_APP); + } finally { + assertThat(purchases).isNotNull(); + assertThat(purchases.getSize()).isEqualTo(size); + assertThat(purchases.getAll()).isNotNull(); + + List purchaseList = purchases.getAll(); + for (Purchase p : purchaseList) { + assertThat(purchases.hasItemId(p.getSku())).isTrue(); + assertThat(purchases.getByPurchaseId(p.getSku())).isNotNull(); + } + } + } + + @Test + public void getWithValidSignatureUsingContinuationToken() throws RemoteException, BillingException { + String continuationString = "continuation_token"; + Bundle bundle = Util.createPurchaseBundle(0, 0, 10, continuationString); + Bundle bundle2 = Util.createPurchaseBundle(0, 10, 10, null); + + Mockito.when(mService.getPurchases( + mBillingContext.getApiVersion(), + mBillingContext.getContext().getPackageName(), + Constants.TYPE_IN_APP, + null + )).thenReturn(bundle); + + Mockito.when(mService.getPurchases( + mBillingContext.getApiVersion(), + mBillingContext.getContext().getPackageName(), + Constants.TYPE_IN_APP, + continuationString + )).thenReturn(bundle2); + + Purchases purchases = null; + try { + purchases = mGetter.get(mService, Constants.TYPE_IN_APP); + } finally { + assertThat(purchases).isNotNull(); + assertThat(purchases.getSize()).isEqualTo(20); + } + } + + @Test + public void getWithInvalidSignatures() throws RemoteException, BillingException { + ArrayList purchaseArray = Util.createPurchaseJsonArray(0, 5); + Bundle bundle = new Bundle(); + bundle.putLong(Constants.RESPONSE_CODE, 0L); + bundle.putStringArrayList(Constants.RESPONSE_INAPP_PURCHASE_LIST, purchaseArray); + bundle.putStringArrayList(Constants.RESPONSE_INAPP_SIGNATURE_LIST, Util.createInvalidSignatureRandomlyArray(purchaseArray)); + + Mockito.when(mService.getPurchases( + mBillingContext.getApiVersion(), + mBillingContext.getContext().getPackageName(), + Constants.TYPE_IN_APP, + null + )).thenReturn(bundle); + + Purchases purchases = null; + try { + purchases = mGetter.get(mService, Constants.TYPE_IN_APP); + } catch (BillingException e) { + assertThat(e.getErrorCode()).isEqualTo(Constants.ERROR_PURCHASE_DATA); + assertThat(e.getMessage()).isEqualTo(Constants.ERROR_MSG_GET_PURCHASE_VERIFICATION_FAILED); + } finally { + assertThat(purchases).isNull(); + } + } + + @Test + public void getWithJsonDataBroken() throws RemoteException { + ArrayList purchaseArray = Util.createPurchaseJsonBrokenArray(); + Bundle bundle = new Bundle(); + bundle.putLong(Constants.RESPONSE_CODE, 0L); + bundle.putStringArrayList(Constants.RESPONSE_INAPP_PURCHASE_LIST, purchaseArray); + bundle.putStringArrayList(Constants.RESPONSE_INAPP_SIGNATURE_LIST, Util.createSignatureArray(purchaseArray)); + + Mockito.when(mService.getPurchases( + mBillingContext.getApiVersion(), + mBillingContext.getContext().getPackageName(), + Constants.TYPE_IN_APP, + null + )).thenReturn(bundle); + + Purchases purchases = null; + try { + purchases = mGetter.get(mService, Constants.TYPE_IN_APP); + } catch (BillingException e) { + assertThat(e.getErrorCode()).isEqualTo(Constants.ERROR_BAD_RESPONSE); + assertThat(e.getMessage()).isEqualTo(Constants.ERROR_MSG_BAD_RESPONSE); + } finally { + assertThat(purchases).isNull(); + } + } + + @Test + public void getWithLongResponseCode() throws RemoteException { + Bundle bundle = Util.createPurchaseBundle(0, 0, 0, null); + bundle.putLong(Constants.RESPONSE_CODE, 0L); + + getPurchases(bundle, -1, ""); + } + + @Test + public void getWithDifferentResponseCode() throws RemoteException { + Bundle bundle = Util.createPurchaseBundle(3, 0, 0, null); + + getPurchases(bundle, 3, Constants.ERROR_MSG_GET_PURCHASES); + } + + @Test + public void getWithIntegerResponseCode() throws RemoteException { + Bundle bundle = Util.createPurchaseBundle(0, 0, 0, null); + + getPurchases(bundle, -1, ""); + } + + @Test + public void getWithNoResponseCode() throws RemoteException { + Bundle bundle = Util.createPurchaseBundle(0, 0, 0, null); + + getPurchases(bundle, -1, ""); + } + + @Test + public void stringResponseCode() throws InterruptedException, RemoteException { + Bundle bundle = new Bundle(); + bundle.putString(Constants.RESPONSE_CODE, "0"); + + getPurchases(bundle, Constants.ERROR_UNEXPECTED_TYPE, Constants.ERROR_MSG_UNEXPECTED_BUNDLE_RESPONSE); + } + + private void getPurchases(Bundle bundle, int errorCode, String errorMessage) throws RemoteException { + Mockito.when(mService.getPurchases( + mBillingContext.getApiVersion(), + mBillingContext.getContext().getPackageName(), + Constants.TYPE_IN_APP, + null + )).thenReturn(bundle); + + Purchases purchases = null; + try { + purchases = mGetter.get(mService, Constants.TYPE_IN_APP); + } catch (BillingException e) { + assertThat(e.getErrorCode()).isEqualTo(errorCode); + assertThat(e.getMessage()).isEqualTo(errorMessage); + } finally { + if (errorCode == -1) { + assertThat(purchases).isNotNull(); + } else { + assertThat(purchases).isNull(); + } + verify(mService).getPurchases( + mBillingContext.getApiVersion(), + mBillingContext.getContext().getPackageName(), + Constants.TYPE_IN_APP, + null + ); + verifyNoMoreInteractions(mService); + } + } +} \ No newline at end of file diff --git a/library/src/test/java/jp/alessandro/android/iab/PurchaseParcelableTest.java b/library/src/test/java/jp/alessandro/android/iab/PurchaseParcelableTest.java new file mode 100644 index 0000000..12aafeb --- /dev/null +++ b/library/src/test/java/jp/alessandro/android/iab/PurchaseParcelableTest.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2016 Alessandro Yuichi Okimoto + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * Contact email: alessandro@alessandro.jp + */ + +package jp.alessandro.android.iab; + +import android.os.Parcel; + +import org.json.JSONException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import static org.assertj.core.api.Java6Assertions.assertThat; + +/** + * Created by Alessandro Yuichi Okimoto on 2017/02/19. + */ + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE, constants = BuildConfig.class) +public class PurchaseParcelableTest { + + @Test + public void writeToParcel() throws JSONException { + Purchase purchase = Purchase.parseJson(Constants.TEST_JSON_RECEIPT, Security.signData(Constants.TEST_JSON_RECEIPT)); + + // Obtain a Parcel object and write the parcelable object to it + Parcel parcel = Parcel.obtain(); + purchase.writeToParcel(parcel, purchase.describeContents()); + + // After you're done with writing, you need to reset the parcel for reading + parcel.setDataPosition(0); + + Purchase fromParcel = Purchase.CREATOR.createFromParcel(parcel); + + assertThat(purchase.getOriginalJson()).isEqualTo(fromParcel.getOriginalJson()); + assertThat(purchase.getOrderId()).isEqualTo(fromParcel.getOrderId()); + assertThat(purchase.getPackageName()).isEqualTo(fromParcel.getPackageName()); + assertThat(purchase.getSku()).isEqualTo(fromParcel.getSku()); + assertThat(purchase.getPurchaseTime()).isEqualTo(fromParcel.getPurchaseTime()); + assertThat(purchase.getPurchaseState()).isEqualTo(fromParcel.getPurchaseState()); + assertThat(purchase.getDeveloperPayload()).isEqualTo(fromParcel.getDeveloperPayload()); + assertThat(purchase.getToken()).isEqualTo(fromParcel.getToken()); + assertThat(purchase.isAutoRenewing()).isEqualTo(fromParcel.isAutoRenewing()); + assertThat(purchase.getSignature()).isEqualTo(fromParcel.getSignature()); + } + + @Test + public void newArray() { + Purchase[] items = Purchase.CREATOR.newArray(10); + assertThat(items.length).isEqualTo(10); + } +} \ No newline at end of file diff --git a/library/src/test/java/jp/alessandro/android/iab/ReleaseTest.java b/library/src/test/java/jp/alessandro/android/iab/ReleaseTest.java new file mode 100644 index 0000000..124de36 --- /dev/null +++ b/library/src/test/java/jp/alessandro/android/iab/ReleaseTest.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2016 Alessandro Yuichi Okimoto + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * Contact email: alessandro@alessandro.jp + */ + +package jp.alessandro.android.iab; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Java6Assertions.assertThat; + +/** + * Created by Alessandro Yuichi Okimoto on 2017/02/19. + */ + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE, constants = BuildConfig.class) +public class ReleaseTest { + + private final BillingContext mContext = Util.newBillingContext(RuntimeEnvironment.application); + + private BillingProcessor mProcessor; + + @Before + public void setUp() { + mProcessor = new BillingProcessor(mContext, null); + } + + @Test + public void releaseAndGetPurchases() { + mProcessor.release(); + try { + mProcessor.getPurchases(PurchaseType.IN_APP, null); + } catch (IllegalStateException e) { + assertThat(e.getMessage()).isEqualTo(Constants.ERROR_MSG_LIBRARY_ALREADY_RELEASED); + } + } + + @Test + @Deprecated + public void releaseAndGetInventory() { + mProcessor.release(); + try { + mProcessor.getInventory(PurchaseType.IN_APP, null); + } catch (IllegalStateException e) { + assertThat(e.getMessage()).isEqualTo(Constants.ERROR_MSG_LIBRARY_ALREADY_RELEASED); + } + } + + @Test + public void releaseAndGetItemDetails() { + mProcessor.release(); + try { + mProcessor.getItemDetails(PurchaseType.IN_APP, null, null); + } catch (IllegalStateException e) { + assertThat(e.getMessage()).isEqualTo(Constants.ERROR_MSG_LIBRARY_ALREADY_RELEASED); + } + } + + @Test + public void releaseAndConsume() { + mProcessor.release(); + try { + mProcessor.consume(null, null); + } catch (IllegalStateException e) { + assertThat(e.getMessage()).isEqualTo(Constants.ERROR_MSG_LIBRARY_ALREADY_RELEASED); + } + } + + @Test + public void releaseAndStartPurchase() { + mProcessor.release(); + try { + mProcessor.startPurchase(null, 0, null, null, null, null); + } catch (IllegalStateException e) { + assertThat(e.getMessage()).isEqualTo(Constants.ERROR_MSG_LIBRARY_ALREADY_RELEASED); + } + } + + @Test + public void releaseAndUpdateSubscription() { + mProcessor.release(); + try { + List oldIds = new ArrayList<>(); + oldIds.add(Constants.TEST_PRODUCT_ID); + + mProcessor.updateSubscription(null, 0, oldIds, null, null, null); + } catch (IllegalStateException e) { + assertThat(e.getMessage()).isEqualTo(Constants.ERROR_MSG_LIBRARY_ALREADY_RELEASED); + } + } + + @Test + public void releaseAndCheckOnActivityResult() { + mProcessor.release(); + try { + mProcessor.onActivityResult(0, 0, null); + } catch (IllegalStateException e) { + assertThat(e.getMessage()).isEqualTo(Constants.ERROR_MSG_LIBRARY_ALREADY_RELEASED); + } + } +} \ No newline at end of file diff --git a/library/src/test/java/jp/alessandro/android/iab/ServiceTest.java b/library/src/test/java/jp/alessandro/android/iab/ServiceTest.java new file mode 100644 index 0000000..fddc73d --- /dev/null +++ b/library/src/test/java/jp/alessandro/android/iab/ServiceTest.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2016 Alessandro Yuichi Okimoto + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * Contact email: alessandro@alessandro.jp + */ + +package jp.alessandro.android.iab; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.RemoteException; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Created by Alessandro Yuichi Okimoto on 2017/02/19. + */ + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE, constants = BuildConfig.class) +public class ServiceTest { + + private final BillingContext mContext = Util.newBillingContext(mock(Context.class)); + + @Rule + public MockitoRule mMockitoRule = MockitoJUnit.rule(); + + @Mock + BillingService mService; + @Mock + ServiceBinder mServiceBinder; + @Mock + Activity mActivity; + + @Test + @SuppressWarnings("unchecked") + public void failedToBind() throws InterruptedException, RemoteException { + final CountDownLatch latch = new CountDownLatch(1); + Intent intent = new Intent(Constants.ACTION_BILLING_SERVICE_BIND); + intent.setPackage(Constants.VENDING_PACKAGE); + ServiceBinder conn = new ServiceBinder(mContext, intent); + + when(mContext.getContext().bindService( + any(Intent.class), + any(ServiceConnection.class), + eq(Context.BIND_AUTO_CREATE)) + ).thenReturn(false); + + conn.getServiceAsync(new ServiceBinder.Handler() { + @Override + public void onBind(BillingService service) { + throw new IllegalStateException(); + } + + @Override + public void onError(BillingException e) { + latch.countDown(); + } + }); + latch.await(15, TimeUnit.SECONDS); + } + + @Test + public void failedToBindNullPointer() throws InterruptedException, RemoteException { + final CountDownLatch latch = new CountDownLatch(1); + Intent intent = new Intent(Constants.ACTION_BILLING_SERVICE_BIND); + intent.setPackage(Constants.VENDING_PACKAGE); + ServiceBinder conn = new ServiceBinder(mContext, intent); + + when(mContext.getContext().bindService( + any(Intent.class), + any(ServiceConnection.class), + eq(Context.BIND_AUTO_CREATE)) + ).thenThrow(NullPointerException.class); + + conn.getServiceAsync(new ServiceBinder.Handler() { + @Override + public void onBind(BillingService service) { + throw new IllegalStateException(); + } + + @Override + public void onError(BillingException e) { + latch.countDown(); + } + }); + latch.await(15, TimeUnit.SECONDS); + } + + @Test + public void failedToBindIllegalArgument() throws InterruptedException, RemoteException { + final CountDownLatch latch = new CountDownLatch(1); + Intent intent = new Intent(Constants.ACTION_BILLING_SERVICE_BIND); + intent.setPackage(Constants.VENDING_PACKAGE); + ServiceBinder conn = new ServiceBinder(mContext, intent); + + when(mContext.getContext().bindService( + any(Intent.class), + any(ServiceConnection.class), + eq(Context.BIND_AUTO_CREATE)) + ).thenThrow(IllegalArgumentException.class); + + conn.getServiceAsync(new ServiceBinder.Handler() { + @Override + public void onBind(BillingService service) { + throw new IllegalStateException(); + } + + @Override + public void onError(BillingException e) { + latch.countDown(); + } + }); + latch.await(15, TimeUnit.SECONDS); + } + + @Test + public void onServiceConnectedServiceNull() throws InterruptedException, RemoteException { + final CountDownLatch latch = new CountDownLatch(1); + Intent intent = new Intent(Constants.ACTION_BILLING_SERVICE_BIND); + intent.setPackage(Constants.VENDING_PACKAGE); + ServiceBinder conn = new ServiceBinder(mContext, intent); + + when(mContext.getContext().bindService( + any(Intent.class), + any(ServiceConnection.class), + eq(Context.BIND_AUTO_CREATE)) + ).thenReturn(true); + + conn.getServiceAsync(new ServiceBinder.Handler() { + @Override + public void onBind(BillingService service) { + throw new IllegalStateException(); + } + + @Override + public void onError(BillingException e) { + latch.countDown(); + } + }); + conn.onServiceConnected(null, null); + + latch.await(15, TimeUnit.SECONDS); + } +} \ No newline at end of file diff --git a/library/src/test/java/jp/alessandro/android/iab/StartActivityTest.java b/library/src/test/java/jp/alessandro/android/iab/StartActivityTest.java new file mode 100644 index 0000000..efebfd2 --- /dev/null +++ b/library/src/test/java/jp/alessandro/android/iab/StartActivityTest.java @@ -0,0 +1,429 @@ +/* + * Copyright (C) 2016 Alessandro Yuichi Okimoto + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * Contact email: alessandro@alessandro.jp + */ + +package jp.alessandro.android.iab; + +import android.app.Activity; +import android.app.PendingIntent; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.RemoteException; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.mockito.stubbing.Answer; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import jp.alessandro.android.iab.handler.StartActivityHandler; + +import static org.assertj.core.api.Java6Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Created by Alessandro Yuichi Okimoto on 2017/02/19. + */ + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE, constants = BuildConfig.class) +public class StartActivityTest { + + private Handler mWorkHandler; + private Handler mMainHandler; + private BillingProcessor mProcessor; + + private final BillingContext mContext = Util.newBillingContext(RuntimeEnvironment.application); + + @Rule + public MockitoRule mMockitoRule = MockitoJUnit.rule(); + + @Mock + BillingService mService; + @Mock + ServiceBinder mServiceBinder; + @Mock + Activity mActivity; + + @Before + public void setUp() { + HandlerThread thread = new HandlerThread("AndroidEasyCheckoutThread"); + thread.start(); + // Handler to post all actions in the library + mWorkHandler = new Handler(thread.getLooper()); + // Handler to post all events in the library + mMainHandler = new Handler(Looper.getMainLooper()); + } + + @Test + public void startActivitySubscriptionSuccess() throws InterruptedException, RemoteException { + startActivitySuccess(PurchaseType.SUBSCRIPTION); + } + + @Test + public void startActivityInAppSuccess() throws InterruptedException, RemoteException { + startActivitySuccess(PurchaseType.IN_APP); + } + + @Test + public void startActivityInAppBillingNotSupported() throws InterruptedException, RemoteException { + startActivityBillingNotSupported(PurchaseType.SUBSCRIPTION); + } + + @Test + public void startActivitySubscriptionBillingNotSupported() throws InterruptedException, RemoteException { + startActivityBillingNotSupported(PurchaseType.IN_APP); + } + + @Test + public void startActivityInAppRemoteException() throws InterruptedException, RemoteException { + startActivityRemoteException(PurchaseType.SUBSCRIPTION); + } + + @Test + public void startActivitySubscriptionRemoteException() throws InterruptedException, RemoteException { + startActivityRemoteException(PurchaseType.IN_APP); + } + + @Test + public void startPurchaseInAppTwiceWithSameRequestCode() throws InterruptedException, RemoteException { + startPurchaseTwiceWithSameRequestCode(PurchaseType.SUBSCRIPTION); + } + + @Test + public void startPurchaseSubscriptionTwiceWithSameRequestCode() throws InterruptedException, RemoteException { + startPurchaseTwiceWithSameRequestCode(PurchaseType.IN_APP); + } + + @Test + public void startPurchaseInAppTwiceWithDifferentRequestCode() throws InterruptedException, RemoteException { + startPurchaseTwiceWithDifferentRequestCode(PurchaseType.SUBSCRIPTION); + } + + @Test + public void startPurchaseSubscriptionTwiceWithDifferentRequestCode() throws InterruptedException, RemoteException { + startPurchaseTwiceWithDifferentRequestCode(PurchaseType.IN_APP); + } + + @Test + @SuppressWarnings("checkstyle:methodlength") + public void startActivityUpdateSubscriptionSuccess() throws InterruptedException, RemoteException { + final CountDownLatch latch = new CountDownLatch(1); + final int requestCode = 1001; + final List oldItemIds = new ArrayList<>(); + oldItemIds.add(Constants.TEST_PRODUCT_ID); + + PendingIntent pendingIntent = PendingIntent.getActivity(mContext.getContext(), 1, new Intent(), 0); + Bundle bundle = new Bundle(); + bundle.putLong(Constants.RESPONSE_CODE, 0L); + bundle.putParcelable(Constants.RESPONSE_BUY_INTENT, pendingIntent); + + when(mService.getBuyIntentToReplaceSkus( + BillingApi.VERSION_5.getValue(), + mContext.getContext().getPackageName(), + oldItemIds, + Constants.TEST_PRODUCT_ID, + Constants.TYPE_SUBSCRIPTION, + Constants.TEST_DEVELOPER_PAYLOAD + )).thenReturn(bundle); + + mProcessor = spy(new BillingProcessor(mContext, null)); + setUpProcessor(bundle, PurchaseType.SUBSCRIPTION); + + doReturn(mMainHandler).when(mProcessor).getMainHandler(); + + mProcessor.updateSubscription(mActivity, requestCode, oldItemIds, Constants.TEST_PRODUCT_ID, Constants.TEST_DEVELOPER_PAYLOAD, + new StartActivityHandler() { + @Override + public void onSuccess() { + try { + verify(mService, never()).getBuyIntent( + mContext.getApiVersion(), + mContext.getContext().getPackageName(), + Constants.TEST_PRODUCT_ID, + Constants.TYPE_SUBSCRIPTION, + Constants.TEST_DEVELOPER_PAYLOAD); + + verify(mService).getBuyIntentToReplaceSkus( + BillingApi.VERSION_5.getValue(), + mContext.getContext().getPackageName(), + oldItemIds, + Constants.TEST_PRODUCT_ID, + Constants.TYPE_SUBSCRIPTION, + Constants.TEST_DEVELOPER_PAYLOAD); + } catch (RemoteException err) { + } finally { + latch.countDown(); + } + } + + @Override + public void onError(BillingException e) { + throw new IllegalStateException(); + } + } + + ); + latch.await(15, TimeUnit.SECONDS); + } + + private void startActivitySuccess(final PurchaseType type) throws InterruptedException, RemoteException { + final CountDownLatch latch = new CountDownLatch(1); + final int requestCode = 1001; + + setUpStartPurchase(type); + mProcessor.startPurchase(mActivity, requestCode, Constants.TEST_PRODUCT_ID, type, Constants.TEST_DEVELOPER_PAYLOAD, + new StartActivityHandler() { + @Override + public void onSuccess() { + try { + verify(mService).getBuyIntent( + eq(mContext.getApiVersion()), + anyString(), + anyString(), + anyString(), + anyString()); + } catch (RemoteException e) { + } finally { + latch.countDown(); + } + } + + @Override + public void onError(BillingException e) { + throw new IllegalStateException(); + } + }); + latch.await(15, TimeUnit.SECONDS); + } + + @SuppressWarnings("checkstyle:methodlength") + private void startActivityBillingNotSupported(final PurchaseType type) throws + InterruptedException, RemoteException { + + final CountDownLatch latch = new CountDownLatch(1); + final int requestCode = 1001; + + setUpStartPurchase(type); + doReturn(false).when(mProcessor).isSupported(type, mService); + + mProcessor.startPurchase(mActivity, requestCode, Constants.TEST_PRODUCT_ID, type, Constants.TEST_DEVELOPER_PAYLOAD, + new StartActivityHandler() { + @Override + public void onSuccess() { + throw new IllegalStateException(); + } + + @Override + public void onError(BillingException e) { + if (type == PurchaseType.SUBSCRIPTION) { + assertThat(e.getErrorCode()).isEqualTo(Constants.ERROR_SUBSCRIPTIONS_NOT_SUPPORTED); + assertThat(e.getMessage()).isEqualTo(Constants.ERROR_MSG_SUBSCRIPTIONS_NOT_SUPPORTED); + } else { + assertThat(e.getErrorCode()).isEqualTo(Constants.ERROR_PURCHASES_NOT_SUPPORTED); + assertThat(e.getMessage()).isEqualTo(Constants.ERROR_MSG_PURCHASES_NOT_SUPPORTED); + } + try { + verify(mService, never()).getBuyIntent( + eq(mContext.getApiVersion()), + anyString(), + anyString(), + anyString(), + anyString()); + } catch (RemoteException err) { + } finally { + latch.countDown(); + } + } + }); + latch.await(15, TimeUnit.SECONDS); + } + + private void startActivityRemoteException(final PurchaseType type) throws InterruptedException, RemoteException { + final CountDownLatch latch = new CountDownLatch(1); + final int requestCode = 1001; + + setUpStartPurchase(type); + + when(mService.getBuyIntent( + eq(mContext.getApiVersion()), + anyString(), + anyString(), + anyString(), + anyString() + )).thenThrow(RemoteException.class); + + mProcessor.startPurchase(mActivity, requestCode, Constants.TEST_PRODUCT_ID, type, Constants.TEST_DEVELOPER_PAYLOAD, + new StartActivityHandler() { + @Override + public void onSuccess() { + throw new IllegalStateException(); + } + + @Override + public void onError(BillingException e) { + assertThat(e.getErrorCode()).isEqualTo(Constants.ERROR_REMOTE_EXCEPTION); + latch.countDown(); + } + }); + latch.await(15, TimeUnit.SECONDS); + } + + private void startPurchaseTwiceWithSameRequestCode(final PurchaseType type) + throws InterruptedException, RemoteException { + + final CountDownLatch latch = new CountDownLatch(1); + final int requestCode = 1001; + + setUpStartPurchase(type); + mProcessor.startPurchase(mActivity, requestCode, Constants.TEST_PRODUCT_ID, type, Constants.TEST_DEVELOPER_PAYLOAD, + new StartActivityHandler() { + @Override + public void onSuccess() { + callStartPurchaseSecondTime(latch, requestCode, false, type); + } + + @Override + public void onError(BillingException e) { + throw new IllegalStateException(); + } + } + ); + latch.await(15, TimeUnit.SECONDS); + } + + private void startPurchaseTwiceWithDifferentRequestCode(final PurchaseType type) + throws InterruptedException, RemoteException { + + final CountDownLatch latch = new CountDownLatch(1); + final int requestCode = 1001; + + setUpStartPurchase(type); + mProcessor.startPurchase(mActivity, requestCode, Constants.TEST_PRODUCT_ID, type, Constants.TEST_DEVELOPER_PAYLOAD, + new StartActivityHandler() { + @Override + public void onSuccess() { + callStartPurchaseSecondTime(latch, 1002, true, type); + } + + @Override + public void onError(BillingException e) { + throw new IllegalStateException(); + } + } + ); + latch.await(15, TimeUnit.SECONDS); + } + + private void callStartPurchaseSecondTime(final CountDownLatch latch, + final int requestCode, + final boolean differentRequestCode, + final PurchaseType type) { + + mProcessor.startPurchase(mActivity, requestCode, Constants.TEST_PRODUCT_ID, type, Constants.TEST_DEVELOPER_PAYLOAD, + new StartActivityHandler() { + @Override + public void onSuccess() { + if (!differentRequestCode) { + throw new IllegalStateException(); + } + latch.countDown(); + } + + @Override + public void onError(BillingException e) { + if (differentRequestCode) { + throw new IllegalStateException(); + } + assertThat(e.getErrorCode()).isEqualTo(Constants.ERROR_PURCHASE_FLOW_ALREADY_EXISTS); + assertThat(e.getMessage()).isEqualTo( + String.format(Locale.US, Constants.ERROR_MSG_PURCHASE_FLOW_ALREADY_EXISTS, requestCode)); + try { + verify(mService).getBuyIntent( + eq(mContext.getApiVersion()), + anyString(), + anyString(), + anyString(), + anyString()); + } catch (RemoteException err) { + } finally { + latch.countDown(); + } + } + } + ); + } + + private void setUpStartPurchase(PurchaseType type) throws RemoteException { + + PendingIntent pendingIntent = PendingIntent.getActivity(mContext.getContext(), 1, new Intent(), 0); + Bundle bundle = new Bundle(); + bundle.putLong(Constants.RESPONSE_CODE, 0L); + bundle.putParcelable(Constants.RESPONSE_BUY_INTENT, pendingIntent); + + mProcessor = spy(new BillingProcessor(mContext, null)); + + setUpProcessor(bundle, type); + + doReturn(mWorkHandler).when(mProcessor).getWorkHandler(); + } + + private void setUpProcessor(Bundle bundle, PurchaseType type) throws RemoteException { + doReturn(true).when(mProcessor).isSupported(type, mService); + doReturn(mServiceBinder).when(mProcessor).createServiceBinder(); + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + ServiceBinder.Handler handler = invocation.getArgument(0); + handler.onBind(mService); + return null; + } + }).when(mServiceBinder).getServiceAsync(any(ServiceBinder.Handler.class)); + + when(mService.getBuyIntent( + eq(mContext.getApiVersion()), + anyString(), + anyString(), + anyString(), + anyString() + )).thenReturn(bundle); + } +} \ No newline at end of file diff --git a/rsa/openssl_command.txt b/rsa/openssl_command.txt new file mode 100644 index 0000000..5a801d7 --- /dev/null +++ b/rsa/openssl_command.txt @@ -0,0 +1,8 @@ +echo '{"orderId":"GPA.1234-5678-9012-34567","packageName":"jp.alessandro.android.iab","productId":"android.test.purchased","purchaseTime":1345678900000,"purchaseState":0,"developerPayload":optional_developer_payload,"purchaseToken":"opaque-token-up-to-1000-characters","autoRenewing":true}' > receipt.json + +openssl genrsa -out private_key.pem 2048 +openssl rsa -in private_key.pem -pubout -outform DER -out public_key.der +openssl pkcs8 -in private_key.pem -topk8 -nocrypt -inform DER -outform DER -out private_key.pk8 + +openssl dgst -sha1 -sign private_key.pem -out receipt.signature receipt.json +openssl dgst -sha1 -verify public_key.der -keyform DER -signature receipt.signature receipt.json diff --git a/rsa/private_key.pem b/rsa/private_key.pem new file mode 100644 index 0000000..8f9d049 --- /dev/null +++ b/rsa/private_key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEA7SEtV7WT1vJKdS1fBgskYk+c8j6YUa6kz8NwLbD7EkKGh+0o +cSmsde4BewrQDijHC0z6Cxs3s8Kks2JC75NTZUvRQRN5T19Po2owTXTrkT5+Zh2n +t5/0lj7RnMyB6qYMeVebDh4oUmj4YkLdQ3QjOpLjGep1xjIunOvJrpMiNkQuRl3E +NBbkwEbDKzSquXXMngjfkx2PyHfirbE2dDVXkG85G542KSBfOHF1AQpEO7hiRgz8 +b5JTuSe4oOdYc11WG4bNxnLpcUeh8xwE9txcipDrz6cUFfb6D3lL8zPIzyZxiwIr +0+G0O7ise+vIMaP0JOA891eqruBVEI7WPCyT0QIDAQABAoIBAE1HWK2S4WFViOpz +JNqlWvAnHfDccWt9TPzgpnhdixVCVPGLWni2qhusuxLMTU2wAF4wcfSYpCiTMHW9 +ei71hmImuUVKAWjamOuaua8kgXjOMwc4duYi3OTyCAHfrB86iiopYMDTFzT0PK5Z +OB65hJmcMSLLBCLZS9OcDBg2nxmrsfDlQZS6kTCBgEI2ioxnOukoBcZIooqJVJUE +XL5u7M1wv8F4tvH+yTSJhV3h+RcIRweNX2xxe3h3Itv/T0QkyOqWsIoklSao94NA +O0zbmF0gfQLWXR19Yjq+9jd9z/zL1sJ2qRl5Cg5uk1/psul00GNKmXNTLT5RLlij +FFrIMHkCgYEA/w4TZAYWmPA6Tn5YJLJiRbphtqaPAivXhkK3lLPzaBNZysSLUAFS +46IVXfPmqE7ohtiFAl/T3uTmDgDozUOKMLxceSgbcupYRl2jjq0VG30bZ9z2E4A7 +HPp++QCJJ+lMmm2gxCGPBJsgvMK9z1/3xKHeb39SfpikjC59d4I9Sl8CgYEA7gIZ +THmDFwNLCclIcAijGunZwRKMSjYxw3JSw36T1X+O6IAPj9jE7ypzp+xH8Hy5olPy +qm2DPvCoCUcmqHzMlr6CAfO9zyNBv5f3fA1t6zL9SvMzy64JJ1KSgzLdKxc8ZUpu +bWBcL0k7HjlGSz7xCS3GFCn7n9rlpPNoHSPKL88CgYBQWHnJR5W0xfBIK8rOfJcy +if0gEaX5NCBnvfqg0HM79OSTWIjeQhx/ct6yQxQFLx5W5Dw6PD+89nR2MtkjWERf +B+dFj1neQG5gdD0CxAljKG0KsfOevwVgIpT/EakjNn4YI7LCNiQcelW8wMgUXJHr +kmZEz2IIWUN0mWySyidOlwKBgCcul0Wct3UBaMgKp+8xrNBQcTW0vP22oEihuHhQ +jTvXjQo/ktBGil0pKvMZFdrEXbcYhNmDv9iLu84TNY0FRpUGddambrf8AOXuuaJl +f5P5x/MfyIYed4lOsaoBpKFkaN/v+e/trh9mueHG4gifKwUs0PAe3Tq6yZV3MMuj +SbTHAoGAHTO4EfmlRV7HnatUBF76Z4ILmu9FT7uesRyfBgodc6w7vVLm5GUPWwe+ +JDWpM74RgZi0bPhq6esCyJuuguDIJpVG8Q5zlkC5Jh7wQ1Z/c+tERPzE8QAy6pRl +g5LlKUmRzdVknypUaY/1Xi1aQERFmAUl6o1mQObA/xihvJejkyY= +-----END RSA PRIVATE KEY----- diff --git a/rsa/private_key.pk8 b/rsa/private_key.pk8 new file mode 100644 index 0000000000000000000000000000000000000000..fe0853649bcfab846b85803e1e1f70dac37946a3 GIT binary patch literal 1216 zcmV;x1V8&Qf&{z*0RS)!1_>&LNQUrrZ9p8q5=T`0)hbn0PP_ySGAMY z@=A3rUj_>#Vo#j%KA2Iiq|d`}EwK9%LWYO!C~+yQb?yOs3eXNH#|upQ3mZ4H!lbie zLhqAPWlPaP6M0WxPoruuO?2y#K7M8$r?;QUG94E}ZMhu9G4*L@q{M#55MG^)9E%$^9}lO2!9cjB$FHgq*tkZ(B~ zo;E2UUpR4f0SZJrxMD^O{BM#|xhJ@w=U8)HRvU)R#&YR#N1^i^1oqrqijeEirxX?T z`VV&P*q^d#Us_gAW};8hTg);uhe(EY?_8LP4Ie0ngxbkJi;nR8PuK2a`MqZC@mFnI!jfdKyw z6J!P!nD9DIepn>3Vnw=Pwx*8)E7yiXx0JKR3iyqmHc=8+{vR-1ZZII~@9c`2dM0=}ek!pu{1M1e+kd z!oAO5_r#&zZ+}vLn52v@eRqOAN?!tjfdK9T8BBSD7XwQP$w+VrqZ;Yi!4iy0HZj9; zQp0|e)qjrYfDez@#P2F|r|d`Ye7T}i@~UlvKJcgsM<%Fz%$B}_0rS1jBSF8H_k0a) z>oWaH^E1n?2`5sLgEHMK7d&N3Zf#&(FG)KdIYvu9@d+))6e;_k+U2D4XdNTUFV6yj zfKXU@$w!s6#qdZg%FcY3GKu{l5vBPwAZNY$pwM$W^yHIRh~7dRe{$ZkLlgxs9#-T$ zIy^tT_H=eK*&|p)UkB$!k6GS8ZeVmh0>lYpC~XR{^Pay2U?Pylnm+jS4@K&_;?~DJt_B723pIw-|)kgTL5|yUr6ejRi)P26fh^ZnyjZC-8#C-m3J`9qe-;K z0)c=XGq@4?rA1!HovTy?UixQ(3!3jmPrIJ69G?aX9doQZy;A1nWe-~iz9coNGrkdl zn6zy8YU%3&$eXT$;K(MGM)3}FmO!~C9`Hj}e{<_ZMEu0@05a;7WrLFCDM^vd)nuP4 eRB4a(UM*TcL`9edCF+f4K<2>z7@@qEqmw3ou{vS^ literal 0 HcmV?d00001 diff --git a/rsa/public_key.der b/rsa/public_key.der new file mode 100644 index 0000000000000000000000000000000000000000..8080e993ce33025d2d3ba9689cf9471cb0238343 GIT binary patch literal 294 zcmV+>0ondAf&n5h4F(A+hDe6@4FLfG1potr0S^E$f&mHwf&l>l?IA5!wUgHJN_8z? z1`8x&Pn_~Tm{G2z&%-af<>g)8Vu`xU75Y$T6ezB;Y*vSE{byRS=HWJS>yZ0s{d60TX6_)c^nh literal 0 HcmV?d00001 diff --git a/rsa/receipt.json b/rsa/receipt.json new file mode 100644 index 0000000..2334b2c --- /dev/null +++ b/rsa/receipt.json @@ -0,0 +1 @@ +{"orderId":"GPA.1234-5678-9012-34567","packageName":"jp.alessandro.android.iab","productId":"android.test.purchased","purchaseTime":1345678900000,"purchaseState":0,"developerPayload":optional_developer_payload,"purchaseToken":"opaque-token-up-to-1000-characters","autoRenewing":true} From 0f56b046a03e5aea7686bce7b0776ee63d892ba5 Mon Sep 17 00:00:00 2001 From: Alessandro Yuichi Okimoto Date: Mon, 20 Feb 2017 17:48:21 +0900 Subject: [PATCH 4/4] Updating android api version --- .travis.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index b7f1715..07ff7e6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,6 +12,10 @@ env: - ANDROID_BUILD_TOOLS_VERSION=25.0.1 android: + licenses: + - 'android-sdk-license-.+' + - 'google-gdk-license-.+' + components: # Use the latest revision of Android SDK Tools - tools # to get the new `repository-11.xml` @@ -40,17 +44,21 @@ before_cache: before_install: - echo y | android update sdk --all --no-ui --force --filter build-tools-$ANDROID_BUILD_TOOLS_VERSION - echo y | android update sdk --all --no-ui --force --filter android-$ANDROID_API_LEVEL + - mkdir "$ANDROID_HOME/licenses" || true + - echo -e "\n8933bad161af4178b1185d1a37fbf41ea5269c55" > "$ANDROID_HOME/licenses/android-sdk-license" + - echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" > "$ANDROID_HOME/licenses/android-sdk-preview-license" before_script: # INSTRUMENTED TESTS ARE DISABLED - until Google or Travis fix the mess with emulation, only local tests are enabled # - echo no | android create avd --force -n test -t "android-"$ANDROID_API_LEVEL --abi $ANDROID_ABI --tag $ANDROID_TAG +# - echo no | android create avd --force -n test -t android-16 --abi $ANDROID_ABI # - emulator -avd test -no-skin -no-audio -no-window & # - android-wait-for-emulator # - adb shell input keyevent 82 & script: - - ./gradlew build jacocoTestReport assembleAndroidTest --stacktrace -# - ./gradlew connectedCheck --stacktrace +# - ./gradlew build connectedCheck jacocoTestReport --stacktrace + - ./gradlew build jacocoTestReport --stacktrace after_success: - bash <(curl -s https://codecov.io/bash)