diff --git a/billing-hack/.gitignore b/billing-hack/.gitignore
new file mode 100644
index 000000000..603b14077
--- /dev/null
+++ b/billing-hack/.gitignore
@@ -0,0 +1,14 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
diff --git a/billing-hack/.idea/.name b/billing-hack/.idea/.name
new file mode 100644
index 000000000..4883ade81
--- /dev/null
+++ b/billing-hack/.idea/.name
@@ -0,0 +1 @@
+BillingHack
\ No newline at end of file
diff --git a/billing-hack/.idea/androidTestResultsUserPreferences.xml b/billing-hack/.idea/androidTestResultsUserPreferences.xml
new file mode 100644
index 000000000..3d64d348d
--- /dev/null
+++ b/billing-hack/.idea/androidTestResultsUserPreferences.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/billing-hack/.idea/codeStyles/Project.xml b/billing-hack/.idea/codeStyles/Project.xml
new file mode 100644
index 000000000..681f41ae2
--- /dev/null
+++ b/billing-hack/.idea/codeStyles/Project.xml
@@ -0,0 +1,116 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ xmlns:android
+
+ ^$
+
+
+
+
+
+
+
+
+ xmlns:.*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*:id
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ .*:name
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ name
+
+ ^$
+
+
+
+
+
+
+
+
+ style
+
+ ^$
+
+
+
+
+
+
+
+
+ .*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*
+
+ http://schemas.android.com/apk/res/android
+
+
+ ANDROID_ATTRIBUTE_ORDER
+
+
+
+
+
+
+ .*
+
+ .*
+
+
+ BY_NAME
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/billing-hack/.idea/compiler.xml b/billing-hack/.idea/compiler.xml
new file mode 100644
index 000000000..b589d56e9
--- /dev/null
+++ b/billing-hack/.idea/compiler.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/billing-hack/.idea/gradle.xml b/billing-hack/.idea/gradle.xml
new file mode 100644
index 000000000..ae388c2a5
--- /dev/null
+++ b/billing-hack/.idea/gradle.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/billing-hack/.idea/jarRepositories.xml b/billing-hack/.idea/jarRepositories.xml
new file mode 100644
index 000000000..e34606ccd
--- /dev/null
+++ b/billing-hack/.idea/jarRepositories.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/billing-hack/.idea/kotlinc.xml b/billing-hack/.idea/kotlinc.xml
new file mode 100644
index 000000000..ae3f30ae1
--- /dev/null
+++ b/billing-hack/.idea/kotlinc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/billing-hack/.idea/misc.xml b/billing-hack/.idea/misc.xml
new file mode 100644
index 000000000..b31a4ac9c
--- /dev/null
+++ b/billing-hack/.idea/misc.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/billing-hack/.idea/vcs.xml b/billing-hack/.idea/vcs.xml
new file mode 100644
index 000000000..d4d270ad9
--- /dev/null
+++ b/billing-hack/.idea/vcs.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/billing-hack/.project b/billing-hack/.project
new file mode 100644
index 000000000..0047338f9
--- /dev/null
+++ b/billing-hack/.project
@@ -0,0 +1,28 @@
+
+
+ BillingHack
+ Project billing-hack created by Buildship.
+
+
+
+
+ org.eclipse.buildship.core.gradleprojectbuilder
+
+
+
+
+
+ org.eclipse.buildship.core.gradleprojectnature
+
+
+
+ 1701744431802
+
+ 30
+
+ org.eclipse.core.resources.regexFilterMatcher
+ node_modules|.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__
+
+
+
+
diff --git a/billing-hack/.settings/org.eclipse.buildship.core.prefs b/billing-hack/.settings/org.eclipse.buildship.core.prefs
new file mode 100644
index 000000000..cfa5c7b0d
--- /dev/null
+++ b/billing-hack/.settings/org.eclipse.buildship.core.prefs
@@ -0,0 +1,13 @@
+arguments=--init-script /home/kuhakupixel/.config/coc/extensions/coc-java-data/server/config_linux/org.eclipse.osgi/51/0/.cp/gradle/init/init.gradle
+auto.sync=false
+build.scans.enabled=false
+connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER)
+connection.project.dir=
+eclipse.preferences.version=1
+gradle.user.home=
+java.home=/usr/lib/jvm/java-17-openjdk-amd64
+jvm.arguments=
+offline.mode=false
+override.workspace.settings=true
+show.console.view=true
+show.executions.view=true
diff --git a/billing-hack/README.md b/billing-hack/README.md
new file mode 100644
index 000000000..ca36c6c72
--- /dev/null
+++ b/billing-hack/README.md
@@ -0,0 +1,19 @@
+# Billing Hack
+local android purchase server for supporting billing hack
+
+updated version of [billing-hack](https://github.com/dschuermann/billing-hack) licensed under [Apache 2](https://github.com/dschuermann/billing-hack/issues/1#issuecomment-67127884)
+
+so it will work on newer version of [billing client](https://developer.android.com/reference/com/android/billingclient/api/BillingClient)
+with improvements such as
+
+- Updated AIDL to be compatable with newer billing client's version
+
+- implement `getPurchases` to store history of purchases
+
+- set transaction code explicitly in AIDL to remain compatable with
+ newer version of `com.android.vending` (google playstore)
+
+
+## Reference
+
+
diff --git a/billing-hack/app/.classpath b/billing-hack/app/.classpath
new file mode 100644
index 000000000..bbe97e501
--- /dev/null
+++ b/billing-hack/app/.classpath
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/billing-hack/app/.gitignore b/billing-hack/app/.gitignore
new file mode 100644
index 000000000..796b96d1c
--- /dev/null
+++ b/billing-hack/app/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/billing-hack/app/.project b/billing-hack/app/.project
new file mode 100644
index 000000000..331a536e4
--- /dev/null
+++ b/billing-hack/app/.project
@@ -0,0 +1,34 @@
+
+
+ BillingHack-app
+ Project BillingHack-app created by Buildship.
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ org.eclipse.buildship.core.gradleprojectbuilder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+ org.eclipse.buildship.core.gradleprojectnature
+
+
+
+ 1701744431804
+
+ 30
+
+ org.eclipse.core.resources.regexFilterMatcher
+ node_modules|.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__
+
+
+
+
diff --git a/billing-hack/app/.settings/org.eclipse.buildship.core.prefs b/billing-hack/app/.settings/org.eclipse.buildship.core.prefs
new file mode 100644
index 000000000..b1886adb4
--- /dev/null
+++ b/billing-hack/app/.settings/org.eclipse.buildship.core.prefs
@@ -0,0 +1,2 @@
+connection.project.dir=..
+eclipse.preferences.version=1
diff --git a/billing-hack/app/build.gradle b/billing-hack/app/build.gradle
new file mode 100644
index 000000000..2d16666e0
--- /dev/null
+++ b/billing-hack/app/build.gradle
@@ -0,0 +1,48 @@
+apply plugin: 'com.android.application'
+apply plugin: 'org.jetbrains.kotlin.android'
+
+android {
+ compileSdkVersion 34
+
+
+ defaultConfig {
+ applicationId "org.billinghack"
+ minSdkVersion 19
+ targetSdkVersion 31
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ buildFeatures {
+ aidl true
+ }
+ namespace 'org.billinghack'
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
+ }
+ kotlinOptions {
+ jvmTarget = '17'
+ }
+}
+
+dependencies {
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
+
+ implementation 'androidx.appcompat:appcompat:1.6.1'
+ implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
+ implementation 'androidx.core:core-ktx:1.12.0'
+ testImplementation 'junit:junit:4.13.2'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.5'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
+ implementation 'com.google.code.gson:gson:2.10.1'
+}
diff --git a/billing-hack/app/proguard-rules.pro b/billing-hack/app/proguard-rules.pro
new file mode 100644
index 000000000..f1b424510
--- /dev/null
+++ b/billing-hack/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/billing-hack/app/src/androidTest/java/org/billinghack/BillingStorageTest.kt b/billing-hack/app/src/androidTest/java/org/billinghack/BillingStorageTest.kt
new file mode 100644
index 000000000..9d8230156
--- /dev/null
+++ b/billing-hack/app/src/androidTest/java/org/billinghack/BillingStorageTest.kt
@@ -0,0 +1,85 @@
+package org.billinghack
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.Assert
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.io.File
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see [Testing documentation](http://d.android.com/tools/testing)
+ */
+@RunWith(AndroidJUnit4::class)
+class BillingStorageTest {
+ val jsonTestFileName = "testFile.json"
+ val dummyPackageName = "com.myapp.dummy"
+
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ Assert.assertEquals("org.billinghack", appContext.packageName)
+ }
+
+ @Test
+ fun loadSaveTest() {
+ // Context of the app under test.
+ val context = InstrumentationRegistry.getInstrumentation().targetContext
+
+ // make sure we start clean
+ val internalStoragePath: File = context.filesDir
+ val jsonFile: File = File(internalStoragePath, jsonTestFileName)
+ jsonFile.delete()
+ // save data
+ val billingStorage = BillingStorage(context)
+ billingStorage.packageNameToPackagePurchaseDataMap.put(
+ key = dummyPackageName,
+ value = PackagePurchaseData(
+ inappPurchaseDataList = arrayListOf(
+ PurchaseDetail(
+ orderId = "orderId",
+ packageName = "com.example.testonly",
+ productId = "coin666",
+ purchaseTime = 1000,
+ purchaseToken = "somePurchaseToken",
+ purchaseState = PurchaseDetail.ITEM_BOUGHT,
+ developerPayload = null,
+ ),
+ ),
+ inappDataSignatureList = arrayListOf("myItemSignature"),
+ inappPurchaseItemList = arrayListOf("myItemSku"),
+ )
+ )
+ billingStorage.save(jsonTestFileName)
+ // load data again
+ val billingStorageLoaded = BillingStorage(context)
+ Assert.assertFalse(
+ billingStorageLoaded.packageNameToPackagePurchaseDataMap.containsKey(
+ dummyPackageName
+ )
+ )
+ billingStorageLoaded.load(jsonTestFileName)
+
+ Assert.assertTrue(
+ billingStorageLoaded.packageNameToPackagePurchaseDataMap.containsKey(
+ dummyPackageName
+ )
+ )
+ val purchaseData: PackagePurchaseData =
+ billingStorageLoaded.packageNameToPackagePurchaseDataMap.get(dummyPackageName)!!
+
+ Assert.assertEquals(1, purchaseData.inappPurchaseDataList.size)
+ Assert.assertEquals("coin666", purchaseData.inappPurchaseDataList[0].productId)
+
+ Assert.assertEquals(1, purchaseData.inappDataSignatureList.size)
+ Assert.assertEquals("myItemSignature", purchaseData.inappDataSignatureList[0])
+
+ Assert.assertEquals(1, purchaseData.inappPurchaseItemList.size)
+ Assert.assertEquals("myItemSku", purchaseData.inappPurchaseItemList[0])
+
+
+ }
+}
\ No newline at end of file
diff --git a/billing-hack/app/src/androidTest/java/org/billinghack/PackagePurchaseDataTest.kt b/billing-hack/app/src/androidTest/java/org/billinghack/PackagePurchaseDataTest.kt
new file mode 100644
index 000000000..e5577bdeb
--- /dev/null
+++ b/billing-hack/app/src/androidTest/java/org/billinghack/PackagePurchaseDataTest.kt
@@ -0,0 +1,62 @@
+package org.billinghack
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.gson.Gson
+import org.junit.Assert
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.io.File
+
+@RunWith(AndroidJUnit4::class)
+class PackagePurchaseDataTest {
+
+ @Test
+ fun loadSaveTest() {
+ val packagePurchaseData = PackagePurchaseData()
+ packagePurchaseData.addPurchase(
+ purchaseDetail = PurchaseDetail(
+ orderId = "orderId",
+ packageName = "com.example.testonly",
+ productId = "gem666",
+ purchaseTime = 1000,
+ purchaseToken = "somePurchaseToken",
+ purchaseState = PurchaseDetail.ITEM_BOUGHT,
+ developerPayload = null,
+ ),
+ purchaseSignature = "purchaseSignature"
+ )
+
+ packagePurchaseData.addPurchase(
+ purchaseDetail = PurchaseDetail(
+ orderId = "orderId",
+ packageName = "com.example.testonly",
+ productId = "coin666",
+ purchaseTime = 1000,
+ purchaseToken = "somePurchaseToken2",
+ purchaseState = PurchaseDetail.ITEM_BOUGHT,
+ developerPayload = null,
+ ),
+ purchaseSignature = "purchaseSignature"
+ )
+ // ==============================================
+ Assert.assertEquals(2, packagePurchaseData.inappPurchaseDataList.size)
+ Assert.assertEquals(2, packagePurchaseData.inappDataSignatureList.size)
+ Assert.assertEquals(2, packagePurchaseData.inappPurchaseItemList.size)
+ // ==============================================
+
+ Assert.assertEquals("gem666", packagePurchaseData.inappPurchaseItemList[0])
+
+ Assert.assertEquals("gem666", packagePurchaseData.inappPurchaseDataList[0].productId)
+ Assert.assertEquals(1000, packagePurchaseData.inappPurchaseDataList[0].purchaseTime)
+
+ // ================================= test remove =======================
+ packagePurchaseData.removePurchaseByPurchaseToken("somePurchaseToken")
+ Assert.assertEquals(1, packagePurchaseData.inappPurchaseDataList.size)
+ Assert.assertEquals(1, packagePurchaseData.inappDataSignatureList.size)
+ Assert.assertEquals(1, packagePurchaseData.inappPurchaseItemList.size)
+ Assert.assertEquals("coin666", packagePurchaseData.inappPurchaseItemList[0])
+
+
+ }
+}
\ No newline at end of file
diff --git a/billing-hack/app/src/main/AndroidManifest.xml b/billing-hack/app/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..ce9af97ed
--- /dev/null
+++ b/billing-hack/app/src/main/AndroidManifest.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/billing-hack/app/src/main/aidl/com/android/vending/billing/IInAppBillingService.aidl b/billing-hack/app/src/main/aidl/com/android/vending/billing/IInAppBillingService.aidl
new file mode 100644
index 000000000..59c135935
--- /dev/null
+++ b/billing-hack/app/src/main/aidl/com/android/vending/billing/IInAppBillingService.aidl
@@ -0,0 +1,292 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.vending.billing;
+
+import android.os.Bundle;
+
+/**
+ * InAppBillingService is the service that provides in-app billing version 3 and beyond.
+ * This service provides the following features:
+ * 1. Provides a new API to get details of in-app items published for the app including
+ * price, type, title and description.
+ * 2. The purchase flow is synchronous and purchase information is available immediately
+ * after it completes.
+ * 3. Purchase information of in-app purchases is maintained within the Google Play system
+ * till the purchase is consumed.
+ * 4. An API to consume a purchase of an inapp item. All purchases of one-time
+ * in-app items are consumable and thereafter can be purchased again.
+ * 5. An API to get current purchases of the user immediately. This will not contain any
+ * consumed purchases.
+ *
+ * All calls will give a response code with the following possible values
+ * RESULT_OK = 0 - success
+ * RESULT_USER_CANCELED = 1 - User pressed back or canceled a dialog
+ * RESULT_SERVICE_UNAVAILABLE = 2 - The network connection is down
+ * RESULT_BILLING_UNAVAILABLE = 3 - This billing API version is not supported for the type requested
+ * RESULT_ITEM_UNAVAILABLE = 4 - Requested SKU is not available for purchase
+ * RESULT_DEVELOPER_ERROR = 5 - Invalid arguments provided to the API
+ * RESULT_ERROR = 6 - Fatal error during the API action
+ * RESULT_ITEM_ALREADY_OWNED = 7 - Failure to purchase since item is already owned
+ * RESULT_ITEM_NOT_OWNED = 8 - Failure to consume since item is not owned
+ */
+interface IInAppBillingService {
+ /**
+ * Checks support for the requested billing API version, package and in-app type.
+ * Minimum API version supported by this interface is 3.
+ * @param apiVersion billing API version that the app is using
+ * @param packageName the package name of the calling app
+ * @param type type of the in-app item being purchased ("inapp" for one-time purchases
+ * and "subs" for subscriptions)
+ * @return RESULT_OK(0) on success and appropriate response code on failures.
+ */
+ int isBillingSupported(int apiVersion, String packageName, String type)=0;
+
+ /**
+ * Provides details of a list of SKUs
+ * Given a list of SKUs of a valid type in the skusBundle, this returns a bundle
+ * with a list JSON strings containing the productId, price, title and description.
+ * This API can be called with a maximum of 20 SKUs.
+ * @param apiVersion billing API version that the app is using
+ * @param packageName the package name of the calling app
+ * @param type of the in-app items ("inapp" for one-time purchases
+ * and "subs" for subscriptions)
+ * @param skusBundle bundle containing a StringArrayList of SKUs with key "ITEM_ID_LIST"
+ * @return Bundle containing the following key-value pairs
+ * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes
+ * on failures.
+ * "DETAILS_LIST" with a StringArrayList containing purchase information
+ * in JSON format similar to:
+ * '{ "productId" : "exampleSku",
+ * "type" : "inapp",
+ * "price" : "$5.00",
+ * "price_currency": "USD",
+ * "price_amount_micros": 5000000,
+ * "title : "Example Title",
+ * "description" : "This is an example description" }'
+ */
+ Bundle getSkuDetails(int apiVersion, String packageName, String type, in Bundle skusBundle)=1;
+
+ /**
+ * Returns a pending intent to launch the purchase flow for an in-app item by providing a SKU,
+ * the type, a unique purchase token and an optional developer payload.
+ * @param apiVersion billing API version that the app is using
+ * @param packageName package name of the calling app
+ * @param sku the SKU of the in-app item as published in the developer console
+ * @param type of the in-app item being purchased ("inapp" for one-time purchases
+ * and "subs" for subscriptions)
+ * @param developerPayload optional argument to be sent back with the purchase information
+ * @return Bundle containing the following key-value pairs
+ * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes
+ * on failures.
+ * "BUY_INTENT" - PendingIntent to start the purchase flow
+ *
+ * The Pending intent should be launched with startIntentSenderForResult. When purchase flow
+ * has completed, the onActivityResult() will give a resultCode of OK or CANCELED.
+ * If the purchase is successful, the result data will contain the following key-value pairs
+ * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response
+ * codes on failures.
+ * "INAPP_PURCHASE_DATA" - String in JSON format similar to
+ * '{"orderId":"12999763169054705758.1371079406387615",
+ * "packageName":"com.example.app",
+ * "productId":"exampleSku",
+ * "purchaseTime":1345678900000,
+ * "purchaseToken" : "122333444455555",
+ * "developerPayload":"example developer payload" }'
+ * "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that
+ * was signed with the private key of the developer
+ */
+ Bundle getBuyIntent(int apiVersion, String packageName, String sku, String type,
+ String developerPayload)=2;
+
+ /**
+ * Returns the current SKUs owned by the user of the type and package name specified along with
+ * purchase information and a signature of the data to be validated.
+ * This will return all SKUs that have been purchased in V3 and managed items purchased using
+ * V1 and V2 that have not been consumed.
+ * @param apiVersion billing API version that the app is using
+ * @param packageName package name of the calling app
+ * @param type of the in-app items being requested ("inapp" for one-time purchases
+ * and "subs" for subscriptions)
+ * @param continuationToken to be set as null for the first call, if the number of owned
+ * skus are too many, a continuationToken is returned in the response bundle.
+ * This method can be called again with the continuation token to get the next set of
+ * owned skus.
+ * @return Bundle containing the following key-value pairs
+ * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes
+ on failures.
+ * "INAPP_PURCHASE_ITEM_LIST" - StringArrayList containing the list of SKUs
+ * "INAPP_PURCHASE_DATA_LIST" - StringArrayList containing the purchase information
+ * "INAPP_DATA_SIGNATURE_LIST"- StringArrayList containing the signatures
+ * of the purchase information
+ * "INAPP_CONTINUATION_TOKEN" - String containing a continuation token for the
+ * next set of in-app purchases. Only set if the
+ * user has more owned skus than the current list.
+ */
+ Bundle getPurchases(int apiVersion, String packageName, String type, String continuationToken)=3;
+
+ /**
+ * Consume the last purchase of the given SKU. This will result in this item being removed
+ * from all subsequent responses to getPurchases() and allow re-purchase of this item.
+ * @param apiVersion billing API version that the app is using
+ * @param packageName package name of the calling app
+ * @param purchaseToken token in the purchase information JSON that identifies the purchase
+ * to be consumed
+ * @return RESULT_OK(0) if consumption succeeded, appropriate response codes on failures.
+ */
+ int consumePurchase(int apiVersion, String packageName, String purchaseToken)=4;
+
+ /**
+ * This API is currently under development.
+ */
+ int stub(int apiVersion, String packageName, String type)=5;
+
+ /**
+ * Returns a pending intent to launch the purchase flow for upgrading or downgrading a
+ * subscription. The existing owned SKU(s) should be provided along with the new SKU that
+ * the user is upgrading or downgrading to.
+ * @param apiVersion billing API version that the app is using, must be 5 or later
+ * @param packageName package name of the calling app
+ * @param oldSkus the SKU(s) that the user is upgrading or downgrading from,
+ * if null or empty this method will behave like {@link #getBuyIntent}
+ * @param newSku the SKU that the user is upgrading or downgrading to
+ * @param type of the item being purchased, currently must be "subs"
+ * @param developerPayload optional argument to be sent back with the purchase information
+ * @return Bundle containing the following key-value pairs
+ * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes
+ * on failures.
+ * "BUY_INTENT" - PendingIntent to start the purchase flow
+ *
+ * The Pending intent should be launched with startIntentSenderForResult. When purchase flow
+ * has completed, the onActivityResult() will give a resultCode of OK or CANCELED.
+ * If the purchase is successful, the result data will contain the following key-value pairs
+ * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response
+ * codes on failures.
+ * "INAPP_PURCHASE_DATA" - String in JSON format similar to
+ * '{"orderId":"12999763169054705758.1371079406387615",
+ * "packageName":"com.example.app",
+ * "productId":"exampleSku",
+ * "purchaseTime":1345678900000,
+ * "purchaseToken" : "122333444455555",
+ * "developerPayload":"example developer payload" }'
+ * "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that
+ * was signed with the private key of the developer
+ */
+ Bundle getBuyIntentToReplaceSkus(int apiVersion, String packageName,
+ in List oldSkus, String newSku, String type, String developerPayload)=6;
+
+ /**
+ * Returns a pending intent to launch the purchase flow for an in-app item. This method is
+ * a variant of the {@link #getBuyIntent} method and takes an additional {@code extraParams}
+ * parameter. This parameter is a Bundle of optional keys and values that affect the
+ * operation of the method.
+ * @param apiVersion billing API version that the app is using, must be 6 or later
+ * @param packageName package name of the calling app
+ * @param sku the SKU of the in-app item as published in the developer console
+ * @param type of the in-app item being purchased ("inapp" for one-time purchases
+ * and "subs" for subscriptions)
+ * @param developerPayload optional argument to be sent back with the purchase information
+ * @extraParams a Bundle with the following optional keys:
+ * "skusToReplace" - List - an optional list of SKUs that the user is
+ * upgrading or downgrading from.
+ * Pass this field if the purchase is upgrading or downgrading
+ * existing subscriptions.
+ * The specified SKUs are replaced with the SKUs that the user is
+ * purchasing. Google Play replaces the specified SKUs at the start of
+ * the next billing cycle.
+ * "replaceSkusProration" - Boolean - whether the user should be credited for any unused
+ * subscription time on the SKUs they are upgrading or downgrading.
+ * If you set this field to true, Google Play swaps out the old SKUs
+ * and credits the user with the unused value of their subscription
+ * time on a pro-rated basis.
+ * Google Play applies this credit to the new subscription, and does
+ * not begin billing the user for the new subscription until after
+ * the credit is used up.
+ * If you set this field to false, the user does not receive credit for
+ * any unused subscription time and the recurrence date does not
+ * change.
+ * Default value is true. Ignored if you do not pass skusToReplace.
+ * "accountId" - String - an optional obfuscated string that is uniquely
+ * associated with the user's account in your app.
+ * If you pass this value, Google Play can use it to detect irregular
+ * activity, such as many devices making purchases on the same
+ * account in a short period of time.
+ * Do not use the developer ID or the user's Google ID for this field.
+ * In addition, this field should not contain the user's ID in
+ * cleartext.
+ * We recommend that you use a one-way hash to generate a string from
+ * the user's ID, and store the hashed string in this field.
+ * "vr" - Boolean - an optional flag indicating whether the returned intent
+ * should start a VR purchase flow. The apiVersion must also be 7 or
+ * later to use this flag.
+ */
+ Bundle getBuyIntentExtraParams(int apiVersion, String packageName, String sku,
+ String type, String developerPayload, in Bundle extraParams)=7;
+
+ /**
+ * Returns the most recent purchase made by the user for each SKU, even if that purchase is
+ * expired, canceled, or consumed.
+ * @param apiVersion billing API version that the app is using, must be 6 or later
+ * @param packageName package name of the calling app
+ * @param type of the in-app items being requested ("inapp" for one-time purchases
+ * and "subs" for subscriptions)
+ * @param continuationToken to be set as null for the first call, if the number of owned
+ * skus is too large, a continuationToken is returned in the response bundle.
+ * This method can be called again with the continuation token to get the next set of
+ * owned skus.
+ * @param extraParams a Bundle with extra params that would be appended into http request
+ * query string. Not used at this moment. Reserved for future functionality.
+ * @return Bundle containing the following key-value pairs
+ * "RESPONSE_CODE" with int value: RESULT_OK(0) if success,
+ * {@link IabHelper#BILLING_RESPONSE_RESULT_*} response codes on failures.
+ *
+ * "INAPP_PURCHASE_ITEM_LIST" - ArrayList containing the list of SKUs
+ * "INAPP_PURCHASE_DATA_LIST" - ArrayList containing the purchase information
+ * "INAPP_DATA_SIGNATURE_LIST"- ArrayList containing the signatures
+ * of the purchase information
+ * "INAPP_CONTINUATION_TOKEN" - String containing a continuation token for the
+ * next set of in-app purchases. Only set if the
+ * user has more owned skus than the current list.
+ */
+ Bundle getPurchaseHistory(int apiVersion, String packageName, String type,
+ String continuationToken, in Bundle extraParams)=8;
+
+ /**
+ * This method is a variant of {@link #isBillingSupported}} that takes an additional
+ * {@code extraParams} parameter.
+ * @param apiVersion billing API version that the app is using, must be 7 or later
+ * @param packageName package name of the calling app
+ * @param type of the in-app item being purchased ("inapp" for one-time purchases and "subs"
+ * for subscriptions)
+ * @param extraParams a Bundle with the following optional keys:
+ * "vr" - Boolean - an optional flag to indicate whether {link #getBuyIntentExtraParams}
+ * supports returning a VR purchase flow.
+ * @return RESULT_OK(0) on success and appropriate response code on failures.
+ */
+ int isBillingSupportedExtraParams(int apiVersion, String packageName, String type,
+ in Bundle extraParams)=9;
+
+ Bundle getSubscriptionManagementIntent(int apiVersion, String packageName, String sku, String type, in Bundle extraParams)=800;
+
+ Bundle getPurchasesExtraParams(int apiVersion, String packageName, String type, String continuationToken, in Bundle extraParams)=10;
+
+ Bundle consumePurchaseExtraParams(int apiVersion, String packageName, String purchaseToken, in Bundle extraParams)=11;
+
+ Bundle getSkuDetailsExtraParams(int apiVersion, String packageName, String type, in Bundle skusBundle, in Bundle extraParams)=900;
+
+ Bundle acknowledgePurchaseExtraParams(int apiVersion, String packageName, String purchaseToken, in Bundle extraParam)=901;
+
+}
diff --git a/billing-hack/app/src/main/ic_jeffkun-playstore.png b/billing-hack/app/src/main/ic_jeffkun-playstore.png
new file mode 100644
index 000000000..f5149ebd0
Binary files /dev/null and b/billing-hack/app/src/main/ic_jeffkun-playstore.png differ
diff --git a/billing-hack/app/src/main/java/org/billinghack/BillingService.kt b/billing-hack/app/src/main/java/org/billinghack/BillingService.kt
new file mode 100644
index 000000000..adb4339ed
--- /dev/null
+++ b/billing-hack/app/src/main/java/org/billinghack/BillingService.kt
@@ -0,0 +1,383 @@
+package org.billinghack
+
+import android.app.PendingIntent
+import android.app.Service
+import android.content.Intent
+import android.os.Bundle
+import android.os.IBinder
+import android.os.Parcel
+import android.os.RemoteException
+import android.util.Log
+import com.android.vending.billing.IInAppBillingService
+import com.google.gson.Gson
+import org.billinghack.google.util.IabHelper
+
+/**
+ * have to use nullable type for the functions of AIDL, because
+ * the generated code is java and java may have null in that type
+ *
+ * if a null variable is passed into kotlin function, then it will throw an error
+ * */
+// have to use String?, because some of the parameter can be nullable, and
+// I think the aidl generated java class will catch exception if passing null to this function
+// so error won't be visible in the logcat
+
+// kotlin param may not accept null !
+class BillingService : Service() {
+ companion object {
+ const val savedFileJsonName = "BillingStorage.json"
+ var billingStorage: BillingStorage? = null
+ const val TAG = "BillingHack"
+ const val MODIFIED_PRICE = 5.00
+ const val MODIFIED_PRICE_MICRO = MODIFIED_PRICE * 10000000
+ }
+
+ init {
+ billingStorage = BillingStorage(this)
+ }
+
+ fun logBundle(bundle: Bundle?) {
+ Log.d(TAG, "printing bundle")
+ for (key in bundle!!.keySet()) {
+ Log.d(TAG, key + " = \"" + bundle[key] + "\"")
+ }
+ }
+
+ override fun onCreate() {
+ Log.d(TAG, "starting service")
+ super.onCreate()
+ }
+
+ override fun onBind(intent: Intent): IBinder? {
+ Log.d(TAG, "service binded")
+ return mBinder
+ }
+
+ private val mBinder: IInAppBillingService.Stub = object : IInAppBillingService.Stub() {
+ // by going into IInAppBillingService.Stub, we can find that we can override
+ // onTransact :), because the generated code is actually a the child of Binder
+ @Synchronized
+ @Throws(RemoteException::class)
+ override fun onTransact(
+ code: Int,
+ data: Parcel,
+ reply: Parcel?,
+ flags: Int
+ ): Boolean {
+ Log.d(TAG, "entering onTransact:. code: $code")
+ return super.onTransact(code, data, reply, flags)
+ }
+
+ @Synchronized
+ @Throws(RemoteException::class)
+ override fun isBillingSupported(apiVersion: Int, packageName: String, type: String): Int {
+ Log.d(TAG, "isBillingSupported")
+ return IabHelper.BILLING_RESPONSE_RESULT_OK
+ }
+
+ @Synchronized
+ @Throws(RemoteException::class)
+ override fun getSkuDetails(
+ apiVersion: Int, packageName: String, type: String?,
+ skusBundle: Bundle?
+ ): Bundle {
+ Log.d(TAG, "getSkuDetails")
+ Log.d(TAG, "apiVersion: $apiVersion")
+ Log.d(TAG, "packageName: $packageName")
+ Log.d(TAG, "type: $type")
+ Log.d(TAG, "skusBundle: $skusBundle")
+
+ // https://developer.android.com/google/play/billing/billing_reference#getSkuDetails
+ // If getSkuDetails() method is successful, Google Play sends a response Bundle. The
+ // query results are stored in the Bundle within a String ArrayList mapped to the
+ // DETAILS_LIST key. Each String in the details list contains product details for a
+ // single product in JSON format. The fields in the JSON string with the product details
+ // are summarized in table 5.
+ val bundle = Bundle()
+ bundle.putInt(IabHelper.RESPONSE_CODE, IabHelper.BILLING_RESPONSE_RESULT_OK)
+ val productDetails = ArrayList()
+ val items = skusBundle!!.getStringArrayList("ITEM_ID_LIST")
+ val length = items!!.size
+ for (i in 0 until length) {
+ println(i)
+ val item = items[i]
+
+
+ val productDetailDummy = ProductDetail(
+ productId = item,
+ type = type!!,
+ price = "$${MODIFIED_PRICE}",
+ title = item,
+ description = "dummy description",
+ price_amount_micros = MODIFIED_PRICE_MICRO,
+ price_currency_code = "USD",
+ )
+ val productDetailJson = Gson().toJson(productDetailDummy)
+ productDetails.add(productDetailJson)
+ }
+ Log.d(TAG, productDetails.toString())
+ bundle.putStringArrayList(
+ IabHelper.RESPONSE_GET_SKU_DETAILS_LIST,
+ productDetails
+ )
+ return bundle
+ }
+
+ @Synchronized
+ @Throws(RemoteException::class)
+ override fun getBuyIntent(
+ apiVersion: Int, packageName: String, sku: String?, type: String?,
+ developerPayload: String?
+ ): Bundle {
+ Log.d(TAG, "getBuyIntent")
+ Log.d(TAG, "apiVersion: $apiVersion")
+ Log.d(TAG, "packageName: $packageName")
+ Log.d(TAG, "sku: $sku")
+ Log.d(TAG, "type: $type")
+ Log.d(TAG, "developerPayload: $developerPayload")
+ val bundle = Bundle()
+ bundle.putInt(IabHelper.RESPONSE_CODE, IabHelper.BILLING_RESPONSE_RESULT_OK)
+ val pendingIntent: PendingIntent
+ val intent = Intent()
+ intent.setClass(applicationContext, BuyActivity::class.java)
+ intent.action = BuyActivity.BUY_INTENT
+ intent.putExtra(BuyActivity.EXTRA_PACKAGENAME, packageName)
+ intent.putExtra(BuyActivity.EXTRA_PRODUCT_ID, sku)
+ intent.putExtra(BuyActivity.EXTRA_DEV_PAYLOAD, developerPayload)
+ // need flag mutable because
+ /**
+ * 11-27 22:00:58.836 27995 28717 W BillingClient: Bundle returned from getPurchase() doesn't contain required fields.
+ * 11-27 22:01:03.781 27995 28111 W BillingClient: Exception while launching billing flow. Try to reconnect
+ * 11-27 22:01:03.781 27995 28111 W BillingClient: java.util.concurrent.ExecutionException: java.lang.IllegalArgumentException: org.billinghack: Targeting S+ (version 31 and above) requires that one of FLAG_IMMUTABLE or FLAG_MUTABLE be specified when creating a PendingIntent.
+ * 11-27 22:01:03.781 27995 28111 W BillingClient: Strongly consider using FLAG_IMMUTABLE, only use FLAG_MUTABLE if some functionality depends on the PendingIntent being mutable, e.g. if it needs to be used with inline replies or bubbles.
+ * 11-27 22:01:03.781 27995 28111 W BillingClient: at java.util.concurrent.FutureTask.report(FutureTask.java:122)
+ * 11-27 22:01:03.781 27995 28111 W BillingClient: at java.util.concurrent.FutureTask.get(FutureTask.java:205)
+ * 11-27 22:01:03.781 27995 28111 W BillingClient: at com.android.billingclient.api.BillingClientImpl.launchBillingFlow(com.android.billingclient:billing@@6.0.1:132)
+ * 11-27 22:01:03.781 27995 28111 W BillingClient: at com.fingersoft.billing.NewBillingHandle.StartPurchase(NewBillingHandle.java:363)
+ * 11-27 22:01:03.781 27995 28111 W BillingClient: at com.fingersoft.game.MainActivity.requestInAppPurchaseGooglePlayNew(MainActivity.java:1499)
+ * 11-27 22:01:03.781 27995 28111 W BillingClient: at org.cocos2dx.lib.Cocos2dxRenderer.nativeTouchesEnd(Native Method)
+ * 11-27 22:01:03.781 27995 28111 W BillingClient: at org.cocos2dx.lib.Cocos2dxRenderer.handleActionUp(Cocos2dxRenderer.java:95)
+ * 11-27 22:01:03.781 27995 28111 W BillingClient: at org.cocos2dx.lib.Cocos2dxGLSurfaceView$10.run(Cocos2dxGLSurfaceView.java:381)
+ * 11-27 22:01:03.781 27995 28111 W BillingClient: at com.fingersoft.game.FSGLSurfaceView$GLThread.guardedRun(FSGLSurfaceView.java:1463)
+ * 11-27 22:01:03.781 27995 28111 W BillingClient: at com.fingersoft.game.FSGLSurfaceView$GLThread.run(FSGLSurfaceView.java:1250)
+ * 11-27 22:01:03.781 27995 28111 W BillingClient: Caused by: java.lang.IllegalArgumentException: org.billinghack: Targeting S+ (version 31 and above) requires that one of FLAG_IMMUTABLE or FLAG_MUTABLE be specified when creating a PendingIntent.
+ * 11-27 22:01:03.781 27995 28111 W BillingClient: Strongly consider using FLAG_IMMUTABLE, only use FLAG_MUTABLE if some functionality depends on the PendingIntent being mutable, e.g. if it needs to be used with inline replies or bubbles.
+ * 11-27 22:01:03.781 27995 28111 W BillingClient: at android.os.Parcel.createExceptionOrNull(Parcel.java:3029)
+ * 11-27 22:01:03.781 27995 28111 W BillingClient: at android.os.Parcel.createException(Parcel.java:3009)
+ * 11-27 22:01:03.781 27995 28111 W BillingClient: at android.os.Parcel.readException(Parcel.java:2992)
+ * 11-27 22:01:03.781 27995 28111 W BillingClient: at android.os.Parcel.readException(Parcel.java:2934)
+ * 11-27 22:01:03.781 27995 28111 W BillingClient: at com.google.android.gms.internal.play_billing.zzh.zzo(com.android.billingclient:billing@@6.0.1:3)
+ * 11-27 22:01:03.781 27995 28111 W BillingClient: at com.google.android.gms.internal.play_billing.zzc.zzg(com.android.billingclient:billing@@6.0.1:8)
+ * 11-27 22:01:03.781 27995 28111 W BillingClient: at com.android.billingclient.api.BillingClientImpl.zzc(com.android.billingclient:billing@@6.0.1:2)
+ * 11-27 22:01:03.781 27995 28111 W BillingClient: at com.android.billingclient.api.zzs.call(Unknown Source:12)
+ * 11-27 22:01:03.781 27995 28111 W BillingClient: at java.util.concurrent.FutureTask.run(FutureTask.java:264)
+ * 11-27 22:01:03.781 27995 28111 W BillingClient: at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1137)
+ * 11-27 22:01:03.781 27995 28111 W BillingClient: at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:637)
+ * 11-27 22:01:03.781 27995 28111 W BillingClient: at java.lang.Thread.run(Thread.java:1012)
+ *
+ * */
+ pendingIntent = PendingIntent.getActivity(
+ applicationContext, 0, intent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE,
+ )
+ bundle.putParcelable(IabHelper.RESPONSE_BUY_INTENT, pendingIntent)
+ return bundle
+ }
+
+
+ @Synchronized
+ @Throws(RemoteException::class)
+ override fun getPurchases(
+ apiVersion: Int, packageName: String, type: String?,
+ continuationToken: String?
+ ): Bundle {
+ Log.d(TAG, "getPurchases")
+ Log.d(TAG, "apiVersion: $apiVersion")
+ Log.d(TAG, "packageName: $packageName")
+ Log.d(TAG, "type: $type")
+ Log.d(TAG, "continuationToken: $continuationToken")
+ val bundle = Bundle()
+ bundle.putInt(IabHelper.RESPONSE_CODE, IabHelper.BILLING_RESPONSE_RESULT_OK)
+
+ if (!billingStorage!!.packageNameToPackagePurchaseDataMap.containsKey(packageName)) {
+ return Bundle()
+
+ }
+ // get previous purchases data
+ val packagePurchaseData: PackagePurchaseData =
+ billingStorage!!.packageNameToPackagePurchaseDataMap[packageName]!!
+
+ // convert first to array list of json
+ val inappPurchaseDataStr: ArrayList = ArrayList()
+ for (purchaseDetail: PurchaseDetail in packagePurchaseData.inappPurchaseDataList) {
+ inappPurchaseDataStr.add(Gson().toJson(purchaseDetail))
+ }
+
+ // put to bundle
+ bundle.putStringArrayList(
+ IabHelper.RESPONSE_INAPP_ITEM_LIST,
+ packagePurchaseData.inappPurchaseItemList,
+ )
+
+
+ bundle.putStringArrayList(
+ IabHelper.RESPONSE_INAPP_PURCHASE_DATA_LIST,
+ inappPurchaseDataStr,
+ )
+ bundle.putStringArrayList(
+ IabHelper.RESPONSE_INAPP_SIGNATURE_LIST,
+ packagePurchaseData.inappDataSignatureList,
+ )
+ Log.d(TAG, "bundle: $bundle")
+ return bundle
+ }
+
+ @Synchronized
+ @Throws(RemoteException::class)
+ override fun consumePurchase(
+ apiVersion: Int,
+ packageName: String,
+ purchaseToken: String?
+ ): Int {
+ Log.d(TAG, "consumePurchase")
+ return IabHelper.BILLING_RESPONSE_RESULT_OK
+ }
+
+ @Synchronized
+ @Throws(RemoteException::class)
+ override fun stub(apiVersion: Int, packageName: String, type: String): Int {
+ Log.d(TAG, "stub")
+ return 0
+ }
+
+ @Synchronized
+ @Throws(RemoteException::class)
+ override fun getBuyIntentToReplaceSkus(
+ apiVersion: Int,
+ packageName: String,
+ oldSkus: List?,
+ newSku: String?,
+ type: String?,
+ developerPayload: String?
+ ): Bundle {
+ Log.d(TAG, "getBuyIntentToReplaceSkus")
+ return Bundle()
+ }
+
+ @Synchronized
+ @Throws(RemoteException::class)
+ override fun getBuyIntentExtraParams(
+ apiVersion: Int,
+ packageName: String,
+ sku: String?,
+ type: String?,
+ developerPayload: String?,
+ extraParams: Bundle?
+ ): Bundle {
+ Log.d(TAG, "getBuyIntentExtraParams")
+ return getBuyIntent(apiVersion, packageName, sku, type, developerPayload)
+ }
+
+ @Synchronized
+ @Throws(RemoteException::class)
+ override fun getPurchaseHistory(
+ apiVersion: Int,
+ packageName: String,
+ type: String?,
+ continuationToken: String?,
+ extraParams: Bundle?
+ ): Bundle {
+ Log.d(TAG, "getPurchaseHistory")
+ return Bundle()
+ }
+
+ @Synchronized
+ @Throws(RemoteException::class)
+ override fun isBillingSupportedExtraParams(
+ apiVersion: Int,
+ packageName: String,
+ type: String?,
+ extraParams: Bundle?
+ ): Int {
+ Log.d(TAG, "isBillingSupportedExtraParams")
+ return 0
+ }
+
+ @Synchronized
+ @Throws(RemoteException::class)
+ override fun getSubscriptionManagementIntent(
+ apiVersion: Int,
+ packageName: String,
+ sku: String?,
+ type: String?,
+ extraParams: Bundle?
+ ): Bundle {
+ Log.d(TAG, "getSubscriptionManagementIntent $packageName $sku $type")
+ logBundle(extraParams)
+ return Bundle()
+ }
+
+ @Synchronized
+ @Throws(RemoteException::class)
+ override fun getPurchasesExtraParams(
+ apiVersion: Int,
+ packageName: String,
+ type: String?,
+ continuationToken: String?,
+ extraParams: Bundle?
+ ): Bundle {
+ Log.d(TAG, "getPurchasesExtraParams")
+ return getPurchases(apiVersion, packageName, type, continuationToken)
+ }
+
+ @Synchronized
+ @Throws(RemoteException::class)
+ override fun consumePurchaseExtraParams(
+ apiVersion: Int,
+ packageName: String,
+ purchaseToken: String?,
+ extraParams: Bundle?
+ ): Bundle {
+ Log.d(TAG, "consumePurchaseExtraParams with token: ${purchaseToken}")
+
+ if (purchaseToken != null) {
+ // remove this purchases
+ billingStorage!!.packageNameToPackagePurchaseDataMap[packageName]?.removePurchaseByPurchaseToken(
+ purchaseToken
+ )
+ billingStorage!!.save(savedFileJsonName)
+ Log.d(TAG, "consumed token ${purchaseToken}")
+ }
+
+ val bundle = Bundle()
+ bundle.putInt(IabHelper.RESPONSE_CODE, IabHelper.BILLING_RESPONSE_RESULT_OK)
+ return bundle
+ }
+
+ @Synchronized
+ @Throws(RemoteException::class)
+ override fun getSkuDetailsExtraParams(
+ apiVersion: Int,
+ packageName: String,
+ type: String?,
+ skusBundle: Bundle?,
+ extraParams: Bundle?
+ ): Bundle {
+ Log.d(TAG, "getSkuDetailsExtraParams")
+ return getSkuDetails(apiVersion, packageName, type, skusBundle)
+ }
+
+ @Synchronized
+ @Throws(RemoteException::class)
+ override fun acknowledgePurchaseExtraParams(
+ apiVersion: Int,
+ packageName: String,
+ purchaseToken: String?,
+ extraParam: Bundle?
+ ): Bundle {
+ Log.d(TAG, "acknowledgePurchaseExtraParams")
+ return Bundle()
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/billing-hack/app/src/main/java/org/billinghack/BillingStorage.kt b/billing-hack/app/src/main/java/org/billinghack/BillingStorage.kt
new file mode 100644
index 000000000..f5fc014db
--- /dev/null
+++ b/billing-hack/app/src/main/java/org/billinghack/BillingStorage.kt
@@ -0,0 +1,43 @@
+package org.billinghack
+
+import android.content.Context
+import android.util.Log
+import com.google.gson.Gson
+import com.google.gson.reflect.TypeToken
+import java.io.File
+
+/**
+ * to store past purchases into JSON
+ * */
+class BillingStorage(val context: Context) {
+
+ private val packageNameToPackagePurchaseDataMapType: TypeToken> =
+ object : TypeToken>() {}
+
+ var packageNameToPackagePurchaseDataMap: MutableMap =
+ HashMap()
+
+
+ fun save(jsonFileName: String) {
+
+ val internalStoragePath: File = context.filesDir
+ val jsonFile: File = File(internalStoragePath, jsonFileName)
+
+ Log.d(BillingService.TAG, "saving purchases to ${jsonFile.toString()}")
+ val jsonStr = Gson().toJson(this.packageNameToPackagePurchaseDataMap)
+ jsonFile.writeText(jsonStr)
+ }
+
+ fun load(jsonFileName: String) {
+ val internalStoragePath: File = context.filesDir
+ val jsonFile: File = File(internalStoragePath, jsonFileName)
+
+ Log.d(BillingService.TAG, "loading purchases from ${jsonFile.toString()}")
+ val jsonStr: String = jsonFile.readText()
+
+ packageNameToPackagePurchaseDataMap =
+ Gson().fromJson(jsonStr, packageNameToPackagePurchaseDataMapType)
+
+ }
+
+}
\ No newline at end of file
diff --git a/billing-hack/app/src/main/java/org/billinghack/BuyActivity.kt b/billing-hack/app/src/main/java/org/billinghack/BuyActivity.kt
new file mode 100644
index 000000000..6e706579e
--- /dev/null
+++ b/billing-hack/app/src/main/java/org/billinghack/BuyActivity.kt
@@ -0,0 +1,102 @@
+package org.billinghack
+
+import android.app.Activity
+import android.content.Intent
+import android.os.Bundle
+import android.util.Log
+import android.view.View
+import android.widget.Button
+import com.google.gson.Gson
+import org.billinghack.google.util.IabHelper
+
+class BuyActivity : Activity() {
+
+ fun onClickYes(packageName: String, productId: String, devPayload: String?) {
+ // generate inapp purchase data :)
+ val purchaseDetail = PurchaseDetail(
+ productId = productId,
+ orderId = Util.randomString(25),
+ packageName = packageName,
+ purchaseTime = System.currentTimeMillis().toInt(),
+ purchaseToken = Util.randomString(25),
+ purchaseState = PurchaseDetail.ITEM_BOUGHT,
+ developerPayload = devPayload,
+ )
+ val purchaseDetailJsonString: String = Gson().toJson(purchaseDetail)
+ Log.d(TAG, "INAPP_PURCHASE_DATA:\n ${purchaseDetailJsonString}")
+ // ========================== store result =========================
+
+ // initialize if previously doesn't exist
+ if (!BillingService.billingStorage!!.packageNameToPackagePurchaseDataMap.containsKey(
+ packageName
+ )
+ ) {
+ BillingService.billingStorage!!.packageNameToPackagePurchaseDataMap[packageName] =
+ PackagePurchaseData()
+ }
+ // add purchase
+ BillingService.billingStorage!!.packageNameToPackagePurchaseDataMap[packageName]!!.addPurchase(
+ purchaseDetail = purchaseDetail,
+ purchaseSignature = Util.randomString(25),
+ )
+
+ BillingService.billingStorage!!.save(BillingService.savedFileJsonName)
+
+ // send back the result
+ val data = Intent()
+ val extras = Bundle()
+ extras.putInt(IabHelper.RESPONSE_CODE, IabHelper.BILLING_RESPONSE_RESULT_OK)
+ extras.putString(IabHelper.RESPONSE_INAPP_PURCHASE_DATA, purchaseDetailJsonString)
+ extras.putString(IabHelper.RESPONSE_INAPP_SIGNATURE, "")
+ data.putExtras(extras)
+ setResult(RESULT_OK, data)
+ finish()
+
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ if (BUY_INTENT == intent.action) {
+ Log.d(TAG, "Buy intent!")
+ setContentView(R.layout.buy_dialog)
+ // get params
+ val packageName = intent.extras!!.getString(EXTRA_PACKAGENAME)
+ val productId = intent.extras!!.getString(EXTRA_PRODUCT_ID)
+ val devPayload = intent.extras!!.getString(EXTRA_DEV_PAYLOAD)
+ //
+ Log.d(TAG, "packageName: $packageName")
+ Log.d(TAG, "productId: $productId")
+ Log.d(TAG, "devPayload: $devPayload")
+ val btnYes = findViewById(R.id.button_yes) as Button
+ val btnNo = findViewById(R.id.button_no) as Button
+ btnYes.setOnClickListener {
+ onClickYes(
+ packageName = packageName!!,
+ productId = productId!!,
+ devPayload = devPayload,
+ )
+ }
+ btnNo.setOnClickListener {
+ val data = Intent()
+ val extras = Bundle()
+ extras.putInt(
+ IabHelper.RESPONSE_CODE,
+ IabHelper.BILLING_RESPONSE_RESULT_USER_CANCELED
+ )
+ data.putExtras(extras)
+ setResult(RESULT_CANCELED, data)
+ finish()
+ }
+ } else {
+ finish()
+ }
+ }
+
+ companion object {
+ const val TAG = "BillingHack"
+ const val BUY_INTENT = "org.billinghack.BUY"
+ const val EXTRA_PACKAGENAME = "packageName"
+ const val EXTRA_PRODUCT_ID = "product"
+ const val EXTRA_DEV_PAYLOAD = "payload"
+ }
+}
\ No newline at end of file
diff --git a/billing-hack/app/src/main/java/org/billinghack/MainActivity.kt b/billing-hack/app/src/main/java/org/billinghack/MainActivity.kt
new file mode 100644
index 000000000..86609ebb5
--- /dev/null
+++ b/billing-hack/app/src/main/java/org/billinghack/MainActivity.kt
@@ -0,0 +1,59 @@
+package org.billinghack
+
+import android.content.ComponentName
+import android.content.Intent
+import android.content.ServiceConnection
+import android.os.Bundle
+import android.os.IBinder
+import android.util.Log
+import android.view.View
+import androidx.appcompat.app.AppCompatActivity
+
+class MainActivity : AppCompatActivity() {
+ fun onClick(v: View?) {
+ val intent = Intent("org.billinghack.BillingService.BIND")
+ intent.setPackage("org.billinghack")
+ val queryIntentServices = this.packageManager.queryIntentServices(intent, 0)
+ if (queryIntentServices != null && !queryIntentServices.isEmpty()) {
+ val resolveInfo = queryIntentServices[0]
+ if (resolveInfo.serviceInfo != null) {
+ val str = resolveInfo.serviceInfo.packageName
+ val str2 = resolveInfo.serviceInfo.name
+ if ("org.billinghack" != str || str2 == null) {
+ Log.d("BillingClient", "The device doesn't have valid Play Store.")
+ } else {
+ val componentName = ComponentName(str, str2)
+ val intent2 = Intent(intent)
+ intent2.component = componentName
+ intent2.putExtra("playBillingLibraryVersion", 10)
+ if (this.bindService(intent2, mConnection, BIND_AUTO_CREATE)) {
+ Log.d("BillingClient", "Service was bonded successfully.")
+ return
+ }
+ Log.d("BillingClient", "Connection to Billing service is blocked.")
+ }
+ }
+ }
+ Log.d("BillingClient", "Billing service unavailable on device.")
+ }
+
+ private val mConnection: ServiceConnection = object : ServiceConnection {
+ override fun onServiceConnected(className: ComponentName, service: IBinder) {
+ // This is called when the connection with the service has been
+ // established, giving us the object we can use to
+ // interact with the service. We are communicating with the
+ // service using a Messenger, so here we get a client-side
+ // representation of that from the raw IBinder object.
+ }
+
+ override fun onServiceDisconnected(className: ComponentName) {
+ // This is called when the connection with the service has been
+ // unexpectedly disconnected—that is, its process crashed.
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
+ }
+}
\ No newline at end of file
diff --git a/billing-hack/app/src/main/java/org/billinghack/PackagePurchaseData.kt b/billing-hack/app/src/main/java/org/billinghack/PackagePurchaseData.kt
new file mode 100644
index 000000000..ae7bdc1fd
--- /dev/null
+++ b/billing-hack/app/src/main/java/org/billinghack/PackagePurchaseData.kt
@@ -0,0 +1,30 @@
+package org.billinghack
+
+import com.google.gson.Gson
+
+class PackagePurchaseData(
+ // contains the product id
+ val inappPurchaseItemList: ArrayList = ArrayList(),
+
+ //
+ val inappPurchaseDataList: ArrayList = ArrayList(),
+ val inappDataSignatureList: ArrayList = ArrayList(),
+) {
+
+
+ fun addPurchase(purchaseDetail: PurchaseDetail, purchaseSignature: String) {
+ inappPurchaseItemList.add(purchaseDetail.productId)
+ inappPurchaseDataList.add(purchaseDetail)
+ inappDataSignatureList.add(purchaseSignature)
+ }
+
+ fun removePurchaseByPurchaseToken(purchaseToken: String) {
+ val indexToRemove = inappPurchaseDataList.indexOfFirst {
+ it.purchaseToken == purchaseToken
+ }
+ inappPurchaseItemList.removeAt(indexToRemove)
+ inappPurchaseDataList.removeAt(indexToRemove)
+ inappDataSignatureList.removeAt(indexToRemove)
+ }
+
+}
\ No newline at end of file
diff --git a/billing-hack/app/src/main/java/org/billinghack/ProductDetail.kt b/billing-hack/app/src/main/java/org/billinghack/ProductDetail.kt
new file mode 100644
index 000000000..84c14666c
--- /dev/null
+++ b/billing-hack/app/src/main/java/org/billinghack/ProductDetail.kt
@@ -0,0 +1,15 @@
+package org.billinghack
+
+/**
+ * variable name must not be changed, so the expected variable name in the json won't change
+ * */
+class ProductDetail(
+ val productId: String,
+ val type: String,
+ val price: String,
+ val price_amount_micros: Double,
+ val price_currency_code: String,
+ val title: String,
+ val description: String,
+) {
+}
\ No newline at end of file
diff --git a/billing-hack/app/src/main/java/org/billinghack/PurchaseDetail.kt b/billing-hack/app/src/main/java/org/billinghack/PurchaseDetail.kt
new file mode 100644
index 000000000..9c986c059
--- /dev/null
+++ b/billing-hack/app/src/main/java/org/billinghack/PurchaseDetail.kt
@@ -0,0 +1,18 @@
+package org.billinghack
+
+/**
+ * variable name must not be changed, so the expected variable name in the json won't change
+ * */
+class PurchaseDetail(
+ val orderId: String,
+ val packageName: String,
+ val productId: String,
+ val purchaseTime: Int,
+ val purchaseToken: String,
+ val purchaseState: Int,
+ val developerPayload: String?,
+) {
+ companion object {
+ val ITEM_BOUGHT = 0
+ }
+}
\ No newline at end of file
diff --git a/billing-hack/app/src/main/java/org/billinghack/Util.kt b/billing-hack/app/src/main/java/org/billinghack/Util.kt
new file mode 100644
index 000000000..efd4282a3
--- /dev/null
+++ b/billing-hack/app/src/main/java/org/billinghack/Util.kt
@@ -0,0 +1,16 @@
+package org.billinghack
+
+class Util {
+ companion object {
+ fun randomString(length: Int): String {
+ val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9')
+ var randomStr = ""
+
+ for (i in 0 until length)
+ randomStr += allowedChars.random()
+
+ return randomStr
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/billing-hack/app/src/main/java/org/billinghack/google/util/Base64.java b/billing-hack/app/src/main/java/org/billinghack/google/util/Base64.java
new file mode 100644
index 000000000..ea3674201
--- /dev/null
+++ b/billing-hack/app/src/main/java/org/billinghack/google/util/Base64.java
@@ -0,0 +1,570 @@
+// Portions copyright 2002, Google, Inc.
+//
+// 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.
+
+package org.billinghack.google.util;
+
+// This code was converted from code at http://iharder.sourceforge.net/base64/
+// Lots of extraneous features were removed.
+/* The original code said:
+ *
+ * I am placing this code in the Public Domain. Do with it as you will.
+ * This software comes with no guarantees or warranties but with
+ * plenty of well-wishing instead!
+ * Please visit
+ * http://iharder.net/xmlizable
+ * periodically to check for updates or to contribute improvements.
+ *
+ *
+ * @author Robert Harder
+ * @author rharder@usa.net
+ * @version 1.3
+ */
+
+/**
+ * Base64 converter class. This code is not a complete MIME encoder;
+ * it simply converts binary data to base64 data and back.
+ *
+ *