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/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)
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/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..86db2cb 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;
- private Handler mWorkHandler;
+ protected static final String WORK_THREAD_NAME = "AndroidEasyCheckoutThread";
+ protected Handler mWorkHandler;
+
+ private final BillingContext mContext;
+ private final SparseArray mPurchaseFlows;
+ private final Logger mLogger;
+ private final Intent mServiceIntent;
+
+ private PurchaseHandler mPurchaseHandler;
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);
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 0000000..fe08536
Binary files /dev/null and b/rsa/private_key.pk8 differ
diff --git a/rsa/public_key.der b/rsa/public_key.der
new file mode 100644
index 0000000..8080e99
Binary files /dev/null and b/rsa/public_key.der differ
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}