Initial commit
14
.gitignore
vendored
Normal file
|
@ -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
|
116
.idea/codeStyles/Project.xml
Normal file
|
@ -0,0 +1,116 @@
|
|||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<codeStyleSettings language="XML">
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
</indentOptions>
|
||||
<arrangement>
|
||||
<rules>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:android</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:id</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>style</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>ANDROID_ATTRIBUTE_ORDER</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
</rules>
|
||||
</arrangement>
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
20
.idea/gradle.xml
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="testRunner" value="PLATFORM" />
|
||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
</set>
|
||||
</option>
|
||||
<option name="resolveModulePerSourceSet" value="false" />
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
14
.idea/misc.xml
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CMakeSettings">
|
||||
<configurations>
|
||||
<configuration PROFILE_NAME="Debug" CONFIG_NAME="Debug" />
|
||||
</configurations>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
</component>
|
||||
</project>
|
12
.idea/runConfigurations.xml
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RunConfigurationProducerService">
|
||||
<option name="ignoredProducers">
|
||||
<set>
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
1
app/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/build
|
34
app/build.gradle
Normal file
|
@ -0,0 +1,34 @@
|
|||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion 28
|
||||
|
||||
|
||||
defaultConfig {
|
||||
applicationId "org.billinghack"
|
||||
minSdkVersion 18
|
||||
targetSdkVersion 28
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
}
|
21
app/proguard-rules.pro
vendored
Normal file
|
@ -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
|
|
@ -0,0 +1,27 @@
|
|||
package org.billinghack;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class ExampleInstrumentedTest {
|
||||
@Test
|
||||
public void useAppContext() {
|
||||
// Context of the app under test.
|
||||
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
|
||||
|
||||
assertEquals("org.billinghack", appContext.getPackageName());
|
||||
}
|
||||
}
|
38
app/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,38 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="org.billinghack">
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
<activity android:name=".MainActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="org.billinghack.BuyActivity"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@android:style/Theme.Dialog" >
|
||||
<intent-filter>
|
||||
<action android:name="org.billinghack.BUY" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<service
|
||||
android:name="org.billinghack.BillingService"
|
||||
android:enabled="true"
|
||||
android:exported="true"
|
||||
android:process=":billing" >
|
||||
<intent-filter android:priority="2147483647" >
|
||||
<action android:name="com.android.vending.billing.InAppBillingService.BIND" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
</application>
|
||||
|
||||
</manifest>
|
|
@ -0,0 +1,144 @@
|
|||
/*
|
||||
* 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_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 the billing version which 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 subscription.
|
||||
* @return RESULT_OK(0) on success, corresponding result code on failures
|
||||
*/
|
||||
int isBillingSupported(int apiVersion, String packageName, String type);
|
||||
|
||||
/**
|
||||
* 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 Third-party is using
|
||||
* @param packageName the package name of the calling app
|
||||
* @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, other response codes on
|
||||
* failure as listed above.
|
||||
* "DETAILS_LIST" with a StringArrayList containing purchase information
|
||||
* in JSON format similar to:
|
||||
* '{ "productId" : "exampleSku", "type" : "inapp", "price" : "$5.00",
|
||||
* "title : "Example Title", "description" : "This is an example description" }'
|
||||
*/
|
||||
Bundle getSkuDetails(int apiVersion, String packageName, String type, in Bundle skusBundle);
|
||||
|
||||
/**
|
||||
* 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 the type of the in-app item ("inapp" for one-time purchases
|
||||
* and "subs" for subscription).
|
||||
* @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, other response codes on
|
||||
* failure as listed above.
|
||||
* "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, other response codes on
|
||||
* failure as listed above.
|
||||
* "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
|
||||
* TODO: change this to app-specific keys.
|
||||
*/
|
||||
Bundle getBuyIntent(int apiVersion, String packageName, String sku, String type,
|
||||
String developerPayload);
|
||||
|
||||
/**
|
||||
* 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 the type of the in-app items being requested
|
||||
* ("inapp" for one-time purchases and "subs" for subscription).
|
||||
* @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, other response codes on
|
||||
* failure as listed above.
|
||||
* "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);
|
||||
|
||||
/**
|
||||
* 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 0 if consumption succeeded. Appropriate error values for failures.
|
||||
*/
|
||||
int consumePurchase(int apiVersion, String packageName, String purchaseToken);
|
||||
}
|
107
app/src/main/java/org/billinghack/BillingService.java
Normal file
|
@ -0,0 +1,107 @@
|
|||
package org.billinghack;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.billinghack.google.util.IabHelper;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.vending.billing.IInAppBillingService;
|
||||
|
||||
public class BillingService extends Service {
|
||||
|
||||
public static final String TAG = "BillingHack";
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return mBinder;
|
||||
}
|
||||
|
||||
private final IInAppBillingService.Stub mBinder = new IInAppBillingService.Stub() {
|
||||
|
||||
@Override
|
||||
public int isBillingSupported(int apiVersion, String packageName, String type)
|
||||
throws RemoteException {
|
||||
Log.d(TAG, "isBillingSupported");
|
||||
|
||||
return IabHelper.BILLING_RESPONSE_RESULT_OK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle getSkuDetails(int apiVersion, String packageName, String type,
|
||||
Bundle skusBundle) throws RemoteException {
|
||||
Log.d(TAG, "getSkuDetails");
|
||||
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putInt(IabHelper.RESPONSE_CODE, IabHelper.BILLING_RESPONSE_RESULT_OK);
|
||||
bundle.putStringArrayList(IabHelper.RESPONSE_GET_SKU_DETAILS_LIST,
|
||||
new ArrayList<String>());
|
||||
return bundle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle getBuyIntent(int apiVersion, String packageName, String sku, String type,
|
||||
String developerPayload) throws RemoteException {
|
||||
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);
|
||||
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putInt(IabHelper.RESPONSE_CODE, IabHelper.BILLING_RESPONSE_RESULT_OK);
|
||||
|
||||
PendingIntent pendingIntent;
|
||||
Intent intent = new Intent();
|
||||
intent.setClass(getApplicationContext(), BuyActivity.class);
|
||||
intent.setAction(BuyActivity.BUY_INTENT);
|
||||
intent.putExtra(BuyActivity.EXTRA_PACKAGENAME, packageName);
|
||||
intent.putExtra(BuyActivity.EXTRA_PRODUCT_ID, sku);
|
||||
intent.putExtra(BuyActivity.EXTRA_DEV_PAYLOAD, developerPayload);
|
||||
pendingIntent = PendingIntent.getActivity(getApplicationContext(), 0, intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
|
||||
bundle.putParcelable(IabHelper.RESPONSE_BUY_INTENT, pendingIntent);
|
||||
|
||||
return bundle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle getPurchases(int apiVersion, String packageName, String type,
|
||||
String continuationToken) throws RemoteException {
|
||||
Log.d(TAG, "getPurchases");
|
||||
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putInt(IabHelper.RESPONSE_CODE, IabHelper.BILLING_RESPONSE_RESULT_OK);
|
||||
bundle.putStringArrayList(IabHelper.RESPONSE_INAPP_ITEM_LIST, new ArrayList<String>());
|
||||
bundle.putStringArrayList(IabHelper.RESPONSE_INAPP_PURCHASE_DATA_LIST,
|
||||
new ArrayList<String>());
|
||||
bundle.putStringArrayList(IabHelper.RESPONSE_INAPP_SIGNATURE_LIST,
|
||||
new ArrayList<String>());
|
||||
|
||||
return bundle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int consumePurchase(int apiVersion, String packageName, String purchaseToken)
|
||||
throws RemoteException {
|
||||
Log.d(TAG, "consumePurchase");
|
||||
|
||||
return IabHelper.BILLING_RESPONSE_RESULT_OK;
|
||||
}
|
||||
|
||||
};
|
||||
}
|
87
app/src/main/java/org/billinghack/BuyActivity.java
Normal file
|
@ -0,0 +1,87 @@
|
|||
package org.billinghack;
|
||||
|
||||
import org.billinghack.google.util.IabHelper;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.widget.Button;
|
||||
|
||||
public class BuyActivity extends Activity {
|
||||
|
||||
public static final String TAG = "BillingHack";
|
||||
|
||||
public static final String BUY_INTENT = "org.billinghack.BUY";
|
||||
public static final String EXTRA_PACKAGENAME = "packageName";
|
||||
public static final String EXTRA_PRODUCT_ID = "product";
|
||||
public static final String EXTRA_DEV_PAYLOAD = "payload";
|
||||
|
||||
String pData;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
if (BUY_INTENT.equals(getIntent().getAction())) {
|
||||
Log.d(TAG, "Buy intent!");
|
||||
|
||||
setContentView(R.layout.buy_dialog);
|
||||
|
||||
String packageName = getIntent().getExtras().getString(EXTRA_PACKAGENAME);
|
||||
String productId = getIntent().getExtras().getString(EXTRA_PRODUCT_ID);
|
||||
String devPayload = getIntent().getExtras().getString(EXTRA_DEV_PAYLOAD);
|
||||
Log.d(TAG, "packageName: " + packageName);
|
||||
Log.d(TAG, "productId: " + productId);
|
||||
Log.d(TAG, "devPayload: " + devPayload);
|
||||
|
||||
pData = "{'orderId':'12999763169054705758.1371079406387615', 'packageName':'"
|
||||
+ packageName
|
||||
+ "', 'productId':'"
|
||||
+ productId
|
||||
+ "', 'purchaseTime':1345678900000, 'purchaseToken' : '122333444455555', 'developerPayload':'"
|
||||
+ devPayload + "'}";
|
||||
Log.d(TAG, "INAPP_PURCHASE_DATA:\n" + pData);
|
||||
|
||||
Button btnYes = (Button) findViewById(R.id.button_yes);
|
||||
Button btnNo = (Button) findViewById(R.id.button_no);
|
||||
|
||||
btnYes.setOnClickListener(new OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent data = new Intent();
|
||||
Bundle extras = new Bundle();
|
||||
extras.putInt(IabHelper.RESPONSE_CODE, IabHelper.BILLING_RESPONSE_RESULT_OK);
|
||||
extras.putString(IabHelper.RESPONSE_INAPP_PURCHASE_DATA, pData);
|
||||
extras.putString(IabHelper.RESPONSE_INAPP_SIGNATURE, "");
|
||||
data.putExtras(extras);
|
||||
|
||||
setResult(RESULT_OK, data);
|
||||
finish();
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
btnNo.setOnClickListener(new OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent data = new Intent();
|
||||
Bundle extras = new Bundle();
|
||||
extras.putInt(IabHelper.RESPONSE_CODE,
|
||||
IabHelper.BILLING_RESPONSE_RESULT_USER_CANCELED);
|
||||
data.putExtras(extras);
|
||||
|
||||
setResult(RESULT_CANCELED, data);
|
||||
finish();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
14
app/src/main/java/org/billinghack/MainActivity.java
Normal file
|
@ -0,0 +1,14 @@
|
|||
package org.billinghack;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
}
|
||||
}
|
570
app/src/main/java/org/billinghack/google/util/Base64.java
Normal file
|
@ -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:
|
||||
* <p>
|
||||
* 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
|
||||
* <a href="http://iharder.net/xmlizable">http://iharder.net/xmlizable</a>
|
||||
* periodically to check for updates or to contribute improvements.
|
||||
* </p>
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* <p>Note {@link CharBase64} is a GWT-compatible implementation of this
|
||||
* class.
|
||||
*/
|
||||
public class Base64 {
|
||||
/** Specify encoding (value is {@code true}). */
|
||||
public final static boolean ENCODE = true;
|
||||
|
||||
/** Specify decoding (value is {@code false}). */
|
||||
public final static boolean DECODE = false;
|
||||
|
||||
/** The equals sign (=) as a byte. */
|
||||
private final static byte EQUALS_SIGN = (byte) '=';
|
||||
|
||||
/** The new line character (\n) as a byte. */
|
||||
private final static byte NEW_LINE = (byte) '\n';
|
||||
|
||||
/**
|
||||
* The 64 valid Base64 values.
|
||||
*/
|
||||
private final static byte[] ALPHABET =
|
||||
{(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F',
|
||||
(byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K',
|
||||
(byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P',
|
||||
(byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U',
|
||||
(byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z',
|
||||
(byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e',
|
||||
(byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j',
|
||||
(byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o',
|
||||
(byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't',
|
||||
(byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y',
|
||||
(byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3',
|
||||
(byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8',
|
||||
(byte) '9', (byte) '+', (byte) '/'};
|
||||
|
||||
/**
|
||||
* The 64 valid web safe Base64 values.
|
||||
*/
|
||||
private final static byte[] WEBSAFE_ALPHABET =
|
||||
{(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F',
|
||||
(byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K',
|
||||
(byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P',
|
||||
(byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U',
|
||||
(byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z',
|
||||
(byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e',
|
||||
(byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j',
|
||||
(byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o',
|
||||
(byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't',
|
||||
(byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y',
|
||||
(byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3',
|
||||
(byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8',
|
||||
(byte) '9', (byte) '-', (byte) '_'};
|
||||
|
||||
/**
|
||||
* Translates a Base64 value to either its 6-bit reconstruction value
|
||||
* or a negative number indicating some other meaning.
|
||||
**/
|
||||
private final static byte[] DECODABET = {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8
|
||||
-5, -5, // Whitespace: Tab and Linefeed
|
||||
-9, -9, // Decimal 11 - 12
|
||||
-5, // Whitespace: Carriage Return
|
||||
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26
|
||||
-9, -9, -9, -9, -9, // Decimal 27 - 31
|
||||
-5, // Whitespace: Space
|
||||
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42
|
||||
62, // Plus sign at decimal 43
|
||||
-9, -9, -9, // Decimal 44 - 46
|
||||
63, // Slash at decimal 47
|
||||
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
|
||||
-9, -9, -9, // Decimal 58 - 60
|
||||
-1, // Equals sign at decimal 61
|
||||
-9, -9, -9, // Decimal 62 - 64
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N'
|
||||
14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z'
|
||||
-9, -9, -9, -9, -9, -9, // Decimal 91 - 96
|
||||
26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm'
|
||||
39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z'
|
||||
-9, -9, -9, -9, -9 // Decimal 123 - 127
|
||||
/* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139
|
||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152
|
||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165
|
||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178
|
||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191
|
||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204
|
||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
|
||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
|
||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
|
||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */
|
||||
};
|
||||
|
||||
/** The web safe decodabet */
|
||||
private final static byte[] WEBSAFE_DECODABET =
|
||||
{-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8
|
||||
-5, -5, // Whitespace: Tab and Linefeed
|
||||
-9, -9, // Decimal 11 - 12
|
||||
-5, // Whitespace: Carriage Return
|
||||
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26
|
||||
-9, -9, -9, -9, -9, // Decimal 27 - 31
|
||||
-5, // Whitespace: Space
|
||||
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 44
|
||||
62, // Dash '-' sign at decimal 45
|
||||
-9, -9, // Decimal 46-47
|
||||
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
|
||||
-9, -9, -9, // Decimal 58 - 60
|
||||
-1, // Equals sign at decimal 61
|
||||
-9, -9, -9, // Decimal 62 - 64
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N'
|
||||
14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z'
|
||||
-9, -9, -9, -9, // Decimal 91-94
|
||||
63, // Underscore '_' at decimal 95
|
||||
-9, // Decimal 96
|
||||
26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm'
|
||||
39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z'
|
||||
-9, -9, -9, -9, -9 // Decimal 123 - 127
|
||||
/* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139
|
||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152
|
||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165
|
||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178
|
||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191
|
||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204
|
||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
|
||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
|
||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
|
||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */
|
||||
};
|
||||
|
||||
// Indicates white space in encoding
|
||||
private final static byte WHITE_SPACE_ENC = -5;
|
||||
// Indicates equals sign in encoding
|
||||
private final static byte EQUALS_SIGN_ENC = -1;
|
||||
|
||||
/** Defeats instantiation. */
|
||||
private Base64() {
|
||||
}
|
||||
|
||||
/* ******** E N C O D I N G M E T H O D S ******** */
|
||||
|
||||
/**
|
||||
* Encodes up to three bytes of the array <var>source</var>
|
||||
* and writes the resulting four Base64 bytes to <var>destination</var>.
|
||||
* The source and destination arrays can be manipulated
|
||||
* anywhere along their length by specifying
|
||||
* <var>srcOffset</var> and <var>destOffset</var>.
|
||||
* This method does not check to make sure your arrays
|
||||
* are large enough to accommodate <var>srcOffset</var> + 3 for
|
||||
* the <var>source</var> array or <var>destOffset</var> + 4 for
|
||||
* the <var>destination</var> array.
|
||||
* The actual number of significant bytes in your array is
|
||||
* given by <var>numSigBytes</var>.
|
||||
*
|
||||
* @param source the array to convert
|
||||
* @param srcOffset the index where conversion begins
|
||||
* @param numSigBytes the number of significant bytes in your array
|
||||
* @param destination the array to hold the conversion
|
||||
* @param destOffset the index where output will be put
|
||||
* @param alphabet is the encoding alphabet
|
||||
* @return the <var>destination</var> array
|
||||
* @since 1.3
|
||||
*/
|
||||
private static byte[] encode3to4(byte[] source, int srcOffset,
|
||||
int numSigBytes, byte[] destination, int destOffset, byte[] alphabet) {
|
||||
// 1 2 3
|
||||
// 01234567890123456789012345678901 Bit position
|
||||
// --------000000001111111122222222 Array position from threeBytes
|
||||
// --------| || || || | Six bit groups to index alphabet
|
||||
// >>18 >>12 >> 6 >> 0 Right shift necessary
|
||||
// 0x3f 0x3f 0x3f Additional AND
|
||||
|
||||
// Create buffer with zero-padding if there are only one or two
|
||||
// significant bytes passed in the array.
|
||||
// We have to shift left 24 in order to flush out the 1's that appear
|
||||
// when Java treats a value as negative that is cast from a byte to an int.
|
||||
int inBuff =
|
||||
(numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0)
|
||||
| (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0)
|
||||
| (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0);
|
||||
|
||||
switch (numSigBytes) {
|
||||
case 3:
|
||||
destination[destOffset] = alphabet[(inBuff >>> 18)];
|
||||
destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
|
||||
destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f];
|
||||
destination[destOffset + 3] = alphabet[(inBuff) & 0x3f];
|
||||
return destination;
|
||||
case 2:
|
||||
destination[destOffset] = alphabet[(inBuff >>> 18)];
|
||||
destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
|
||||
destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f];
|
||||
destination[destOffset + 3] = EQUALS_SIGN;
|
||||
return destination;
|
||||
case 1:
|
||||
destination[destOffset] = alphabet[(inBuff >>> 18)];
|
||||
destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
|
||||
destination[destOffset + 2] = EQUALS_SIGN;
|
||||
destination[destOffset + 3] = EQUALS_SIGN;
|
||||
return destination;
|
||||
default:
|
||||
return destination;
|
||||
} // end switch
|
||||
} // end encode3to4
|
||||
|
||||
/**
|
||||
* Encodes a byte array into Base64 notation.
|
||||
* Equivalent to calling
|
||||
* {@code encodeBytes(source, 0, source.length)}
|
||||
*
|
||||
* @param source The data to convert
|
||||
* @since 1.4
|
||||
*/
|
||||
public static String encode(byte[] source) {
|
||||
return encode(source, 0, source.length, ALPHABET, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes a byte array into web safe Base64 notation.
|
||||
*
|
||||
* @param source The data to convert
|
||||
* @param doPadding is {@code true} to pad result with '=' chars
|
||||
* if it does not fall on 3 byte boundaries
|
||||
*/
|
||||
public static String encodeWebSafe(byte[] source, boolean doPadding) {
|
||||
return encode(source, 0, source.length, WEBSAFE_ALPHABET, doPadding);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes a byte array into Base64 notation.
|
||||
*
|
||||
* @param source the data to convert
|
||||
* @param off offset in array where conversion should begin
|
||||
* @param len length of data to convert
|
||||
* @param alphabet the encoding alphabet
|
||||
* @param doPadding is {@code true} to pad result with '=' chars
|
||||
* if it does not fall on 3 byte boundaries
|
||||
* @since 1.4
|
||||
*/
|
||||
public static String encode(byte[] source, int off, int len, byte[] alphabet,
|
||||
boolean doPadding) {
|
||||
byte[] outBuff = encode(source, off, len, alphabet, Integer.MAX_VALUE);
|
||||
int outLen = outBuff.length;
|
||||
|
||||
// If doPadding is false, set length to truncate '='
|
||||
// padding characters
|
||||
while (doPadding == false && outLen > 0) {
|
||||
if (outBuff[outLen - 1] != '=') {
|
||||
break;
|
||||
}
|
||||
outLen -= 1;
|
||||
}
|
||||
|
||||
return new String(outBuff, 0, outLen);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes a byte array into Base64 notation.
|
||||
*
|
||||
* @param source the data to convert
|
||||
* @param off offset in array where conversion should begin
|
||||
* @param len length of data to convert
|
||||
* @param alphabet is the encoding alphabet
|
||||
* @param maxLineLength maximum length of one line.
|
||||
* @return the BASE64-encoded byte array
|
||||
*/
|
||||
public static byte[] encode(byte[] source, int off, int len, byte[] alphabet,
|
||||
int maxLineLength) {
|
||||
int lenDiv3 = (len + 2) / 3; // ceil(len / 3)
|
||||
int len43 = lenDiv3 * 4;
|
||||
byte[] outBuff = new byte[len43 // Main 4:3
|
||||
+ (len43 / maxLineLength)]; // New lines
|
||||
|
||||
int d = 0;
|
||||
int e = 0;
|
||||
int len2 = len - 2;
|
||||
int lineLength = 0;
|
||||
for (; d < len2; d += 3, e += 4) {
|
||||
|
||||
// The following block of code is the same as
|
||||
// encode3to4( source, d + off, 3, outBuff, e, alphabet );
|
||||
// but inlined for faster encoding (~20% improvement)
|
||||
int inBuff =
|
||||
((source[d + off] << 24) >>> 8)
|
||||
| ((source[d + 1 + off] << 24) >>> 16)
|
||||
| ((source[d + 2 + off] << 24) >>> 24);
|
||||
outBuff[e] = alphabet[(inBuff >>> 18)];
|
||||
outBuff[e + 1] = alphabet[(inBuff >>> 12) & 0x3f];
|
||||
outBuff[e + 2] = alphabet[(inBuff >>> 6) & 0x3f];
|
||||
outBuff[e + 3] = alphabet[(inBuff) & 0x3f];
|
||||
|
||||
lineLength += 4;
|
||||
if (lineLength == maxLineLength) {
|
||||
outBuff[e + 4] = NEW_LINE;
|
||||
e++;
|
||||
lineLength = 0;
|
||||
} // end if: end of line
|
||||
} // end for: each piece of array
|
||||
|
||||
if (d < len) {
|
||||
encode3to4(source, d + off, len - d, outBuff, e, alphabet);
|
||||
|
||||
lineLength += 4;
|
||||
if (lineLength == maxLineLength) {
|
||||
// Add a last newline
|
||||
outBuff[e + 4] = NEW_LINE;
|
||||
e++;
|
||||
}
|
||||
e += 4;
|
||||
}
|
||||
|
||||
assert (e == outBuff.length);
|
||||
return outBuff;
|
||||
}
|
||||
|
||||
|
||||
/* ******** D E C O D I N G M E T H O D S ******** */
|
||||
|
||||
|
||||
/**
|
||||
* Decodes four bytes from array <var>source</var>
|
||||
* and writes the resulting bytes (up to three of them)
|
||||
* to <var>destination</var>.
|
||||
* The source and destination arrays can be manipulated
|
||||
* anywhere along their length by specifying
|
||||
* <var>srcOffset</var> and <var>destOffset</var>.
|
||||
* This method does not check to make sure your arrays
|
||||
* are large enough to accommodate <var>srcOffset</var> + 4 for
|
||||
* the <var>source</var> array or <var>destOffset</var> + 3 for
|
||||
* the <var>destination</var> array.
|
||||
* This method returns the actual number of bytes that
|
||||
* were converted from the Base64 encoding.
|
||||
*
|
||||
*
|
||||
* @param source the array to convert
|
||||
* @param srcOffset the index where conversion begins
|
||||
* @param destination the array to hold the conversion
|
||||
* @param destOffset the index where output will be put
|
||||
* @param decodabet the decodabet for decoding Base64 content
|
||||
* @return the number of decoded bytes converted
|
||||
* @since 1.3
|
||||
*/
|
||||
private static int decode4to3(byte[] source, int srcOffset,
|
||||
byte[] destination, int destOffset, byte[] decodabet) {
|
||||
// Example: Dk==
|
||||
if (source[srcOffset + 2] == EQUALS_SIGN) {
|
||||
int outBuff =
|
||||
((decodabet[source[srcOffset]] << 24) >>> 6)
|
||||
| ((decodabet[source[srcOffset + 1]] << 24) >>> 12);
|
||||
|
||||
destination[destOffset] = (byte) (outBuff >>> 16);
|
||||
return 1;
|
||||
} else if (source[srcOffset + 3] == EQUALS_SIGN) {
|
||||
// Example: DkL=
|
||||
int outBuff =
|
||||
((decodabet[source[srcOffset]] << 24) >>> 6)
|
||||
| ((decodabet[source[srcOffset + 1]] << 24) >>> 12)
|
||||
| ((decodabet[source[srcOffset + 2]] << 24) >>> 18);
|
||||
|
||||
destination[destOffset] = (byte) (outBuff >>> 16);
|
||||
destination[destOffset + 1] = (byte) (outBuff >>> 8);
|
||||
return 2;
|
||||
} else {
|
||||
// Example: DkLE
|
||||
int outBuff =
|
||||
((decodabet[source[srcOffset]] << 24) >>> 6)
|
||||
| ((decodabet[source[srcOffset + 1]] << 24) >>> 12)
|
||||
| ((decodabet[source[srcOffset + 2]] << 24) >>> 18)
|
||||
| ((decodabet[source[srcOffset + 3]] << 24) >>> 24);
|
||||
|
||||
destination[destOffset] = (byte) (outBuff >> 16);
|
||||
destination[destOffset + 1] = (byte) (outBuff >> 8);
|
||||
destination[destOffset + 2] = (byte) (outBuff);
|
||||
return 3;
|
||||
}
|
||||
} // end decodeToBytes
|
||||
|
||||
|
||||
/**
|
||||
* Decodes data from Base64 notation.
|
||||
*
|
||||
* @param s the string to decode (decoded in default encoding)
|
||||
* @return the decoded data
|
||||
* @since 1.4
|
||||
*/
|
||||
public static byte[] decode(String s) throws Base64DecoderException {
|
||||
byte[] bytes = s.getBytes();
|
||||
return decode(bytes, 0, bytes.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes data from web safe Base64 notation.
|
||||
* Web safe encoding uses '-' instead of '+', '_' instead of '/'
|
||||
*
|
||||
* @param s the string to decode (decoded in default encoding)
|
||||
* @return the decoded data
|
||||
*/
|
||||
public static byte[] decodeWebSafe(String s) throws Base64DecoderException {
|
||||
byte[] bytes = s.getBytes();
|
||||
return decodeWebSafe(bytes, 0, bytes.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes Base64 content in byte array format and returns
|
||||
* the decoded byte array.
|
||||
*
|
||||
* @param source The Base64 encoded data
|
||||
* @return decoded data
|
||||
* @since 1.3
|
||||
* @throws Base64DecoderException
|
||||
*/
|
||||
public static byte[] decode(byte[] source) throws Base64DecoderException {
|
||||
return decode(source, 0, source.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes web safe Base64 content in byte array format and returns
|
||||
* the decoded data.
|
||||
* Web safe encoding uses '-' instead of '+', '_' instead of '/'
|
||||
*
|
||||
* @param source the string to decode (decoded in default encoding)
|
||||
* @return the decoded data
|
||||
*/
|
||||
public static byte[] decodeWebSafe(byte[] source)
|
||||
throws Base64DecoderException {
|
||||
return decodeWebSafe(source, 0, source.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes Base64 content in byte array format and returns
|
||||
* the decoded byte array.
|
||||
*
|
||||
* @param source the Base64 encoded data
|
||||
* @param off the offset of where to begin decoding
|
||||
* @param len the length of characters to decode
|
||||
* @return decoded data
|
||||
* @since 1.3
|
||||
* @throws Base64DecoderException
|
||||
*/
|
||||
public static byte[] decode(byte[] source, int off, int len)
|
||||
throws Base64DecoderException {
|
||||
return decode(source, off, len, DECODABET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes web safe Base64 content in byte array format and returns
|
||||
* the decoded byte array.
|
||||
* Web safe encoding uses '-' instead of '+', '_' instead of '/'
|
||||
*
|
||||
* @param source the Base64 encoded data
|
||||
* @param off the offset of where to begin decoding
|
||||
* @param len the length of characters to decode
|
||||
* @return decoded data
|
||||
*/
|
||||
public static byte[] decodeWebSafe(byte[] source, int off, int len)
|
||||
throws Base64DecoderException {
|
||||
return decode(source, off, len, WEBSAFE_DECODABET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes Base64 content using the supplied decodabet and returns
|
||||
* the decoded byte array.
|
||||
*
|
||||
* @param source the Base64 encoded data
|
||||
* @param off the offset of where to begin decoding
|
||||
* @param len the length of characters to decode
|
||||
* @param decodabet the decodabet for decoding Base64 content
|
||||
* @return decoded data
|
||||
*/
|
||||
public static byte[] decode(byte[] source, int off, int len, byte[] decodabet)
|
||||
throws Base64DecoderException {
|
||||
int len34 = len * 3 / 4;
|
||||
byte[] outBuff = new byte[2 + len34]; // Upper limit on size of output
|
||||
int outBuffPosn = 0;
|
||||
|
||||
byte[] b4 = new byte[4];
|
||||
int b4Posn = 0;
|
||||
int i = 0;
|
||||
byte sbiCrop = 0;
|
||||
byte sbiDecode = 0;
|
||||
for (i = 0; i < len; i++) {
|
||||
sbiCrop = (byte) (source[i + off] & 0x7f); // Only the low seven bits
|
||||
sbiDecode = decodabet[sbiCrop];
|
||||
|
||||
if (sbiDecode >= WHITE_SPACE_ENC) { // White space Equals sign or better
|
||||
if (sbiDecode >= EQUALS_SIGN_ENC) {
|
||||
// An equals sign (for padding) must not occur at position 0 or 1
|
||||
// and must be the last byte[s] in the encoded value
|
||||
if (sbiCrop == EQUALS_SIGN) {
|
||||
int bytesLeft = len - i;
|
||||
byte lastByte = (byte) (source[len - 1 + off] & 0x7f);
|
||||
if (b4Posn == 0 || b4Posn == 1) {
|
||||
throw new Base64DecoderException(
|
||||
"invalid padding byte '=' at byte offset " + i);
|
||||
} else if ((b4Posn == 3 && bytesLeft > 2)
|
||||
|| (b4Posn == 4 && bytesLeft > 1)) {
|
||||
throw new Base64DecoderException(
|
||||
"padding byte '=' falsely signals end of encoded value "
|
||||
+ "at offset " + i);
|
||||
} else if (lastByte != EQUALS_SIGN && lastByte != NEW_LINE) {
|
||||
throw new Base64DecoderException(
|
||||
"encoded value has invalid trailing byte");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
b4[b4Posn++] = sbiCrop;
|
||||
if (b4Posn == 4) {
|
||||
outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet);
|
||||
b4Posn = 0;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new Base64DecoderException("Bad Base64 input character at " + i
|
||||
+ ": " + source[i + off] + "(decimal)");
|
||||
}
|
||||
}
|
||||
|
||||
// Because web safe encoding allows non padding base64 encodes, we
|
||||
// need to pad the rest of the b4 buffer with equal signs when
|
||||
// b4Posn != 0. There can be at most 2 equal signs at the end of
|
||||
// four characters, so the b4 buffer must have two or three
|
||||
// characters. This also catches the case where the input is
|
||||
// padded with EQUALS_SIGN
|
||||
if (b4Posn != 0) {
|
||||
if (b4Posn == 1) {
|
||||
throw new Base64DecoderException("single trailing character at offset "
|
||||
+ (len - 1));
|
||||
}
|
||||
b4[b4Posn++] = EQUALS_SIGN;
|
||||
outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet);
|
||||
}
|
||||
|
||||
byte[] out = new byte[outBuffPosn];
|
||||
System.arraycopy(outBuff, 0, out, 0, outBuffPosn);
|
||||
return out;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
// 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;
|
||||
|
||||
/**
|
||||
* Exception thrown when encountering an invalid Base64 input character.
|
||||
*
|
||||
* @author nelson
|
||||
*/
|
||||
public class Base64DecoderException extends Exception {
|
||||
public Base64DecoderException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public Base64DecoderException(String s) {
|
||||
super(s);
|
||||
}
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
/* Copyright (c) 2012 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;
|
||||
|
||||
/**
|
||||
* Exception thrown when something went wrong with in-app billing.
|
||||
* An IabException has an associated IabResult (an error).
|
||||
* To get the IAB result that caused this exception to be thrown,
|
||||
* call {@link #getResult()}.
|
||||
*/
|
||||
public class IabException extends Exception {
|
||||
IabResult mResult;
|
||||
|
||||
public IabException(IabResult r) {
|
||||
this(r, null);
|
||||
}
|
||||
public IabException(int response, String message) {
|
||||
this(new IabResult(response, message));
|
||||
}
|
||||
public IabException(IabResult r, Exception cause) {
|
||||
super(r.getMessage(), cause);
|
||||
mResult = r;
|
||||
}
|
||||
public IabException(int response, String message, Exception cause) {
|
||||
this(new IabResult(response, message), cause);
|
||||
}
|
||||
|
||||
/** Returns the IAB result (error) that this exception signals. */
|
||||
public IabResult getResult() { return mResult; }
|
||||
}
|
991
app/src/main/java/org/billinghack/google/util/IabHelper.java
Normal file
|
@ -0,0 +1,991 @@
|
|||
/* Copyright (c) 2012 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;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentSender.SendIntentException;
|
||||
import android.content.ServiceConnection;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteException;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.vending.billing.IInAppBillingService;
|
||||
|
||||
import org.json.JSONException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/**
|
||||
* Provides convenience methods for in-app billing. You can create one instance of this
|
||||
* class for your application and use it to process in-app billing operations.
|
||||
* It provides synchronous (blocking) and asynchronous (non-blocking) methods for
|
||||
* many common in-app billing operations, as well as automatic signature
|
||||
* verification.
|
||||
*
|
||||
* After instantiating, you must perform setup in order to start using the object.
|
||||
* To perform setup, call the {@link #startSetup} method and provide a listener;
|
||||
* that listener will be notified when setup is complete, after which (and not before)
|
||||
* you may call other methods.
|
||||
*
|
||||
* After setup is complete, you will typically want to request an inventory of owned
|
||||
* items and subscriptions. See {@link #queryInventory}, {@link #queryInventoryAsync}
|
||||
* and related methods.
|
||||
*
|
||||
* When you are done with this object, don't forget to call {@link #dispose}
|
||||
* to ensure proper cleanup. This object holds a binding to the in-app billing
|
||||
* service, which will leak unless you dispose of it correctly. If you created
|
||||
* the object on an Activity's onCreate method, then the recommended
|
||||
* place to dispose of it is the Activity's onDestroy method.
|
||||
*
|
||||
* A note about threading: When using this object from a background thread, you may
|
||||
* call the blocking versions of methods; when using from a UI thread, call
|
||||
* only the asynchronous versions and handle the results via callbacks.
|
||||
* Also, notice that you can only call one asynchronous operation at a time;
|
||||
* attempting to start a second asynchronous operation while the first one
|
||||
* has not yet completed will result in an exception being thrown.
|
||||
*
|
||||
* @author Bruno Oliveira (Google)
|
||||
*
|
||||
*/
|
||||
public class IabHelper {
|
||||
// Is debug logging enabled?
|
||||
boolean mDebugLog = false;
|
||||
String mDebugTag = "IabHelper";
|
||||
|
||||
// Is setup done?
|
||||
boolean mSetupDone = false;
|
||||
|
||||
// Has this object been disposed of? (If so, we should ignore callbacks, etc)
|
||||
boolean mDisposed = false;
|
||||
|
||||
// Are subscriptions supported?
|
||||
boolean mSubscriptionsSupported = false;
|
||||
|
||||
// Is an asynchronous operation in progress?
|
||||
// (only one at a time can be in progress)
|
||||
boolean mAsyncInProgress = false;
|
||||
|
||||
// (for logging/debugging)
|
||||
// if mAsyncInProgress == true, what asynchronous operation is in progress?
|
||||
String mAsyncOperation = "";
|
||||
|
||||
// Context we were passed during initialization
|
||||
Context mContext;
|
||||
|
||||
// Connection to the service
|
||||
IInAppBillingService mService;
|
||||
ServiceConnection mServiceConn;
|
||||
|
||||
// The request code used to launch purchase flow
|
||||
int mRequestCode;
|
||||
|
||||
// The item type of the current purchase flow
|
||||
String mPurchasingItemType;
|
||||
|
||||
// Public key for verifying signature, in base64 encoding
|
||||
String mSignatureBase64 = null;
|
||||
|
||||
// Billing response codes
|
||||
public static final int BILLING_RESPONSE_RESULT_OK = 0;
|
||||
public static final int BILLING_RESPONSE_RESULT_USER_CANCELED = 1;
|
||||
public static final int BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE = 3;
|
||||
public static final int BILLING_RESPONSE_RESULT_ITEM_UNAVAILABLE = 4;
|
||||
public static final int BILLING_RESPONSE_RESULT_DEVELOPER_ERROR = 5;
|
||||
public static final int BILLING_RESPONSE_RESULT_ERROR = 6;
|
||||
public static final int BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED = 7;
|
||||
public static final int BILLING_RESPONSE_RESULT_ITEM_NOT_OWNED = 8;
|
||||
|
||||
// IAB Helper error codes
|
||||
public static final int IABHELPER_ERROR_BASE = -1000;
|
||||
public static final int IABHELPER_REMOTE_EXCEPTION = -1001;
|
||||
public static final int IABHELPER_BAD_RESPONSE = -1002;
|
||||
public static final int IABHELPER_VERIFICATION_FAILED = -1003;
|
||||
public static final int IABHELPER_SEND_INTENT_FAILED = -1004;
|
||||
public static final int IABHELPER_USER_CANCELLED = -1005;
|
||||
public static final int IABHELPER_UNKNOWN_PURCHASE_RESPONSE = -1006;
|
||||
public static final int IABHELPER_MISSING_TOKEN = -1007;
|
||||
public static final int IABHELPER_UNKNOWN_ERROR = -1008;
|
||||
public static final int IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE = -1009;
|
||||
public static final int IABHELPER_INVALID_CONSUMPTION = -1010;
|
||||
|
||||
// Keys for the responses from InAppBillingService
|
||||
public static final String RESPONSE_CODE = "RESPONSE_CODE";
|
||||
public static final String RESPONSE_GET_SKU_DETAILS_LIST = "DETAILS_LIST";
|
||||
public static final String RESPONSE_BUY_INTENT = "BUY_INTENT";
|
||||
public static final String RESPONSE_INAPP_PURCHASE_DATA = "INAPP_PURCHASE_DATA";
|
||||
public static final String RESPONSE_INAPP_SIGNATURE = "INAPP_DATA_SIGNATURE";
|
||||
public static final String RESPONSE_INAPP_ITEM_LIST = "INAPP_PURCHASE_ITEM_LIST";
|
||||
public static final String RESPONSE_INAPP_PURCHASE_DATA_LIST = "INAPP_PURCHASE_DATA_LIST";
|
||||
public static final String RESPONSE_INAPP_SIGNATURE_LIST = "INAPP_DATA_SIGNATURE_LIST";
|
||||
public static final String INAPP_CONTINUATION_TOKEN = "INAPP_CONTINUATION_TOKEN";
|
||||
|
||||
// Item types
|
||||
public static final String ITEM_TYPE_INAPP = "inapp";
|
||||
public static final String ITEM_TYPE_SUBS = "subs";
|
||||
|
||||
// some fields on the getSkuDetails response bundle
|
||||
public static final String GET_SKU_DETAILS_ITEM_LIST = "ITEM_ID_LIST";
|
||||
public static final String GET_SKU_DETAILS_ITEM_TYPE_LIST = "ITEM_TYPE_LIST";
|
||||
|
||||
/**
|
||||
* Creates an instance. After creation, it will not yet be ready to use. You must perform
|
||||
* setup by calling {@link #startSetup} and wait for setup to complete. This constructor does not
|
||||
* block and is safe to call from a UI thread.
|
||||
*
|
||||
* @param ctx Your application or Activity context. Needed to bind to the in-app billing service.
|
||||
* @param base64PublicKey Your application's public key, encoded in base64.
|
||||
* This is used for verification of purchase signatures. You can find your app's base64-encoded
|
||||
* public key in your application's page on Google Play Developer Console. Note that this
|
||||
* is NOT your "developer public key".
|
||||
*/
|
||||
public IabHelper(Context ctx, String base64PublicKey) {
|
||||
mContext = ctx.getApplicationContext();
|
||||
mSignatureBase64 = base64PublicKey;
|
||||
logDebug("IAB helper created.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables or disable debug logging through LogCat.
|
||||
*/
|
||||
public void enableDebugLogging(boolean enable, String tag) {
|
||||
checkNotDisposed();
|
||||
mDebugLog = enable;
|
||||
mDebugTag = tag;
|
||||
}
|
||||
|
||||
public void enableDebugLogging(boolean enable) {
|
||||
checkNotDisposed();
|
||||
mDebugLog = enable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for setup process. This listener's {@link #onIabSetupFinished} method is called
|
||||
* when the setup process is complete.
|
||||
*/
|
||||
public interface OnIabSetupFinishedListener {
|
||||
/**
|
||||
* Called to notify that setup is complete.
|
||||
*
|
||||
* @param result The result of the setup process.
|
||||
*/
|
||||
public void onIabSetupFinished(IabResult result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the setup process. This will start up the setup process asynchronously.
|
||||
* You will be notified through the listener when the setup process is complete.
|
||||
* This method is safe to call from a UI thread.
|
||||
*
|
||||
* @param listener The listener to notify when the setup process is complete.
|
||||
*/
|
||||
public void startSetup(final OnIabSetupFinishedListener listener) {
|
||||
// If already set up, can't do it again.
|
||||
checkNotDisposed();
|
||||
if (mSetupDone) throw new IllegalStateException("IAB helper is already set up.");
|
||||
|
||||
// Connection to IAB service
|
||||
logDebug("Starting in-app billing setup.");
|
||||
mServiceConn = new ServiceConnection() {
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
logDebug("Billing service disconnected.");
|
||||
mService = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
if (mDisposed) return;
|
||||
logDebug("Billing service connected.");
|
||||
mService = IInAppBillingService.Stub.asInterface(service);
|
||||
String packageName = mContext.getPackageName();
|
||||
try {
|
||||
logDebug("Checking for in-app billing 3 support.");
|
||||
|
||||
// check for in-app billing v3 support
|
||||
int response = mService.isBillingSupported(3, packageName, ITEM_TYPE_INAPP);
|
||||
if (response != BILLING_RESPONSE_RESULT_OK) {
|
||||
if (listener != null) listener.onIabSetupFinished(new IabResult(response,
|
||||
"Error checking for billing v3 support."));
|
||||
|
||||
// if in-app purchases aren't supported, neither are subscriptions.
|
||||
mSubscriptionsSupported = false;
|
||||
return;
|
||||
}
|
||||
logDebug("In-app billing version 3 supported for " + packageName);
|
||||
|
||||
// check for v3 subscriptions support
|
||||
response = mService.isBillingSupported(3, packageName, ITEM_TYPE_SUBS);
|
||||
if (response == BILLING_RESPONSE_RESULT_OK) {
|
||||
logDebug("Subscriptions AVAILABLE.");
|
||||
mSubscriptionsSupported = true;
|
||||
}
|
||||
else {
|
||||
logDebug("Subscriptions NOT AVAILABLE. Response: " + response);
|
||||
}
|
||||
|
||||
mSetupDone = true;
|
||||
}
|
||||
catch (RemoteException e) {
|
||||
if (listener != null) {
|
||||
listener.onIabSetupFinished(new IabResult(IABHELPER_REMOTE_EXCEPTION,
|
||||
"RemoteException while setting up in-app billing."));
|
||||
}
|
||||
e.printStackTrace();
|
||||
return;
|
||||
}
|
||||
|
||||
if (listener != null) {
|
||||
listener.onIabSetupFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Setup successful."));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Intent serviceIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND");
|
||||
if (!mContext.getPackageManager().queryIntentServices(serviceIntent, 0).isEmpty()) {
|
||||
// service available to handle that Intent
|
||||
mContext.bindService(serviceIntent, mServiceConn, Context.BIND_AUTO_CREATE);
|
||||
}
|
||||
else {
|
||||
// no service available to handle that Intent
|
||||
if (listener != null) {
|
||||
listener.onIabSetupFinished(
|
||||
new IabResult(BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE,
|
||||
"Billing service unavailable on device."));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose of object, releasing resources. It's very important to call this
|
||||
* method when you are done with this object. It will release any resources
|
||||
* used by it such as service connections. Naturally, once the object is
|
||||
* disposed of, it can't be used again.
|
||||
*/
|
||||
public void dispose() {
|
||||
logDebug("Disposing.");
|
||||
mSetupDone = false;
|
||||
if (mServiceConn != null) {
|
||||
logDebug("Unbinding from service.");
|
||||
if (mContext != null) mContext.unbindService(mServiceConn);
|
||||
}
|
||||
mDisposed = true;
|
||||
mContext = null;
|
||||
mServiceConn = null;
|
||||
mService = null;
|
||||
mPurchaseListener = null;
|
||||
}
|
||||
|
||||
private void checkNotDisposed() {
|
||||
if (mDisposed) throw new IllegalStateException("IabHelper was disposed of, so it cannot be used.");
|
||||
}
|
||||
|
||||
/** Returns whether subscriptions are supported. */
|
||||
public boolean subscriptionsSupported() {
|
||||
checkNotDisposed();
|
||||
return mSubscriptionsSupported;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Callback that notifies when a purchase is finished.
|
||||
*/
|
||||
public interface OnIabPurchaseFinishedListener {
|
||||
/**
|
||||
* Called to notify that an in-app purchase finished. If the purchase was successful,
|
||||
* then the sku parameter specifies which item was purchased. If the purchase failed,
|
||||
* the sku and extraData parameters may or may not be null, depending on how far the purchase
|
||||
* process went.
|
||||
*
|
||||
* @param result The result of the purchase.
|
||||
* @param info The purchase information (null if purchase failed)
|
||||
*/
|
||||
public void onIabPurchaseFinished(IabResult result, Purchase info);
|
||||
}
|
||||
|
||||
// The listener registered on launchPurchaseFlow, which we have to call back when
|
||||
// the purchase finishes
|
||||
OnIabPurchaseFinishedListener mPurchaseListener;
|
||||
|
||||
public void launchPurchaseFlow(Activity act, String sku, int requestCode, OnIabPurchaseFinishedListener listener) {
|
||||
launchPurchaseFlow(act, sku, requestCode, listener, "");
|
||||
}
|
||||
|
||||
public void launchPurchaseFlow(Activity act, String sku, int requestCode,
|
||||
OnIabPurchaseFinishedListener listener, String extraData) {
|
||||
launchPurchaseFlow(act, sku, ITEM_TYPE_INAPP, requestCode, listener, extraData);
|
||||
}
|
||||
|
||||
public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode,
|
||||
OnIabPurchaseFinishedListener listener) {
|
||||
launchSubscriptionPurchaseFlow(act, sku, requestCode, listener, "");
|
||||
}
|
||||
|
||||
public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode,
|
||||
OnIabPurchaseFinishedListener listener, String extraData) {
|
||||
launchPurchaseFlow(act, sku, ITEM_TYPE_SUBS, requestCode, listener, extraData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate the UI flow for an in-app purchase. Call this method to initiate an in-app purchase,
|
||||
* which will involve bringing up the Google Play screen. The calling activity will be paused while
|
||||
* the user interacts with Google Play, and the result will be delivered via the activity's
|
||||
* {@link android.app.Activity#onActivityResult} method, at which point you must call
|
||||
* this object's {@link #handleActivityResult} method to continue the purchase flow. This method
|
||||
* MUST be called from the UI thread of the Activity.
|
||||
*
|
||||
* @param act The calling activity.
|
||||
* @param sku The sku of the item to purchase.
|
||||
* @param itemType indicates if it's a product or a subscription (ITEM_TYPE_INAPP or ITEM_TYPE_SUBS)
|
||||
* @param requestCode A request code (to differentiate from other responses --
|
||||
* as in {@link android.app.Activity#startActivityForResult}).
|
||||
* @param listener The listener to notify when the purchase process finishes
|
||||
* @param extraData Extra data (developer payload), which will be returned with the purchase data
|
||||
* when the purchase completes. This extra data will be permanently bound to that purchase
|
||||
* and will always be returned when the purchase is queried.
|
||||
*/
|
||||
public void launchPurchaseFlow(Activity act, String sku, String itemType, int requestCode,
|
||||
OnIabPurchaseFinishedListener listener, String extraData) {
|
||||
checkNotDisposed();
|
||||
checkSetupDone("launchPurchaseFlow");
|
||||
flagStartAsync("launchPurchaseFlow");
|
||||
IabResult result;
|
||||
|
||||
if (itemType.equals(ITEM_TYPE_SUBS) && !mSubscriptionsSupported) {
|
||||
IabResult r = new IabResult(IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE,
|
||||
"Subscriptions are not available.");
|
||||
flagEndAsync();
|
||||
if (listener != null) listener.onIabPurchaseFinished(r, null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
logDebug("Constructing buy intent for " + sku + ", item type: " + itemType);
|
||||
Bundle buyIntentBundle = mService.getBuyIntent(3, mContext.getPackageName(), sku, itemType, extraData);
|
||||
int response = getResponseCodeFromBundle(buyIntentBundle);
|
||||
if (response != BILLING_RESPONSE_RESULT_OK) {
|
||||
logError("Unable to buy item, Error response: " + getResponseDesc(response));
|
||||
flagEndAsync();
|
||||
result = new IabResult(response, "Unable to buy item");
|
||||
if (listener != null) listener.onIabPurchaseFinished(result, null);
|
||||
return;
|
||||
}
|
||||
|
||||
PendingIntent pendingIntent = buyIntentBundle.getParcelable(RESPONSE_BUY_INTENT);
|
||||
logDebug("Launching buy intent for " + sku + ". Request code: " + requestCode);
|
||||
mRequestCode = requestCode;
|
||||
mPurchaseListener = listener;
|
||||
mPurchasingItemType = itemType;
|
||||
act.startIntentSenderForResult(pendingIntent.getIntentSender(),
|
||||
requestCode, new Intent(),
|
||||
Integer.valueOf(0), Integer.valueOf(0),
|
||||
Integer.valueOf(0));
|
||||
}
|
||||
catch (SendIntentException e) {
|
||||
logError("SendIntentException while launching purchase flow for sku " + sku);
|
||||
e.printStackTrace();
|
||||
flagEndAsync();
|
||||
|
||||
result = new IabResult(IABHELPER_SEND_INTENT_FAILED, "Failed to send intent.");
|
||||
if (listener != null) listener.onIabPurchaseFinished(result, null);
|
||||
}
|
||||
catch (RemoteException e) {
|
||||
logError("RemoteException while launching purchase flow for sku " + sku);
|
||||
e.printStackTrace();
|
||||
flagEndAsync();
|
||||
|
||||
result = new IabResult(IABHELPER_REMOTE_EXCEPTION, "Remote exception while starting purchase flow");
|
||||
if (listener != null) listener.onIabPurchaseFinished(result, null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles an activity result that's part of the purchase flow in in-app billing. If you
|
||||
* are calling {@link #launchPurchaseFlow}, then you must call this method from your
|
||||
* Activity's {@link android.app.Activity@onActivityResult} method. This method
|
||||
* MUST be called from the UI thread of the Activity.
|
||||
*
|
||||
* @param requestCode The requestCode as you received it.
|
||||
* @param resultCode The resultCode as you received it.
|
||||
* @param data The data (Intent) as you received it.
|
||||
* @return Returns true if the result was related to a purchase flow and was handled;
|
||||
* false if the result was not related to a purchase, in which case you should
|
||||
* handle it normally.
|
||||
*/
|
||||
public boolean handleActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
IabResult result;
|
||||
if (requestCode != mRequestCode) return false;
|
||||
|
||||
checkNotDisposed();
|
||||
checkSetupDone("handleActivityResult");
|
||||
|
||||
// end of async purchase operation that started on launchPurchaseFlow
|
||||
flagEndAsync();
|
||||
|
||||
if (data == null) {
|
||||
logError("Null data in IAB activity result.");
|
||||
result = new IabResult(IABHELPER_BAD_RESPONSE, "Null data in IAB result");
|
||||
if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
|
||||
return true;
|
||||
}
|
||||
|
||||
int responseCode = getResponseCodeFromIntent(data);
|
||||
String purchaseData = data.getStringExtra(RESPONSE_INAPP_PURCHASE_DATA);
|
||||
String dataSignature = data.getStringExtra(RESPONSE_INAPP_SIGNATURE);
|
||||
|
||||
if (resultCode == Activity.RESULT_OK && responseCode == BILLING_RESPONSE_RESULT_OK) {
|
||||
logDebug("Successful resultcode from purchase activity.");
|
||||
logDebug("Purchase data: " + purchaseData);
|
||||
logDebug("Data signature: " + dataSignature);
|
||||
logDebug("Extras: " + data.getExtras());
|
||||
logDebug("Expected item type: " + mPurchasingItemType);
|
||||
|
||||
if (purchaseData == null || dataSignature == null) {
|
||||
logError("BUG: either purchaseData or dataSignature is null.");
|
||||
logDebug("Extras: " + data.getExtras().toString());
|
||||
result = new IabResult(IABHELPER_UNKNOWN_ERROR, "IAB returned null purchaseData or dataSignature");
|
||||
if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
|
||||
return true;
|
||||
}
|
||||
|
||||
Purchase purchase = null;
|
||||
try {
|
||||
purchase = new Purchase(mPurchasingItemType, purchaseData, dataSignature);
|
||||
String sku = purchase.getSku();
|
||||
|
||||
// Verify signature
|
||||
if (!Security.verifyPurchase(mSignatureBase64, purchaseData, dataSignature)) {
|
||||
logError("Purchase signature verification FAILED for sku " + sku);
|
||||
result = new IabResult(IABHELPER_VERIFICATION_FAILED, "Signature verification failed for sku " + sku);
|
||||
if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, purchase);
|
||||
return true;
|
||||
}
|
||||
logDebug("Purchase signature successfully verified.");
|
||||
}
|
||||
catch (JSONException e) {
|
||||
logError("Failed to parse purchase data.");
|
||||
e.printStackTrace();
|
||||
result = new IabResult(IABHELPER_BAD_RESPONSE, "Failed to parse purchase data.");
|
||||
if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (mPurchaseListener != null) {
|
||||
mPurchaseListener.onIabPurchaseFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Success"), purchase);
|
||||
}
|
||||
}
|
||||
else if (resultCode == Activity.RESULT_OK) {
|
||||
// result code was OK, but in-app billing response was not OK.
|
||||
logDebug("Result code was OK but in-app billing response was not OK: " + getResponseDesc(responseCode));
|
||||
if (mPurchaseListener != null) {
|
||||
result = new IabResult(responseCode, "Problem purchashing item.");
|
||||
mPurchaseListener.onIabPurchaseFinished(result, null);
|
||||
}
|
||||
}
|
||||
else if (resultCode == Activity.RESULT_CANCELED) {
|
||||
logDebug("Purchase canceled - Response: " + getResponseDesc(responseCode));
|
||||
result = new IabResult(IABHELPER_USER_CANCELLED, "User canceled.");
|
||||
if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
|
||||
}
|
||||
else {
|
||||
logError("Purchase failed. Result code: " + Integer.toString(resultCode)
|
||||
+ ". Response: " + getResponseDesc(responseCode));
|
||||
result = new IabResult(IABHELPER_UNKNOWN_PURCHASE_RESPONSE, "Unknown purchase response.");
|
||||
if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public Inventory queryInventory(boolean querySkuDetails, List<String> moreSkus) throws IabException {
|
||||
return queryInventory(querySkuDetails, moreSkus, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries the inventory. This will query all owned items from the server, as well as
|
||||
* information on additional skus, if specified. This method may block or take long to execute.
|
||||
* Do not call from a UI thread. For that, use the non-blocking version {@link #refreshInventoryAsync}.
|
||||
*
|
||||
* @param querySkuDetails if true, SKU details (price, description, etc) will be queried as well
|
||||
* as purchase information.
|
||||
* @param moreItemSkus additional PRODUCT skus to query information on, regardless of ownership.
|
||||
* Ignored if null or if querySkuDetails is false.
|
||||
* @param moreSubsSkus additional SUBSCRIPTIONS skus to query information on, regardless of ownership.
|
||||
* Ignored if null or if querySkuDetails is false.
|
||||
* @throws IabException if a problem occurs while refreshing the inventory.
|
||||
*/
|
||||
public Inventory queryInventory(boolean querySkuDetails, List<String> moreItemSkus,
|
||||
List<String> moreSubsSkus) throws IabException {
|
||||
checkNotDisposed();
|
||||
checkSetupDone("queryInventory");
|
||||
try {
|
||||
Inventory inv = new Inventory();
|
||||
int r = queryPurchases(inv, ITEM_TYPE_INAPP);
|
||||
if (r != BILLING_RESPONSE_RESULT_OK) {
|
||||
throw new IabException(r, "Error refreshing inventory (querying owned items).");
|
||||
}
|
||||
|
||||
if (querySkuDetails) {
|
||||
r = querySkuDetails(ITEM_TYPE_INAPP, inv, moreItemSkus);
|
||||
if (r != BILLING_RESPONSE_RESULT_OK) {
|
||||
throw new IabException(r, "Error refreshing inventory (querying prices of items).");
|
||||
}
|
||||
}
|
||||
|
||||
// if subscriptions are supported, then also query for subscriptions
|
||||
if (mSubscriptionsSupported) {
|
||||
r = queryPurchases(inv, ITEM_TYPE_SUBS);
|
||||
if (r != BILLING_RESPONSE_RESULT_OK) {
|
||||
throw new IabException(r, "Error refreshing inventory (querying owned subscriptions).");
|
||||
}
|
||||
|
||||
if (querySkuDetails) {
|
||||
r = querySkuDetails(ITEM_TYPE_SUBS, inv, moreItemSkus);
|
||||
if (r != BILLING_RESPONSE_RESULT_OK) {
|
||||
throw new IabException(r, "Error refreshing inventory (querying prices of subscriptions).");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return inv;
|
||||
}
|
||||
catch (RemoteException e) {
|
||||
throw new IabException(IABHELPER_REMOTE_EXCEPTION, "Remote exception while refreshing inventory.", e);
|
||||
}
|
||||
catch (JSONException e) {
|
||||
throw new IabException(IABHELPER_BAD_RESPONSE, "Error parsing JSON response while refreshing inventory.", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener that notifies when an inventory query operation completes.
|
||||
*/
|
||||
public interface QueryInventoryFinishedListener {
|
||||
/**
|
||||
* Called to notify that an inventory query operation completed.
|
||||
*
|
||||
* @param result The result of the operation.
|
||||
* @param inv The inventory.
|
||||
*/
|
||||
public void onQueryInventoryFinished(IabResult result, Inventory inv);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Asynchronous wrapper for inventory query. This will perform an inventory
|
||||
* query as described in {@link #queryInventory}, but will do so asynchronously
|
||||
* and call back the specified listener upon completion. This method is safe to
|
||||
* call from a UI thread.
|
||||
*
|
||||
* @param querySkuDetails as in {@link #queryInventory}
|
||||
* @param moreSkus as in {@link #queryInventory}
|
||||
* @param listener The listener to notify when the refresh operation completes.
|
||||
*/
|
||||
public void queryInventoryAsync(final boolean querySkuDetails,
|
||||
final List<String> moreSkus,
|
||||
final QueryInventoryFinishedListener listener) {
|
||||
final Handler handler = new Handler();
|
||||
checkNotDisposed();
|
||||
checkSetupDone("queryInventory");
|
||||
flagStartAsync("refresh inventory");
|
||||
(new Thread(new Runnable() {
|
||||
public void run() {
|
||||
IabResult result = new IabResult(BILLING_RESPONSE_RESULT_OK, "Inventory refresh successful.");
|
||||
Inventory inv = null;
|
||||
try {
|
||||
inv = queryInventory(querySkuDetails, moreSkus);
|
||||
}
|
||||
catch (IabException ex) {
|
||||
result = ex.getResult();
|
||||
}
|
||||
|
||||
flagEndAsync();
|
||||
|
||||
final IabResult result_f = result;
|
||||
final Inventory inv_f = inv;
|
||||
if (!mDisposed && listener != null) {
|
||||
handler.post(new Runnable() {
|
||||
public void run() {
|
||||
listener.onQueryInventoryFinished(result_f, inv_f);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
})).start();
|
||||
}
|
||||
|
||||
public void queryInventoryAsync(QueryInventoryFinishedListener listener) {
|
||||
queryInventoryAsync(true, null, listener);
|
||||
}
|
||||
|
||||
public void queryInventoryAsync(boolean querySkuDetails, QueryInventoryFinishedListener listener) {
|
||||
queryInventoryAsync(querySkuDetails, null, listener);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Consumes a given in-app product. Consuming can only be done on an item
|
||||
* that's owned, and as a result of consumption, the user will no longer own it.
|
||||
* This method may block or take long to return. Do not call from the UI thread.
|
||||
* For that, see {@link #consumeAsync}.
|
||||
*
|
||||
* @param itemInfo The PurchaseInfo that represents the item to consume.
|
||||
* @throws IabException if there is a problem during consumption.
|
||||
*/
|
||||
void consume(Purchase itemInfo) throws IabException {
|
||||
checkNotDisposed();
|
||||
checkSetupDone("consume");
|
||||
|
||||
if (!itemInfo.mItemType.equals(ITEM_TYPE_INAPP)) {
|
||||
throw new IabException(IABHELPER_INVALID_CONSUMPTION,
|
||||
"Items of type '" + itemInfo.mItemType + "' can't be consumed.");
|
||||
}
|
||||
|
||||
try {
|
||||
String token = itemInfo.getToken();
|
||||
String sku = itemInfo.getSku();
|
||||
if (token == null || token.equals("")) {
|
||||
logError("Can't consume "+ sku + ". No token.");
|
||||
throw new IabException(IABHELPER_MISSING_TOKEN, "PurchaseInfo is missing token for sku: "
|
||||
+ sku + " " + itemInfo);
|
||||
}
|
||||
|
||||
logDebug("Consuming sku: " + sku + ", token: " + token);
|
||||
int response = mService.consumePurchase(3, mContext.getPackageName(), token);
|
||||
if (response == BILLING_RESPONSE_RESULT_OK) {
|
||||
logDebug("Successfully consumed sku: " + sku);
|
||||
}
|
||||
else {
|
||||
logDebug("Error consuming consuming sku " + sku + ". " + getResponseDesc(response));
|
||||
throw new IabException(response, "Error consuming sku " + sku);
|
||||
}
|
||||
}
|
||||
catch (RemoteException e) {
|
||||
throw new IabException(IABHELPER_REMOTE_EXCEPTION, "Remote exception while consuming. PurchaseInfo: " + itemInfo, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback that notifies when a consumption operation finishes.
|
||||
*/
|
||||
public interface OnConsumeFinishedListener {
|
||||
/**
|
||||
* Called to notify that a consumption has finished.
|
||||
*
|
||||
* @param purchase The purchase that was (or was to be) consumed.
|
||||
* @param result The result of the consumption operation.
|
||||
*/
|
||||
public void onConsumeFinished(Purchase purchase, IabResult result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback that notifies when a multi-item consumption operation finishes.
|
||||
*/
|
||||
public interface OnConsumeMultiFinishedListener {
|
||||
/**
|
||||
* Called to notify that a consumption of multiple items has finished.
|
||||
*
|
||||
* @param purchases The purchases that were (or were to be) consumed.
|
||||
* @param results The results of each consumption operation, corresponding to each
|
||||
* sku.
|
||||
*/
|
||||
public void onConsumeMultiFinished(List<Purchase> purchases, List<IabResult> results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronous wrapper to item consumption. Works like {@link #consume}, but
|
||||
* performs the consumption in the background and notifies completion through
|
||||
* the provided listener. This method is safe to call from a UI thread.
|
||||
*
|
||||
* @param purchase The purchase to be consumed.
|
||||
* @param listener The listener to notify when the consumption operation finishes.
|
||||
*/
|
||||
public void consumeAsync(Purchase purchase, OnConsumeFinishedListener listener) {
|
||||
checkNotDisposed();
|
||||
checkSetupDone("consume");
|
||||
List<Purchase> purchases = new ArrayList<Purchase>();
|
||||
purchases.add(purchase);
|
||||
consumeAsyncInternal(purchases, listener, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as {@link consumeAsync}, but for multiple items at once.
|
||||
* @param purchases The list of PurchaseInfo objects representing the purchases to consume.
|
||||
* @param listener The listener to notify when the consumption operation finishes.
|
||||
*/
|
||||
public void consumeAsync(List<Purchase> purchases, OnConsumeMultiFinishedListener listener) {
|
||||
checkNotDisposed();
|
||||
checkSetupDone("consume");
|
||||
consumeAsyncInternal(purchases, null, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a human-readable description for the given response code.
|
||||
*
|
||||
* @param code The response code
|
||||
* @return A human-readable string explaining the result code.
|
||||
* It also includes the result code numerically.
|
||||
*/
|
||||
public static String getResponseDesc(int code) {
|
||||
String[] iab_msgs = ("0:OK/1:User Canceled/2:Unknown/" +
|
||||
"3:Billing Unavailable/4:Item unavailable/" +
|
||||
"5:Developer Error/6:Error/7:Item Already Owned/" +
|
||||
"8:Item not owned").split("/");
|
||||
String[] iabhelper_msgs = ("0:OK/-1001:Remote exception during initialization/" +
|
||||
"-1002:Bad response received/" +
|
||||
"-1003:Purchase signature verification failed/" +
|
||||
"-1004:Send intent failed/" +
|
||||
"-1005:User cancelled/" +
|
||||
"-1006:Unknown purchase response/" +
|
||||
"-1007:Missing token/" +
|
||||
"-1008:Unknown error/" +
|
||||
"-1009:Subscriptions not available/" +
|
||||
"-1010:Invalid consumption attempt").split("/");
|
||||
|
||||
if (code <= IABHELPER_ERROR_BASE) {
|
||||
int index = IABHELPER_ERROR_BASE - code;
|
||||
if (index >= 0 && index < iabhelper_msgs.length) return iabhelper_msgs[index];
|
||||
else return String.valueOf(code) + ":Unknown IAB Helper Error";
|
||||
}
|
||||
else if (code < 0 || code >= iab_msgs.length)
|
||||
return String.valueOf(code) + ":Unknown";
|
||||
else
|
||||
return iab_msgs[code];
|
||||
}
|
||||
|
||||
|
||||
// Checks that setup was done; if not, throws an exception.
|
||||
void checkSetupDone(String operation) {
|
||||
if (!mSetupDone) {
|
||||
logError("Illegal state for operation (" + operation + "): IAB helper is not set up.");
|
||||
throw new IllegalStateException("IAB helper is not set up. Can't perform operation: " + operation);
|
||||
}
|
||||
}
|
||||
|
||||
// Workaround to bug where sometimes response codes come as Long instead of Integer
|
||||
int getResponseCodeFromBundle(Bundle b) {
|
||||
Object o = b.get(RESPONSE_CODE);
|
||||
if (o == null) {
|
||||
logDebug("Bundle with null response code, assuming OK (known issue)");
|
||||
return BILLING_RESPONSE_RESULT_OK;
|
||||
}
|
||||
else if (o instanceof Integer) return ((Integer)o).intValue();
|
||||
else if (o instanceof Long) return (int)((Long)o).longValue();
|
||||
else {
|
||||
logError("Unexpected type for bundle response code.");
|
||||
logError(o.getClass().getName());
|
||||
throw new RuntimeException("Unexpected type for bundle response code: " + o.getClass().getName());
|
||||
}
|
||||
}
|
||||
|
||||
// Workaround to bug where sometimes response codes come as Long instead of Integer
|
||||
int getResponseCodeFromIntent(Intent i) {
|
||||
Object o = i.getExtras().get(RESPONSE_CODE);
|
||||
if (o == null) {
|
||||
logError("Intent with no response code, assuming OK (known issue)");
|
||||
return BILLING_RESPONSE_RESULT_OK;
|
||||
}
|
||||
else if (o instanceof Integer) return ((Integer)o).intValue();
|
||||
else if (o instanceof Long) return (int)((Long)o).longValue();
|
||||
else {
|
||||
logError("Unexpected type for intent response code.");
|
||||
logError(o.getClass().getName());
|
||||
throw new RuntimeException("Unexpected type for intent response code: " + o.getClass().getName());
|
||||
}
|
||||
}
|
||||
|
||||
void flagStartAsync(String operation) {
|
||||
if (mAsyncInProgress) throw new IllegalStateException("Can't start async operation (" +
|
||||
operation + ") because another async operation(" + mAsyncOperation + ") is in progress.");
|
||||
mAsyncOperation = operation;
|
||||
mAsyncInProgress = true;
|
||||
logDebug("Starting async operation: " + operation);
|
||||
}
|
||||
|
||||
void flagEndAsync() {
|
||||
logDebug("Ending async operation: " + mAsyncOperation);
|
||||
mAsyncOperation = "";
|
||||
mAsyncInProgress = false;
|
||||
}
|
||||
|
||||
|
||||
int queryPurchases(Inventory inv, String itemType) throws JSONException, RemoteException {
|
||||
// Query purchases
|
||||
logDebug("Querying owned items, item type: " + itemType);
|
||||
logDebug("Package name: " + mContext.getPackageName());
|
||||
boolean verificationFailed = false;
|
||||
String continueToken = null;
|
||||
|
||||
do {
|
||||
logDebug("Calling getPurchases with continuation token: " + continueToken);
|
||||
Bundle ownedItems = mService.getPurchases(3, mContext.getPackageName(),
|
||||
itemType, continueToken);
|
||||
|
||||
int response = getResponseCodeFromBundle(ownedItems);
|
||||
logDebug("Owned items response: " + String.valueOf(response));
|
||||
if (response != BILLING_RESPONSE_RESULT_OK) {
|
||||
logDebug("getPurchases() failed: " + getResponseDesc(response));
|
||||
return response;
|
||||
}
|
||||
if (!ownedItems.containsKey(RESPONSE_INAPP_ITEM_LIST)
|
||||
|| !ownedItems.containsKey(RESPONSE_INAPP_PURCHASE_DATA_LIST)
|
||||
|| !ownedItems.containsKey(RESPONSE_INAPP_SIGNATURE_LIST)) {
|
||||
logError("Bundle returned from getPurchases() doesn't contain required fields.");
|
||||
return IABHELPER_BAD_RESPONSE;
|
||||
}
|
||||
|
||||
ArrayList<String> ownedSkus = ownedItems.getStringArrayList(
|
||||
RESPONSE_INAPP_ITEM_LIST);
|
||||
ArrayList<String> purchaseDataList = ownedItems.getStringArrayList(
|
||||
RESPONSE_INAPP_PURCHASE_DATA_LIST);
|
||||
ArrayList<String> signatureList = ownedItems.getStringArrayList(
|
||||
RESPONSE_INAPP_SIGNATURE_LIST);
|
||||
|
||||
for (int i = 0; i < purchaseDataList.size(); ++i) {
|
||||
String purchaseData = purchaseDataList.get(i);
|
||||
String signature = signatureList.get(i);
|
||||
String sku = ownedSkus.get(i);
|
||||
if (Security.verifyPurchase(mSignatureBase64, purchaseData, signature)) {
|
||||
logDebug("Sku is owned: " + sku);
|
||||
Purchase purchase = new Purchase(itemType, purchaseData, signature);
|
||||
|
||||
if (TextUtils.isEmpty(purchase.getToken())) {
|
||||
logWarn("BUG: empty/null token!");
|
||||
logDebug("Purchase data: " + purchaseData);
|
||||
}
|
||||
|
||||
// Record ownership and token
|
||||
inv.addPurchase(purchase);
|
||||
}
|
||||
else {
|
||||
logWarn("Purchase signature verification **FAILED**. Not adding item.");
|
||||
logDebug(" Purchase data: " + purchaseData);
|
||||
logDebug(" Signature: " + signature);
|
||||
verificationFailed = true;
|
||||
}
|
||||
}
|
||||
|
||||
continueToken = ownedItems.getString(INAPP_CONTINUATION_TOKEN);
|
||||
logDebug("Continuation token: " + continueToken);
|
||||
} while (!TextUtils.isEmpty(continueToken));
|
||||
|
||||
return verificationFailed ? IABHELPER_VERIFICATION_FAILED : BILLING_RESPONSE_RESULT_OK;
|
||||
}
|
||||
|
||||
int querySkuDetails(String itemType, Inventory inv, List<String> moreSkus)
|
||||
throws RemoteException, JSONException {
|
||||
logDebug("Querying SKU details.");
|
||||
ArrayList<String> skuList = new ArrayList<String>();
|
||||
skuList.addAll(inv.getAllOwnedSkus(itemType));
|
||||
if (moreSkus != null) {
|
||||
for (String sku : moreSkus) {
|
||||
if (!skuList.contains(sku)) {
|
||||
skuList.add(sku);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (skuList.size() == 0) {
|
||||
logDebug("queryPrices: nothing to do because there are no SKUs.");
|
||||
return BILLING_RESPONSE_RESULT_OK;
|
||||
}
|
||||
|
||||
Bundle querySkus = new Bundle();
|
||||
querySkus.putStringArrayList(GET_SKU_DETAILS_ITEM_LIST, skuList);
|
||||
Bundle skuDetails = mService.getSkuDetails(3, mContext.getPackageName(),
|
||||
itemType, querySkus);
|
||||
|
||||
if (!skuDetails.containsKey(RESPONSE_GET_SKU_DETAILS_LIST)) {
|
||||
int response = getResponseCodeFromBundle(skuDetails);
|
||||
if (response != BILLING_RESPONSE_RESULT_OK) {
|
||||
logDebug("getSkuDetails() failed: " + getResponseDesc(response));
|
||||
return response;
|
||||
}
|
||||
else {
|
||||
logError("getSkuDetails() returned a bundle with neither an error nor a detail list.");
|
||||
return IABHELPER_BAD_RESPONSE;
|
||||
}
|
||||
}
|
||||
|
||||
ArrayList<String> responseList = skuDetails.getStringArrayList(
|
||||
RESPONSE_GET_SKU_DETAILS_LIST);
|
||||
|
||||
for (String thisResponse : responseList) {
|
||||
SkuDetails d = new SkuDetails(itemType, thisResponse);
|
||||
logDebug("Got sku details: " + d);
|
||||
inv.addSkuDetails(d);
|
||||
}
|
||||
return BILLING_RESPONSE_RESULT_OK;
|
||||
}
|
||||
|
||||
|
||||
void consumeAsyncInternal(final List<Purchase> purchases,
|
||||
final OnConsumeFinishedListener singleListener,
|
||||
final OnConsumeMultiFinishedListener multiListener) {
|
||||
final Handler handler = new Handler();
|
||||
flagStartAsync("consume");
|
||||
(new Thread(new Runnable() {
|
||||
public void run() {
|
||||
final List<IabResult> results = new ArrayList<IabResult>();
|
||||
for (Purchase purchase : purchases) {
|
||||
try {
|
||||
consume(purchase);
|
||||
results.add(new IabResult(BILLING_RESPONSE_RESULT_OK, "Successful consume of sku " + purchase.getSku()));
|
||||
}
|
||||
catch (IabException ex) {
|
||||
results.add(ex.getResult());
|
||||
}
|
||||
}
|
||||
|
||||
flagEndAsync();
|
||||
if (!mDisposed && singleListener != null) {
|
||||
handler.post(new Runnable() {
|
||||
public void run() {
|
||||
singleListener.onConsumeFinished(purchases.get(0), results.get(0));
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!mDisposed && multiListener != null) {
|
||||
handler.post(new Runnable() {
|
||||
public void run() {
|
||||
multiListener.onConsumeMultiFinished(purchases, results);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
})).start();
|
||||
}
|
||||
|
||||
void logDebug(String msg) {
|
||||
if (mDebugLog) Log.d(mDebugTag, msg);
|
||||
}
|
||||
|
||||
void logError(String msg) {
|
||||
Log.e(mDebugTag, "In-app billing error: " + msg);
|
||||
}
|
||||
|
||||
void logWarn(String msg) {
|
||||
Log.w(mDebugTag, "In-app billing warning: " + msg);
|
||||
}
|
||||
}
|
45
app/src/main/java/org/billinghack/google/util/IabResult.java
Normal file
|
@ -0,0 +1,45 @@
|
|||
/* Copyright (c) 2012 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;
|
||||
|
||||
/**
|
||||
* Represents the result of an in-app billing operation.
|
||||
* A result is composed of a response code (an integer) and possibly a
|
||||
* message (String). You can get those by calling
|
||||
* {@link #getResponse} and {@link #getMessage()}, respectively. You
|
||||
* can also inquire whether a result is a success or a failure by
|
||||
* calling {@link #isSuccess()} and {@link #isFailure()}.
|
||||
*/
|
||||
public class IabResult {
|
||||
int mResponse;
|
||||
String mMessage;
|
||||
|
||||
public IabResult(int response, String message) {
|
||||
mResponse = response;
|
||||
if (message == null || message.trim().length() == 0) {
|
||||
mMessage = IabHelper.getResponseDesc(response);
|
||||
}
|
||||
else {
|
||||
mMessage = message + " (response: " + IabHelper.getResponseDesc(response) + ")";
|
||||
}
|
||||
}
|
||||
public int getResponse() { return mResponse; }
|
||||
public String getMessage() { return mMessage; }
|
||||
public boolean isSuccess() { return mResponse == IabHelper.BILLING_RESPONSE_RESULT_OK; }
|
||||
public boolean isFailure() { return !isSuccess(); }
|
||||
public String toString() { return "IabResult: " + getMessage(); }
|
||||
}
|
||||
|
91
app/src/main/java/org/billinghack/google/util/Inventory.java
Normal file
|
@ -0,0 +1,91 @@
|
|||
/* Copyright (c) 2012 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;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Represents a block of information about in-app items.
|
||||
* An Inventory is returned by such methods as {@link IabHelper#queryInventory}.
|
||||
*/
|
||||
public class Inventory {
|
||||
Map<String,SkuDetails> mSkuMap = new HashMap<String,SkuDetails>();
|
||||
Map<String,Purchase> mPurchaseMap = new HashMap<String,Purchase>();
|
||||
|
||||
Inventory() { }
|
||||
|
||||
/** Returns the listing details for an in-app product. */
|
||||
public SkuDetails getSkuDetails(String sku) {
|
||||
return mSkuMap.get(sku);
|
||||
}
|
||||
|
||||
/** Returns purchase information for a given product, or null if there is no purchase. */
|
||||
public Purchase getPurchase(String sku) {
|
||||
return mPurchaseMap.get(sku);
|
||||
}
|
||||
|
||||
/** Returns whether or not there exists a purchase of the given product. */
|
||||
public boolean hasPurchase(String sku) {
|
||||
return mPurchaseMap.containsKey(sku);
|
||||
}
|
||||
|
||||
/** Return whether or not details about the given product are available. */
|
||||
public boolean hasDetails(String sku) {
|
||||
return mSkuMap.containsKey(sku);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erase a purchase (locally) from the inventory, given its product ID. This just
|
||||
* modifies the Inventory object locally and has no effect on the server! This is
|
||||
* useful when you have an existing Inventory object which you know to be up to date,
|
||||
* and you have just consumed an item successfully, which means that erasing its
|
||||
* purchase data from the Inventory you already have is quicker than querying for
|
||||
* a new Inventory.
|
||||
*/
|
||||
public void erasePurchase(String sku) {
|
||||
if (mPurchaseMap.containsKey(sku)) mPurchaseMap.remove(sku);
|
||||
}
|
||||
|
||||
/** Returns a list of all owned product IDs. */
|
||||
List<String> getAllOwnedSkus() {
|
||||
return new ArrayList<String>(mPurchaseMap.keySet());
|
||||
}
|
||||
|
||||
/** Returns a list of all owned product IDs of a given type */
|
||||
List<String> getAllOwnedSkus(String itemType) {
|
||||
List<String> result = new ArrayList<String>();
|
||||
for (Purchase p : mPurchaseMap.values()) {
|
||||
if (p.getItemType().equals(itemType)) result.add(p.getSku());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Returns a list of all purchases. */
|
||||
List<Purchase> getAllPurchases() {
|
||||
return new ArrayList<Purchase>(mPurchaseMap.values());
|
||||
}
|
||||
|
||||
void addSkuDetails(SkuDetails d) {
|
||||
mSkuMap.put(d.getSku(), d);
|
||||
}
|
||||
|
||||
void addPurchase(Purchase p) {
|
||||
mPurchaseMap.put(p.getSku(), p);
|
||||
}
|
||||
}
|
63
app/src/main/java/org/billinghack/google/util/Purchase.java
Normal file
|
@ -0,0 +1,63 @@
|
|||
/* Copyright (c) 2012 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;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
/**
|
||||
* Represents an in-app billing purchase.
|
||||
*/
|
||||
public class Purchase {
|
||||
String mItemType; // ITEM_TYPE_INAPP or ITEM_TYPE_SUBS
|
||||
String mOrderId;
|
||||
String mPackageName;
|
||||
String mSku;
|
||||
long mPurchaseTime;
|
||||
int mPurchaseState;
|
||||
String mDeveloperPayload;
|
||||
String mToken;
|
||||
String mOriginalJson;
|
||||
String mSignature;
|
||||
|
||||
public Purchase(String itemType, String jsonPurchaseInfo, String signature) throws JSONException {
|
||||
mItemType = itemType;
|
||||
mOriginalJson = jsonPurchaseInfo;
|
||||
JSONObject o = new JSONObject(mOriginalJson);
|
||||
mOrderId = o.optString("orderId");
|
||||
mPackageName = o.optString("packageName");
|
||||
mSku = o.optString("productId");
|
||||
mPurchaseTime = o.optLong("purchaseTime");
|
||||
mPurchaseState = o.optInt("purchaseState");
|
||||
mDeveloperPayload = o.optString("developerPayload");
|
||||
mToken = o.optString("token", o.optString("purchaseToken"));
|
||||
mSignature = signature;
|
||||
}
|
||||
|
||||
public String getItemType() { return mItemType; }
|
||||
public String getOrderId() { return mOrderId; }
|
||||
public String getPackageName() { return mPackageName; }
|
||||
public String getSku() { return mSku; }
|
||||
public long getPurchaseTime() { return mPurchaseTime; }
|
||||
public int getPurchaseState() { return mPurchaseState; }
|
||||
public String getDeveloperPayload() { return mDeveloperPayload; }
|
||||
public String getToken() { return mToken; }
|
||||
public String getOriginalJson() { return mOriginalJson; }
|
||||
public String getSignature() { return mSignature; }
|
||||
|
||||
@Override
|
||||
public String toString() { return "PurchaseInfo(type:" + mItemType + "):" + mOriginalJson; }
|
||||
}
|
130
app/src/main/java/org/billinghack/google/util/Security.java
Normal file
|
@ -0,0 +1,130 @@
|
|||
/* Copyright (c) 2012 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;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Signature;
|
||||
import java.security.SignatureException;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
|
||||
/**
|
||||
* Security-related methods. For a secure implementation, all of this code
|
||||
* should be implemented on a server that communicates with the
|
||||
* application on the device. For the sake of simplicity and clarity of this
|
||||
* example, this code is included here and is executed on the device. If you
|
||||
* must verify the purchases on the phone, you should obfuscate this code to
|
||||
* make it harder for an attacker to replace the code with stubs that treat all
|
||||
* purchases as verified.
|
||||
*/
|
||||
public class Security {
|
||||
private static final String TAG = "IABUtil/Security";
|
||||
|
||||
private static final String KEY_FACTORY_ALGORITHM = "RSA";
|
||||
private static final String SIGNATURE_ALGORITHM = "SHA1withRSA";
|
||||
|
||||
/**
|
||||
* Verifies that the data was signed with the given signature, and returns
|
||||
* the verified purchase. The data is in JSON format and signed
|
||||
* with a private key. The data also contains the {@link PurchaseState}
|
||||
* and product ID of the purchase.
|
||||
* @param base64PublicKey the base64-encoded public key to use for verifying.
|
||||
* @param signedData the signed JSON string (signed, not encrypted)
|
||||
* @param signature the signature for the data, signed with the private key
|
||||
*/
|
||||
public static boolean verifyPurchase(String base64PublicKey, String signedData, String signature) {
|
||||
if (signedData == null) {
|
||||
Log.e(TAG, "data is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean verified = false;
|
||||
if (!TextUtils.isEmpty(signature)) {
|
||||
PublicKey key = Security.generatePublicKey(base64PublicKey);
|
||||
verified = Security.verify(key, signedData, signature);
|
||||
if (!verified) {
|
||||
Log.w(TAG, "signature does not match data.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a PublicKey instance from a string containing the
|
||||
* Base64-encoded public key.
|
||||
*
|
||||
* @param encodedPublicKey Base64-encoded public key
|
||||
* @throws IllegalArgumentException if encodedPublicKey is invalid
|
||||
*/
|
||||
public static PublicKey generatePublicKey(String encodedPublicKey) {
|
||||
try {
|
||||
byte[] decodedKey = Base64.decode(encodedPublicKey);
|
||||
KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
|
||||
return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey));
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (InvalidKeySpecException e) {
|
||||
Log.e(TAG, "Invalid key specification.");
|
||||
throw new IllegalArgumentException(e);
|
||||
} catch (Base64DecoderException e) {
|
||||
Log.e(TAG, "Base64 decoding failed.");
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the signature from the server matches the computed
|
||||
* signature on the data. Returns true if the data is correctly signed.
|
||||
*
|
||||
* @param publicKey public key associated with the developer account
|
||||
* @param signedData signed data from server
|
||||
* @param signature server signature
|
||||
* @return true if the data and signature match
|
||||
*/
|
||||
public static boolean verify(PublicKey publicKey, String signedData, String signature) {
|
||||
Signature sig;
|
||||
try {
|
||||
sig = Signature.getInstance(SIGNATURE_ALGORITHM);
|
||||
sig.initVerify(publicKey);
|
||||
sig.update(signedData.getBytes());
|
||||
if (!sig.verify(Base64.decode(signature))) {
|
||||
Log.e(TAG, "Signature verification failed.");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
Log.e(TAG, "NoSuchAlgorithmException.");
|
||||
} catch (InvalidKeyException e) {
|
||||
Log.e(TAG, "Invalid key specification.");
|
||||
} catch (SignatureException e) {
|
||||
Log.e(TAG, "Signature exception.");
|
||||
} catch (Base64DecoderException e) {
|
||||
Log.e(TAG, "Base64 decoding failed.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
/* Copyright (c) 2012 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;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
/**
|
||||
* Represents an in-app product's listing details.
|
||||
*/
|
||||
public class SkuDetails {
|
||||
String mItemType;
|
||||
String mSku;
|
||||
String mType;
|
||||
String mPrice;
|
||||
String mTitle;
|
||||
String mDescription;
|
||||
String mJson;
|
||||
|
||||
public SkuDetails(String jsonSkuDetails) throws JSONException {
|
||||
this(IabHelper.ITEM_TYPE_INAPP, jsonSkuDetails);
|
||||
}
|
||||
|
||||
public SkuDetails(String itemType, String jsonSkuDetails) throws JSONException {
|
||||
mItemType = itemType;
|
||||
mJson = jsonSkuDetails;
|
||||
JSONObject o = new JSONObject(mJson);
|
||||
mSku = o.optString("productId");
|
||||
mType = o.optString("type");
|
||||
mPrice = o.optString("price");
|
||||
mTitle = o.optString("title");
|
||||
mDescription = o.optString("description");
|
||||
}
|
||||
|
||||
public String getSku() { return mSku; }
|
||||
public String getType() { return mType; }
|
||||
public String getPrice() { return mPrice; }
|
||||
public String getTitle() { return mTitle; }
|
||||
public String getDescription() { return mDescription; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SkuDetails:" + mJson;
|
||||
}
|
||||
}
|
30
app/src/main/res/drawable-v24/ic_launcher_foreground.xml
Normal file
|
@ -0,0 +1,30 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
170
app/src/main/res/drawable/ic_launcher_background.xml
Normal file
|
@ -0,0 +1,170 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
18
app/src/main/res/layout/activity_main.xml
Normal file
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Hello World!"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
37
app/src/main/res/layout/buy_dialog.xml
Normal file
|
@ -0,0 +1,37 @@
|
|||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
tools:context=".MainActivity" >
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:text="Do you want to get this item for free? :D" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_no"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_below="@+id/textView1"
|
||||
android:text="@android:string/cancel" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_yes"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignBaseline="@+id/button_no"
|
||||
android:layout_alignBottom="@+id/button_no"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_toEndOf="@+id/button_no"
|
||||
android:text="@android:string/ok" />
|
||||
|
||||
</RelativeLayout>
|
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 5.2 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 7.3 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 7.7 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 16 KiB |
6
app/src/main/res/values/colors.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="colorPrimary">#6200EE</color>
|
||||
<color name="colorPrimaryDark">#3700B3</color>
|
||||
<color name="colorAccent">#03DAC5</color>
|
||||
</resources>
|
5
app/src/main/res/values/dimens.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<dimen name="activity_vertical_margin">16dp</dimen>
|
||||
<dimen name="activity_horizontal_margin">16dp</dimen>
|
||||
</resources>
|
3
app/src/main/res/values/strings.xml
Normal file
|
@ -0,0 +1,3 @@
|
|||
<resources>
|
||||
<string name="app_name">BillingHack</string>
|
||||
</resources>
|
11
app/src/main/res/values/styles.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<resources>
|
||||
|
||||
<!-- Base application theme. -->
|
||||
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
<item name="colorAccent">@color/colorAccent</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
17
app/src/test/java/org/billinghack/ExampleUnitTest.java
Normal file
|
@ -0,0 +1,17 @@
|
|||
package org.billinghack;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||
*/
|
||||
public class ExampleUnitTest {
|
||||
@Test
|
||||
public void addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2);
|
||||
}
|
||||
}
|
29
build.gradle
Normal file
|
@ -0,0 +1,29 @@
|
|||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.6.3'
|
||||
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
20
gradle.properties
Normal file
|
@ -0,0 +1,20 @@
|
|||
# Project-wide Gradle settings.
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx1536m
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app's APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
# Automatically convert third-party libraries to use AndroidX
|
||||
android.enableJetifier=true
|
||||
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
#Sat Apr 18 19:33:11 CEST 2020
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
|
172
gradlew
vendored
Executable file
|
@ -0,0 +1,172 @@
|
|||
#!/usr/bin/env sh
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS=""
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=$((i+1))
|
||||
done
|
||||
case $i in
|
||||
(0) set -- ;;
|
||||
(1) set -- "$args0" ;;
|
||||
(2) set -- "$args0" "$args1" ;;
|
||||
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=$(save "$@")
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
|
||||
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
|
||||
cd "$(dirname "$0")"
|
||||
fi
|
||||
|
||||
exec "$JAVACMD" "$@"
|
84
gradlew.bat
vendored
Normal file
|
@ -0,0 +1,84 @@
|
|||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS=
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windows variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
2
settings.gradle
Normal file
|
@ -0,0 +1,2 @@
|
|||
rootProject.name='BillingHack'
|
||||
include ':app'
|